Compare commits

...
264 Commits
Author SHA1 Message Date
Jason Volk 64a029ee09 Bump 1.4.6
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 19:34:51 +00:00
Jason Volk a3f6971579 docker: Add rust-sdk memcheck target.
docker: Separate and tweak cargo cache paths.

ci: Improve memcheck job targets.

cargo: Add missing debug syms to bench profile.

docker: Optimize layer copying.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 19:34:51 +00:00
Jason Volk 51681aec1b docker: Bump complement.
ci: Run complement with optimizations.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk 39c84fabb4 Optimize filter fetch in syncv3 prologue.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk f77bd41837 Add empty config default for rtc_transports to not generate false.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk 6f34b8e9ca Fix missing pushrules evaluation on federation invite.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk 5051da493a Fix pushrule evals relying on non-option RoomPowerLevels.
Add option to bypass pushrules to send everything to pushers.

Improve robustness of pushrule eval loops.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk ff0e007c45 Fix readability of admin query account_data outputs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Alexandru Scvorțov b85fb5ea6f Fix tests in nix and re-enable 2025-11-06 16:38:13 +00:00
dasha_uwuandJason Volk e905538269 Shuffle additional join servers. 2025-11-04 06:40:51 +00:00
Jason Volk 6f672b7304 Handle missing media dir as individual item errors w/o aborting check. (closes #213)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-04 06:40:51 +00:00
Jason Volk 4363ed6ec3 Adapt MSC4143 rtc_foci support to Ruma types. (e1f89b69ea)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-04 06:40:51 +00:00
Jason Volk dd50a4cb0b Fix ignored column descriptions; enhance preservation behavior. (7320d0a40b)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 23:02:42 +00:00
tototomate123andJason Volk e0a997c227 docs(reverse-proxy): restructure and add nginx guide
- Moved Caddy and Nginx configuration examples to separate files
- Added documentation for Nginx setup
- Updated generic reverse proxy section
2025-11-03 12:49:04 +00:00
tototomate123andJason Volk e1f89b69ea feat: Add Element Call / MatrixRTC support 2025-11-03 11:36:19 +00:00
dasha_uwuandJason Volk 888f72d8d0 Generalize common_prefix for AsStr inputs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 11:36:19 +00:00
Jason Volk 06618eadab Re-establish federating with several Conduit endpoints.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 11:36:19 +00:00
Benedikt Vollmerhaus 05390d6097 Fix shared TURN secret from turn_secret_file not being used 2025-11-03 12:30:00 +01:00
Jason Volk 1f803fe3a9 Fix verification loss; workaround Nheko-Reborn/nheko#1908 (closes #146)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 04:45:31 +00:00
Jason Volk 1492d68e25 Additional tracing of request body.
Optimize router request parse types.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 04:45:31 +00:00
Jason Volk c1aa94fb91 Describe roomsynctoken_shortstatehash for deletion.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 02:05:35 +00:00
Jason Volk 7320d0a40b Improve config options for missing and dropping db columns.
Implement actual drop functionality.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 02:05:35 +00:00
Jason Volk abded2d442 Allow guests even when token required for normal registration. (closes #189)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 18:19:11 +00:00
Jason Volk 4afd6f347b Implement GET /_matrix/client/v3/events live room previews.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 08:06:32 +00:00
Jason Volk 6b8d6956a3 Fix timeline events order in /initialSync.
Implement acccount_data for response.

Ensure consistency of revealed events.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 08:06:32 +00:00
Jason Volk f59d62c01c Bump Ruma for CanonicalJson property name optimizations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 03:57:11 +00:00
Jason Volk a14556da97 Implement encryption_enabled_by_default_for_room_type. (closes #29)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk 8b1de3d8db Add update for device last_seen_ts. (closes #40)
Piggyback on presence ping for controlled device seen updates.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk 240b498489 Add unwrap suite to future::OptionExt extensions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk d680a6ba53 Bump Ruma for device type optimizations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk aa3f14cd57 Optimize federation user devices response.
Modernize some error branches.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-01 23:15:37 +00:00
Jason Volk 15627bc8d0 Simplify device metadata update interface.
Add broadcast argument; reorder notify to after write.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-01 23:15:37 +00:00
Jason Volk 084facf474 Move user profile related functions to profile unit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-01 23:15:37 +00:00
Jason Volk d24986edf1 sliding-sync: Stop propagation from room handlers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-31 03:38:35 +00:00
Jason Volk ce1ac277a6 Refactor sync v3 leave handler.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-31 03:38:35 +00:00
Jason Volk 7aeed0a95a Fix next_batch pagination tokens in relations response.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-31 03:38:35 +00:00
Jason Volk 9265748a57 Remove legacy ruma return type on sync endpoint.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 16:28:27 +00:00
Jason Volk e85cfdf48a Remove errant inline. (48923b3657)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 14:01:57 +00:00
Jason Volk 48923b3657 Implement notifications retrieval. (closes #201)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk aedaf3f0c1 Fix pushing useless notifications.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk b24b59dc38 Add admin query raw command to clear table.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 738b5e3fa5 Add attempted newtype struct name to deserialization error message.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 1f0cfec5ca Use Result for last_notification_read as zero-default not appropriate.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 9c5caa3a5f Move appservice request unit from sender to appservice.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 1cf4a26ae9 docker: Use prefix match for nightly rust_toolchain settings.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 3694ffbab3 Bump Ruma w/ PushRule type optimizations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-28 23:23:27 +00:00
Jason Volk af0e01e016 Add util to assist with Display/to_string for SmallString.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-28 23:23:27 +00:00
Jason Volk 5e89f0acae Split pusher service send/request into unit.
Refactor sender's push destination handler.

Combine remnants of service::rooms::user with pusher service.

Further split and reorg pusher service units.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-28 23:23:19 +00:00
Jason Volk 563873af77 Use signed PduCount for better bumpstamp on create and backfill.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 15:24:50 +00:00
Jason Volk d2072080c9 Add admin query for remote alias resolve.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 15:24:17 +00:00
Jason Volk e191730950 Tracing span adjustments for ret verbosity.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 15:15:12 +00:00
Jason Volk f660e00bb5 Implement Dehydrated Devices MSC3814 (closes #200)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 12:06:43 +00:00
Jason Volk f613d0c2ad Add admin command to delete a user's device.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 10:55:12 +00:00
Jason Volk 7596ad2019 Add rountripping test for database cbor serialization.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 10:41:49 +00:00
Jason Volk 384add9784 ci: Extract Nix artifacts.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:39:57 +00:00
Jason Volk a234f019b1 Advertise MSC2659 support; reorder features by MSC ascending.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk e8a87bdfa3 Remove DynamicUser from systemd files (closes #207).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 6a4aff424f Add count value to the to_device iter item.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 8959d9e2c1 Add bulk one_time_keys adder to interface.
Add device_exists to interface.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk a30c043386 Support creating devices without access_tokens.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk a39ef994d2 Add is_err_or to Result utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk b714f24029 sliding-sync: Filter redacted events from bump calculation.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 2263f2e874 docker: Add dirty-runs option passed to complement.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 9abe9becd6 Split pusher/push-rule from timeline append.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 4b74c01895 Split appservice dissemination from timeline::append().
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 1223763e2b Split sliding-sync into additional units.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 9b64c1f105 Bump Nix flake. 2025-10-27 09:06:02 +00:00
Jason Volk 212a8434a8 Bump Rust 1.89
We can't upgrade to 1.90 because NixOS breaks. 🙂‍↔️

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk bfaac8b5a2 Bump dependencies.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 17:46:25 +00:00
Jason Volk 9af15ecbba Bump 1.4.5
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 13:33:32 +00:00
Jason Volk f66a83763e Persist sliding-sync state; mitigate initial-sync.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 13:33:32 +00:00
Jason Volk 718c3adcb2 ci: Expand the rust-sdk integration matrix.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 08:44:16 +00:00
Jason Volk af80482c04 Add branch in room handler for left rooms.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 08:34:49 +00:00
Jason Volk a20ddcd586 Recency bump on own membership event.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 08:08:26 +00:00
Jason Volk b483306367 Fix sliding-sync invite regression.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 07:52:40 +00:00
Jason Volk f5e98467be Improve sliding-sync robustness to deeper replays.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-24 18:31:48 +00:00
Jason Volk d44db45f83 Adjust the order of operations during read_marker handling.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-24 18:31:48 +00:00
Jason Volk 0397bb8237 Implement specified non-blocking semantic for sliding-sync.
Simplify sliding-sync watch loop and bounds.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 11:00:55 +00:00
Jason Volk 4010fc62bc Use take_while rather than filter for scanning last_receipt_count.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 03:37:52 +00:00
Jason Volk 0d823a2822 Revert "Involve m.fully_read for resetting notification counts."
This reverts commit 0df5e5e7ac.
2025-10-23 03:37:52 +00:00
Jason Volk 9cd175b125 Additional logging/tracing tweaks around sliding-sync.
Additional spans around receipt service interface.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 03:37:52 +00:00
Jason Volk 050a1a350a Add dbg profile to Cargo; default to faster builds accomodating new users. 2025-10-23 03:37:52 +00:00
Jason Volk ec0f872f8f Add tracing around db watcher notify including cf name.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 20:23:30 +00:00
Jason Volk 024e8eae62 Implement num_live for sliding-sync room response.
Avoid using origin_server_ts for recency stamp.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 09:14:41 +00:00
Jason Volk 4fd60b2605 Improve sliding-sync selector and windowing logic.
Bump Ruma; eliminate premature release compat.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 08:15:25 +00:00
Jason Volk e53968d9eb Improve tracing spans for notif counts and sliding-sync exts.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 07:05:03 +00:00
Jason Volk 7cbc2ee385 Fix the accidentally dual-keyspaced cf from conduit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 07:03:10 +00:00
Jason Volk 0df5e5e7ac Involve m.fully_read for resetting notification counts.
EXA is not reliably removing its unread marker any other way. Clients
wishing to move the fully_read marker in the other direction should
file an issue to make themselves known.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 06:52:30 +00:00
Jason Volk 312eb69450 Add tool to get sibling connections for a device.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 17:48:08 +00:00
Jason Volk 5b620a2c37 Add .and_is() / .and_if() to the BoolExt smalltalk.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 17:41:42 +00:00
Jason Volk 3b4fbb8c1a Fix inverted debug assert message.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 15:48:05 +00:00
Jason Volk 158d44e1a9 docs: Remove line about other reverse proxies. (#197)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 15:02:36 +00:00
Jason Volk d2aab468cf Fixes for sliding-sync request replays.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 06:46:02 +00:00
Jason Volk aa4486dfdf Simplify types, mitigate expansion; eliminate unnecessary move.
Further simplify future::BoolExt toward type expansion mitigation.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 05:25:11 +00:00
Jason Volk 2a662445b6 Make account_data.last_count() upper bound argument optional.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 23:58:38 +00:00
Jason Volk 5b3f0fde23 Add invite rooms to sync v3 watch rooms list.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 23:57:52 +00:00
Jason Volk 9640afebff Join the async prologue operations in sync v3.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 23:55:42 +00:00
Jason Volk dd5c5c7a4a Notify watchers on delete operations in addition to inserts. 2025-10-20 20:45:38 +00:00
Jason Volk fc0f04defa Optimize watcher range collection on notify.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 20:44:45 +00:00
Jason Volk 7d8f7cbe5d Bump 1.4.4
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-16 01:55:43 +00:00
Jason Volk 4e0249cd2f Fix sliding-sync again by resetting after interrupted requests. (fixes #190)
This solution is not elegant but appears to be correct for fixing the issue. A
a more efficient solution might be possible without resetting the connection
which should be further investigated.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-16 01:05:16 +00:00
Jason Volk d5b39aa995 Bump 1.4.3
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-11 02:12:43 +00:00
Jason Volk 46c940b863 Refactor sliding window selector. (fixes #170)
Refactor list filtering.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 15:56:08 +00:00
Jason Volk ab8536d5c3 Add query to get latest account data count.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 12:44:23 +00:00
Jason Volk 4918868632 Add query to get last read receipt for room.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 12:44:23 +00:00
Jason Volk 9e00f70197 Allow nursery clippy::tuple_array_conversions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 08:30:08 +00:00
Jason Volk d3aaf9e4a9 Additional FutureBoolExt; attempt to reduce type-length expansion.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk 96dc56ad07 Relax clippy::rest-pat-in-fully-bound-structs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk e12b0262da Fix unnecessary async for future and/or utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk e5bf005eaf Simplify state_cache.user_memberships() masking for callers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk 02ccb1dceb Cleanup legacy config path component.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk a450eb96eb ci: Remove default-features builds from publication to registries.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk 155af0fda3 Add iteration of space children; simplify space child event fetcher.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk 5c61b4d4a3 Add room state accessor iterating state_keys joined by respective event_id.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-10 07:37:40 +00:00
Jason Volk 97e709492c Add is_direct() alternative query for m.room.member.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk 51fc2342a4 Add is_direct() query for account_data.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk 4ca68deef8 Add better interface for getting and setting room tag account data.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk 4d8d64f5c7 Add missing wildcard matching for sliding-sync required state.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk dff30e5924 Additional state accessor iterations for pdus of a specific type.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk 7fee459b1a Add admin diagnostic query suite for sync state.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk a6127fcd1a Provide sync watch rooms as argument rather than assuming all joined rooms.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:12 +00:00
Jason Volk 6c724bbc2f Cleanup/optimize sliding sync types and arguments.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-08 03:33:08 +00:00
Jason Volk b1ea7b101d Split sliding-sync extensions into units.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 15:56:49 +00:00
Jason Volk 4baa25f66f Add presence ping to sliding-sync.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 15:56:49 +00:00
Jason Volk 227b77e58e Refactor sliding-sync e2ee extension.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 15:56:49 +00:00
Jason Volk 54057da84e ci: Fix artifact extraction argument for package.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 15:56:49 +00:00
Jason Volk 732825a390 ci: Publish verbose logging container builds to registries. (closes #181)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 04:11:57 +00:00
Jason Volk 6b74425f76 ci: Add integration memcheck to test matrix.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:22:02 +00:00
Jason Volk 74a576caf7 Add option to toggle compact log format.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 2b7cf7d5d5 Add optimized iteration of all room memberships for user.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 443248965d Add more optimized iterations for user memberships room_id's.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk ee777bc287 Rename user rooms iterations which have state vectors as values.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk aef38b1178 Rename userroomid/roomuserid table handles to reflect counter value.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 4743a8d968 Move args and runtime back to main from core (1313eb0b64).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 53ab6742c8 Fix shutdown signalling on startup.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 46193de7e8 Create appservice user on registration.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 9253f46c80 ci: Dedup the nix package job with the other distro package jobs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 5c127b5abd Add is_false() to BoolExt.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 17f6f1a5a6 Device list updates for left users was previously deduplicated.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 75509d50ca Add shorteventid to pdu_id query; reorg related id query interface.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jason Volk 1d14426018 Fix missing Send on generic stream and result items.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-06 01:19:36 +00:00
Jimmy Brush 2b9563be67 Fix off-by-one in sync v5
Simplified sliding sync specifies ranges to be inclusive while rust ranges are
exclusive.
2025-10-05 18:28:05 -04:00
Jason Volk 68c4f60bb3 Implement stateless sync; erase all the sync tokens.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 15:07:06 +00:00
Jason Volk d95c3f126f Fix typos
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 15:07:06 +00:00
Jason Volk f1c2548807 Add PduCount value to userroomid/roomuserid_joined; move PduCount to argument for update_membership.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk eda45e445c Implement better fmt::Debug for pdu::Builder.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 049defe977 nix: Disable doChecks due to missing /etc/resolve.conf.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 3c073110b8 Update nix flake.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 8d6bfde5a0 Use insta and criterion for main integration test and benches respectively.
docker/ci: Separate integration and unit tests and benches jobs.

Add directives to remove db before/after integration tests are performed.

Split start/run/stop phases; add more granular smoketests.

Split main integration tests into units for isolation.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
dasha_uwuandJason Volk 43f0882d83 clear destination and overrides cache on request failure. (#186) 2025-10-02 05:29:48 +00:00
Jason Volk fed52d24e4 Implement additional Matches for RoomFilter and Filter.
Apply filter for rooms/not_rooms; sender filter for presence.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk e6c85c97c6 Fix names and types misrepresenting PduCount as ShortEventId.
Add get_shorteventid_from_pdu_id() conversion.

Fix prev/next nearest-state interface (dev branch 642086ecfcfa).

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 368ead20a6 Add then_none() to BoolExt; move unsorted then_ utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk a803b84b27 Add missing reverse query for shortroomid => room_id.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 1058fbe9a7 Use specific key,val buffer sizes for append_to_state (no change to correctness).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk ae4aad3641 Add set difference for sorted streams util.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 95435ffe98 Fix unconnected inspect_log traits in Result utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 63e2cfa21b Rename core integration test directory for state_res.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk 9383922d09 Use trace log level for multi-line table factory options on rocksdb startup.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:48 +00:00
Jason Volk ae52676e33 Add back the default database path.
Allow default server_name when testing.

Default to smoke test vector when testing.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:45 +00:00
Jason Volk 292b601755 Fix possibly dependent conditions for server shutdown in config worker.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:29:00 +00:00
Jason Volk 1313eb0b64 Reorganize main crate for testability.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:28:58 +00:00
Jason Volk ba12773a5a Optimize force_state query pattern.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 83afe81f60 Move shortstatehash queries from state_accessor to state service.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk f2740822e2 De-indent rooms state service definitions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 2417764771 Fix redundant prefix in admin query raw command suite.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 41ff81f843 Add handler_prefix to admin_command_dispatch for method dedup purposes.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk fbcf4ba4f3 ci: Support job masking directives in commit message. [ci only it]
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason VolkandHashbrown777 af3cdf9263 Add script to docs for available optimization levels.
Co-authored-by: Hashbrown777 <hashbrown777@gmail.com>
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 9d9ace1452 Add timeline interface to get next or prev pdu nearest to a pduid.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 83d64e0879 Implement Deserialize for RawPduId for database convenience.
Add room equality convenience on RawPduId.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 4cae17e83d Fix missing Send bound for Future.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 1c6992ccd4 Add option to bypass dropped column erasure.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk 89be6dc097 Switch to Criterion for benchtests.
ci: Add benches to workflow.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk a47f8f8a82 Rename complement test results file.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-02 05:27:48 +00:00
Jason Volk aac5006bf5 ci: Add nix build w/ smoketest. (closes #111)
docker: Add target for nix build. (#111)

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-25 14:38:23 +00:00
Jason Volk 6d3ed09a2b ci: Add matrix-rust-sdk integration testing.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-25 14:38:23 +00:00
Jason Volk 377b7166f0 Bump Ruma.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-25 12:25:08 +00:00
Jason Volk 85b3de055d Broadcast send_join concurrent to other operations; pre-gather state concurrently.
Broadcast send_knock concurrently.

Concurrent access check for fed event.

Concurrent gather for state responses.

Populate room_version for format_pdu to elide repeated queries.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-25 02:32:45 +00:00
Jason Volk b9c790326a docker: Consolidate distro package fetches in base builder image.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-24 08:20:54 +00:00
Jason Volk 4e5910471b Require stronger ordering to sample final counter values on shutdown.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-24 08:20:54 +00:00
Jason Volk c3bc8c14f7 Make hickory system_conf error more useful. (resolves #179)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 21:02:37 +00:00
Jason Volk ac3ceb1b95 Modernize federation profile query route.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 19:48:07 +00:00
Jason Volk aa37e32471 Optimize Pdu prev_events and auth_events containers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 18:16:24 +00:00
Jason Volk 887a22dabd Fix docs regarding optimized builds; replace haswell with v1/v2/v3 etc. (fixes #175)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 18:16:24 +00:00
Jason Volk 6dca02860c Allow clippy::needless-update.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 11:53:39 +00:00
Jason Volkanddasha_uwu 86103183b3 Remove unused utils.
Co-authored-by: dasha_uwu <dasha@linuxping.win>
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 09:02:35 +00:00
Jason Volk 8f4cc87051 Query rooms in server_sees_user() concurrently.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 08:49:09 +00:00
Jason Volk 14721c90c9 Fetch presence avatar_url and displayname concurrently.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 08:49:09 +00:00
Jason Volk 6d3b2d864f Simplify keys_changed iteration using room_keys_changed() upper-bound.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 08:49:09 +00:00
Jason Volk 7cf246eb73 Support dot paths for -O/--option command-line args. (fixes #162) (fixes #167)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:16:29 +00:00
Jason Volk bffb7f89c8 Provide better generated config defaults for ldap section.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:16:29 +00:00
Jason Volk 5467c9e486 Add option to redirect logging to stderr. (resolves #154)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk 89a67af607 pull stuff out of globals 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk 6bb101ac51 add local alias check 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk 1c0b4e94ac fix spec violation and slight alias resolution refactor 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk 9c4d376bec pass args by ref 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk d6e95c51c0 current_exe changes
remove unsafe

remove incorrect doc
2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk d5ce4b316f remove unnecessary as_ref 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk 8f07a6c60f remove unused macros 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk b882e7efdb rename db to engine 2025-09-23 05:16:29 +00:00
dasha_uwuandJason Volk e0169e3dca presence refactor 2025-09-23 05:16:29 +00:00
Jason Volk fe46755418 Remove declaration for Room V1 until support is confirmed complete. (#12)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk bbac80d2e6 ci: Fix pages deployment. (#165)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk a31c9b0c62 Fix documented examples of delegation. (fixes #165)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk cf4e65c607 Fix alternative resolver minimum TTL configuration. (fixes #176)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk f32b6ae17d Abstract and simplify serde_json round-trip to Ruma Raw.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk 27222f23d2 Macroize several legacy errors.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk ed324d5972 Update device_signing/upload UIAA (fixes #169).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk 69075b166f Add config option to toggle dns case randomization.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk e3a711482a Update assertion for serde crate reorg to serde_core.
Show the deserialize_any type which fails the assertion.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-23 05:15:10 +00:00
Jason Volk 6759187b37 Bump dependencies.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-22 20:44:25 +00:00
Guido GüntherandJason Volk 83ed29eb65 example config: Document support items
Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-09-22 20:44:25 +00:00
Guido Günther 6a685b7ee9 packaging: Move unit alias to install section
Otherwise there will be

  systemd[1]: /usr/lib/systemd/system/tuwunel.service:5: Unknown key 'Alias' in section [Unit], ignoring.

on startup. See `systemd.unit(5)`.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-09-15 19:53:05 +02:00
Jason Volk fb4dfafe2d Bump 1.4.2
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:34 +00:00
Jason Volk 8dbea51968 Slightly decrease the default OTK limit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:34 +00:00
Jason Volk 7b5905bbca Remove inapposite readme sections on versioning.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:34 +00:00
Jason Volk 42d143c013 Allow matching for passthru resolver on all resolution paths. (#158)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:27 +00:00
Jason Volk 10fb1cd192 Purge room synctokens during deletion.
Purge last notification read counts.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 08:56:38 +00:00
Jason Volk 1322ba1b00 Send unmaintained hardened_malloc-rs to valhalla.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 08:56:38 +00:00
Jason Volk d73b4332bf Fix deletion of local leave state to allow sync for clients.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 06:20:41 +00:00
Jason Volk 11eeca7e01 Prune one time keys based on reasonable configured limit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 04:27:33 +00:00
Jason Volk 19dc5fafd4 Fix missing device_one_time_keys_count in empty timeout responses.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 21:29:04 +00:00
Jason Volk 54b347b855 Abstract and dedup the general UIAA pattern into api::router.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 10:35:49 +00:00
Jason Volk 8ed61aecb0 Split api::router::auth server/appservice units.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 05:41:44 +00:00
Jason Volk bf8aa57d03 Add config option for dns passthru for appservices. (#158)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 01:17:40 +00:00
Jason Volk a9f1926654 Use dns_resolver2 per reqwest client builder deprecation notice.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 00:59:50 +00:00
Jason Volk 248d778290 Add alternative resolver path with passthru cache-characteristics. (resolves #158)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 00:01:26 +00:00
Jason Volk 2e13e87e43 docs: Update the Helm Chart link for Kubernetes.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 22:47:33 +00:00
Jason Volk ba19d407d0 Add config to control m.federate in room create events. (fixes #151)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 04:46:19 +00:00
Jason Volk de7c5dcbc8 Make iss and aud args optional to debug create-jwt command.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 03:03:53 +00:00
dasha_uwuandJason Volk adadafa88f Fix alias resolution logic; element-web v1.11.111 compat
element-hq/element-web#30641
element-hq/element-web#30611

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 02:28:19 +00:00
Jason Volk deda746222 Support JWT authentication on deactivate route.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 02:28:19 +00:00
Jason Volk 29c2c8a333 Branch based on config to note sync for push suppression.
Minor if-let condition and scope reduction; formatting.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 22:25:00 +00:00
tototomate123andJason Volk 296018f0cc mark notification suppression as experimental 2025-09-08 22:25:00 +00:00
tototomate123andJason Volk 6295ca135a feature marked as experimental and executed rustfmt 2025-09-08 22:25:00 +00:00
tototomate123andJason Volk b5a9884194 feat(presence,push) optionally suppress push notifications for active users 2025-09-08 22:25:00 +00:00
Jason Volk 1be7fd9247 Fix cfg condition for debug logging (996a5488be)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 22:25:00 +00:00
Jason Volk be1264965a Refactor sliding-sync.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk f6d2ce2f22 Greenlight hydra backports.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 3cbe1e25cd Cleanup Resolver construction/configuration.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 897322964c Fix dns_min_ttl config doc.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 996a5488be ci: Add build for release-logging, optimized build with debug logging.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 0ed4bd1e07 Only ping presence for public read receipts.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-07 21:39:20 +00:00
Jason Volk da79de5381 Add cfg to preclude log elision without requiring debug_assertions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-07 21:39:20 +00:00
Jason Volk 1bcf3ae19a Soften the Docker troubleshooting note.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-07 20:50:14 +00:00
Jason Volk 91e500efe3 Further tweak troubleshooting formatting.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 22:11:42 +00:00
Jason Volk 35b1ebaf3b Bump dependencies.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 07:45:04 +00:00
Jason Volk ea5deb7e85 Generate CoC HTML inside book directory. (closes #155)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 07:45:04 +00:00
Jason Volk 542cc51047 ci: Add docs test step.
docker: Add docs test target.

docker: Add mising color args to rustfmt.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 06:49:45 +00:00
Jason Volk 3225db0ddd Add external repair tool step to db repair troubleshooting.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk a164da8b58 Fix and extend the apply! util macro.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 7221d466ce Ignore canonical_alias with removed alias for directory results.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk f6c88e3a16 Add query for alias existing; tweak tracing spans.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 3deebeab78 Fix sliding-sync room_avatar conditions.
Eliminate jsOption from non-sliding-sync avatar related codepaths.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 1d2818de58 Round upward for events_after in /context limit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 86781522b6 Allow state visibility for once-joined users when history_visibility=shared.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 4b49aaad53 Fix spec compliance limiting /joined_rooms to current members only.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 3a78ba2b16 Support v1/v2 conditions for join/leave, creation and other operations. (#12)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 685946faed Cleanup validate/verify suite room version related; optimize event_id insert. (#12)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 13c9385ef7 Add v1/v2 support to gen_event_id() scheme. (#12)
Add v1/v2 and improve reference and content hashing suite. (#12)

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 6a583a359e Support v1/v2 prev_events/auth_events outgoing federation format. (#12)
Support v1/v2 prev_events/auth_events when handling outlier pdu. (#12)

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk eec6e45358 Improve error messages for missing auth event and invalid join validations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Alexandru ScvorțovandJason Volk 804257eb52 Fix broken doctest in src/core/utils/debug.rs 2025-09-04 22:21:14 +00:00
342 changed files with 12180 additions and 7353 deletions
+56 -17
View File
@@ -20,13 +20,13 @@ jobs:
name: Init
runs-on: ${{matrix.runner}}
env:
default_cargo_profiles: '["test", "release"]'
default_cargo_profiles: '["test", "bench", "release"]'
default_feat_sets: '["none", "default", "all"]'
default_rust_toolchains: '["nightly", "stable"]'
default_sys_names: '["debian"]'
default_sys_versions: '["testing-slim"]'
default_rust_targets: '["x86_64-unknown-linux-gnu"]'
default_sys_targets: '["x86_64-v1-linux-gnu"]'
default_sys_targets: '["x86_64-v1-linux-gnu", "x86_64-v3-linux-gnu"]'
default_machines: '["X64"]'
outputs:
@@ -38,14 +38,18 @@ 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')}}
package: ${{vars.PACKAGE != 'false'}}
publish: ${{vars.PUBLISH != 'false'}}
build_nix: ${{vars.BUILD_NIX != 'false'}}
build_pkgs: ${{vars.BUILD_PKGS || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || contains(github.ref, 'tags/v')}}
check_pkgs: ${{vars.CHECK_PKGS || 'false'}}
complement: ${{vars.COMPLEMENT || 'true'}}
complement: ${{vars.COMPLEMENT != 'false'}}
complement_runner: 'het'
docker_repo: ${{vars.DOCKER_REPO}}
release_url: ${{steps.release.outputs.upload_url}}
pages_url: 'https://matrix-construct.github.io/tuwunel/'
rust_sdk_integ: ${{vars.RUST_SDK_INTEGRATION != 'false'}}
head_msg: ${{github.event.head_commit.message || github.event.workflow_run.head_commit.message}}
strategy:
fail-fast: true
@@ -58,8 +62,8 @@ jobs:
- name: Initialize Builder
env:
runner: ${{matrix.runner}}
reserved_space: '{"het": "128GB", "aws": "48GB", "gcp": "160GB"}'
max_used_space: '{"het": "256GB", "aws": "64GB", "gcp": "192GB"}'
reserved_space: '{"het": "192GB", "aws": "48GB", "gcp": "160GB"}'
max_used_space: '{"het": "384GB", "aws": "64GB", "gcp": "192GB"}'
run: |
set +e
docker buildx inspect "${GITHUB_ACTOR}"
@@ -114,6 +118,8 @@ jobs:
if: >
!failure() && !cancelled()
&& !contains(github.ref, 'refs/tags/v')
&& !contains(needs.init.outputs.head_msg, '[ci no lint]')
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
name: Lint
needs: [init] #needs: [init, deps]
@@ -129,17 +135,22 @@ jobs:
machines: ${{needs.init.outputs.machines}}
excludes: >
[
{"cargo_profile": "test", "feat_set": "logging"},
{"cargo_profile": "test", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "bench"},
{"cargo_profile": "release", "rust_toolchain": "nightly", "feat_set": "none"},
{"cargo_profile": "release", "rust_toolchain": "nightly", "feat_set": "default"},
{"cargo_profile": "release", "rust_toolchain": "nightly", "feat_set": "logging"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "none"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "feat_set": "logging"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-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", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "rust_toolchain": "nightly"},
{"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"},
@@ -157,11 +168,15 @@ jobs:
if: >
!failure() && !cancelled()
&& !contains(github.ref, 'refs/tags/v')
&& !contains(needs.init.outputs.head_msg, '[ci no test]')
name: Test
needs: [init, lint]
uses: ./.github/workflows/test.yml
with:
head_msg: ${{needs.init.outputs.head_msg}}
build_nix: ${{fromJSON(needs.init.outputs.build_nix)}}
rust_sdk_integ: ${{fromJSON(needs.init.outputs.rust_sdk_integ)}}
complement: ${{fromJSON(needs.init.outputs.complement)}}
complement_runner: ${{needs.init.outputs.complement_runner}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
@@ -177,12 +192,13 @@ jobs:
{"cargo_profile": "test", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "feat_set": "none"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-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", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "rust_toolchain": "nightly"},
{"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"},
@@ -206,14 +222,19 @@ jobs:
if: >
!failure() && !cancelled()
&& needs.init.outputs.package
&& !contains(github.ref, 'refs/pull')
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
&& !contains(needs.init.outputs.head_msg, '[ci no build]')
&& !contains(needs.init.outputs.head_msg, '[ci no package]')
name: Package
needs: [init, lint]
needs: [init, test]
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}}
build_nix: ${{fromJSON(needs.init.outputs.build_nix)}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
feat_sets: ${{needs.init.outputs.feat_sets}}
rust_toolchains: ${{needs.init.outputs.rust_toolchains}}
@@ -226,8 +247,10 @@ jobs:
[
{"feat_set": "none"},
{"cargo_profile": "test"},
{"cargo_profile": "bench"},
{"cargo_profile": "release-native"},
{"cargo_profile": "release-debuginfo", "feat_set": "default"},
{"cargo_profile": "release-debuginfo", "feat_set": "logging"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "nightly"},
{"cargo_profile": "release-debuginfo", "bake_target": "oci"},
{"cargo_profile": "release-debuginfo", "rust_target": "aarch64-unknown-linux-gnu"},
@@ -236,6 +259,8 @@ jobs:
{"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", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "bake_target": "nix"},
{"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"},
@@ -245,16 +270,27 @@ jobs:
{"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-v2-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v2-linux-gnu", "bake_target": "nix"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v3-linux-gnu", "bake_target": "nix"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v4-linux-gnu", "bake_target": "nix"},
]
publish:
if: >
!failure() && !cancelled()
&& needs.init.outputs.publish
&& !contains(github.ref, 'refs/pull')
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
&& !contains(needs.init.outputs.head_msg, '[ci no build]')
&& !contains(needs.init.outputs.head_msg, '[ci no package]')
&& !contains(needs.init.outputs.head_msg, '[ci no publish]')
name: Publish
needs: [init, test, package]
@@ -262,6 +298,7 @@ jobs:
with:
docker_repo: ${{needs.init.outputs.docker_repo}}
release_url: ${{needs.init.outputs.release_url}}
pages_url: ${{needs.init.outputs.pages_url}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
feat_sets: ${{needs.init.outputs.feat_sets}}
rust_toolchains: ${{needs.init.outputs.rust_toolchains}}
@@ -273,11 +310,13 @@ jobs:
excludes: >
[
{"feat_set": "none"},
{"feat_set": "default"},
{"cargo_profile": "test"},
{"cargo_profile": "bench"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "logging"},
{"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"},
@@ -286,11 +325,11 @@ jobs:
{"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-v2-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "logging"},
]
secrets:
+10 -1
View File
@@ -44,6 +44,9 @@ on:
check_pkgs:
type: string
default: 'false'
build_nix:
type: string
default: 'true'
jobs:
book:
@@ -155,7 +158,7 @@ jobs:
name: Distro Packages
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["deb", "rpm"]'
bake_targets: '["deb", "rpm", "nix"]'
cargo_profiles: '["release"]'
feat_sets: '["all"]'
rust_toolchains: '["stable"]'
@@ -176,6 +179,12 @@ jobs:
"rpm": {
"dst": "tuwunel.rpm",
"mime": "application/x-rpm"
},
"nix": {
"dst": "tuwunel.nix.tar.zst",
"src": "/opt/tuwunel.nix.tar",
"mime": "application/zstd",
"zstd": 11
}
}
+9 -2
View File
@@ -44,20 +44,27 @@ on:
release_url:
type: string
description: For release assets
pages_url:
type: string
description: For pages deployment
jobs:
documents:
if: >
!failure() && !cancelled()
&& contains(fromJSON(inputs.sys_targets), fromJSON('["X64"]')[0])
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
&& !contains(github.ref, '-draft')
name: Documents
runs-on: ['X64', 'het']
permissions:
pages: write
contents: read
id-token: write
pages: write
environment:
name: github-pages
url: ${{inputs.pages_url}}
steps:
- id: book
+188 -22
View File
@@ -38,24 +38,53 @@ on:
complement:
type: boolean
default: true
complement_feat_sets:
type: string
default: '["all"]'
complement_runner:
type: string
rust_sdk_integ:
type: boolean
default: true
build_nix:
type: boolean
default: true
head_msg:
type: string
jobs:
unit:
docs:
if: >
contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
!contains(inputs.head_msg, '[ci only it]')
&& 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.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Unit
name: Docs
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["unit"]'
bake_targets: '["docs"]'
cargo_profiles: '["test"]'
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
unit:
if: >
!contains(inputs.head_msg, '[ci only it]')
&& 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.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Module
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["unit", "integ"]'
cargo_profiles: '["test"]'
feat_sets: '["all"]'
rust_toolchains: ${{inputs.rust_toolchains}}
@@ -67,9 +96,58 @@ jobs:
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
bench:
if: >
!contains(inputs.head_msg, '[ci only it]')
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
name: Bench
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["unit", "integ"]'
cargo_profiles: '["bench"]'
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v3-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
memcheck:
if: >
!contains(inputs.head_msg, '[ci only it]')
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
name: Memcheck
uses: ./.github/workflows/bake.yml
with:
#bake_targets: '["unit-valgrind", "integ-valgrind"]'
bake_targets: '["integ-valgrind"]'
cargo_profiles: '["bench"]' # use bench not release for debug syms
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v3-linux-gnu"]'
machines: ${{inputs.machines}}
includes: ${{inputs.includes}}
excludes: ${{inputs.excludes}}
smoke:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci only it]')
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.cargo_profiles
&& inputs.machines
@@ -89,14 +167,18 @@ jobs:
excludes: >
[
{"cargo_profile": "test", "feat_set": "default"},
{"cargo_profile": "test", "feat_set": "logging"},
{"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"},
{"cargo_profile": "test", "sys_target": "x86_64-v3-linux-gnu"},
{"cargo_profile": "test", "bake_target": "smoke-valgrind"},
{"cargo_profile": "bench"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"cargo_profile": "release", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "release", "bake_target": "smoke-valgrind"},
{"cargo_profile": "release-debuginfo", "feat_set": "logging"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "nightly"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "release-debuginfo", "bake_target": "smoke"},
@@ -104,6 +186,7 @@ jobs:
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "none"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "feat_set": "logging"},
{"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"},
@@ -111,6 +194,7 @@ jobs:
{"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", "feat_set": "logging"},
{"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"},
@@ -121,36 +205,118 @@ jobs:
{"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-v2-linux-gnu", "feat_set": "logging"},
{"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", "feat_set": "logging"},
{"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", "feat_set": "logging"},
{"sys_target": "x86_64-v4-linux-gnu", "bake_target": "smoke-valgrind"},
]
nix:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci only it]')
&& !contains(inputs.head_msg, '[ci no build]')
&& !contains(github.ref, 'refs/pull')
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& fromJSON(inputs.build_nix)
name: Smoke NixOS
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["smoke-nix"]'
cargo_profiles: '["release"]'
feat_sets: '["all"]'
rust_toolchains: '["stable"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
rust-sdk-integ:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.rust_sdk_integ
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.rust_targets), fromJSON('["x86_64-unknown-linux-gnu"]')[0])
name: Matrix SDK Integration
needs: [smoke]
uses: ./.github/workflows/bake.yml
with:
#bake_targets: '["rust-sdk-integ", "rust-sdk-valgrind"]'
bake_targets: '["rust-sdk-integ"]'
cargo_profiles: ${{inputs.cargo_profiles}}
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: '["x86_64-unknown-linux-gnu"]'
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
runner: ${{inputs.complement_runner}}
includes: ${{inputs.includes}}
artifact: >
{
"rust-sdk-integ": {
"src": "/var/log/tuwunel.log",
"dst": "rust-sdk-integ.tuwunel.log",
},
"rust-sdk-valgrind": {
"src": "/var/log/tuwunel.log",
"dst": "rust-sdk-valgrind.tuwunel.log",
}
}
excludes: >
[
{"bake_target": "rust-sdk-valgrind", "cargo_profile": "test"},
{"feat_set": "none"},
{"feat_set": "logging"},
{"cargo_profile": "release"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native"},
{"cargo_profile": "test", "sys_target": "x86_64-v2-linux-gnu"},
{"cargo_profile": "test", "sys_target": "x86_64-v3-linux-gnu"},
{"cargo_profile": "bench", "sys_target": "x86_64-v1-linux-gnu"},
{"cargo_profile": "bench", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu"},
]
complement:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.complement
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
name: Complement
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["complement-tester", "complement-testee"]'
cargo_profiles: '["test"]'
feat_sets: ${{inputs.complement_feat_sets}}
cargo_profiles: '["bench"]'
feat_sets: '["logging"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v1-linux-gnu"]'
sys_targets: '["x86_64-v3-linux-gnu"]'
machines: ${{inputs.machines}}
runner: ${{inputs.complement_runner}}
excludes: ${{inputs.excludes}}
@@ -159,15 +325,15 @@ jobs:
compliance:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.complement
&& inputs.machines
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
name: Matrix Compliance
needs: [complement]
needs: [complement, smoke]
runs-on: ["${{matrix.machine}}", "${{inputs.complement_runner}}"]
concurrency:
group: complement-cant-walk-and-chew-bubblegum
@@ -176,13 +342,13 @@ jobs:
strategy:
fail-fast: false
matrix:
cargo_profile: ${{fromJSON('["test"]')}}
feat_set: ${{fromJSON(inputs.complement_feat_sets)}}
cargo_profile: ${{fromJSON('["bench"]')}}
feat_set: ${{fromJSON('["logging"]')}}
rust_toolchain: ${{fromJSON('["nightly"]')}}
sys_name: ${{fromJSON(inputs.sys_names)}}
sys_version: ${{fromJSON(inputs.sys_versions)}}
rust_target: ${{fromJSON(inputs.rust_targets)}}
sys_target: ${{fromJSON('["x86_64-v1-linux-gnu"]')}}
sys_target: ${{fromJSON('["x86_64-v3-linux-gnu"]')}}
machine: ${{fromJSON(inputs.machines)}}
exclude: ${{fromJSON(inputs.excludes)}}
include: ${{fromJSON(inputs.includes)}}
@@ -213,7 +379,7 @@ jobs:
run: |
cid=$(cat "$name")
docker cp "$cid:/usr/src/complement/new_results.jsonl" tests/test_results/complement/test_results.jsonl
docker cp "$cid:/usr/src/complement/new_results.jsonl" tests/complement/results.jsonl
- if: success() || failure() && steps.execute.outcome == 'failure'
name: Upload New Results
@@ -221,7 +387,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: complement_results-${{matrix.feat_set}}-${{matrix.sys_name}}-${{matrix.sys_target}}.jsonl
path: ./tests/test_results/complement/test_results.jsonl
path: ./tests/complement/results.jsonl
- if: failure() && steps.execute.outcome == 'failure'
name: Upload Failure Output
Generated
+743 -737
View File
File diff suppressed because it is too large Load Diff
+53 -38
View File
@@ -28,8 +28,8 @@ keywords = [
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-construct/tuwunel"
rust-version = "1.88.0"
version = "1.4.1"
rust-version = "1.89.0"
version = "1.4.6"
[workspace.metadata.crane]
name = "tuwunel"
@@ -104,7 +104,7 @@ features = [
version = "1.10"
[workspace.dependencies.bytesize]
version = "2.0"
version = "2.1"
[workspace.dependencies.cargo_toml]
version = "0.22"
@@ -140,10 +140,19 @@ features = [
version = "0.8.3"
[workspace.dependencies.const-str]
version = "0.6"
version = "0.7"
[workspace.dependencies.criterion]
version = "0.7"
default-features = false
features = [
"cargo_bench_support",
"async_futures",
"async_tokio",
]
[workspace.dependencies.ctor]
version = "0.4"
version = "0.5"
[workspace.dependencies.cyborgtime]
version = "2.1"
@@ -163,15 +172,6 @@ version = "0.3"
default-features = false
features = ["std", "async-await"]
[workspace.dependencies.hardened_malloc-rs]
version = "0.1.2"
default-features = false
features = [
"static",
"gcc",
"light",
]
[workspace.dependencies.hickory-resolver]
version = "0.25"
default-features = false
@@ -192,7 +192,7 @@ version = "1.3"
version = "0.1"
[workspace.dependencies.hyper]
version = "1.6"
version = "1.7"
default-features = false
features = [
"server",
@@ -220,7 +220,7 @@ features = [
]
[workspace.dependencies.insta]
version = "1.43.1"
version = "1.43"
features = [
"json",
]
@@ -262,7 +262,7 @@ version = "0.1"
version = "1.0"
[workspace.dependencies.minicbor]
version = "2.0"
version = "2.1"
features = ["std"]
[workspace.dependencies.minicbor-serde]
@@ -272,13 +272,16 @@ features = ["std"]
[workspace.dependencies.nix]
version = "0.30"
default-features = false
features = ["resource"]
features = [
"resource",
"user",
]
[workspace.dependencies.num-traits]
version = "0.2"
[workspace.dependencies.opentelemetry]
version = "0.30.0"
version = "0.31"
# Disabled until they move to opentelemetry 0.30
#[workspace.dependencies.opentelemetry-jaeger]
@@ -286,7 +289,7 @@ version = "0.30.0"
#features = ["rt-tokio"]
[workspace.dependencies.opentelemetry_sdk]
version = "0.30"
version = "0.31"
features = ["rt-tokio"]
[workspace.dependencies.proc-macro2]
@@ -299,7 +302,7 @@ version = "1.0"
version = "0.8"
[workspace.dependencies.regex]
version = "1.11"
version = "1.12"
[workspace.dependencies.reqwest]
version = "0.12"
@@ -317,7 +320,7 @@ default-features = false
[workspace.dependencies.ruma]
git = "https://github.com/matrix-construct/ruma"
rev = "8bc15ba4f145e7b995d36e82e8624c3ac3ce0ef6"
rev = "db4bf59eaa1757735f38489f8924348ae05a6a17"
features = [
"__compat",
"appservice-api-c",
@@ -336,6 +339,7 @@ features = [
"unstable-msc2870",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3814",
"unstable-msc3245",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
@@ -345,11 +349,12 @@ features = [
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4133",
"unstable-msc4143",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4310",
"unstable-msc4311",
"unstable-extensible-events",
"unstable-hydra",
]
[workspace.dependencies.rustls]
@@ -381,7 +386,7 @@ version = "0.4"
default-features = false
[workspace.dependencies.sentry]
version = "0.42"
version = "0.45"
default-features = false
features = [
"backtrace",
@@ -397,10 +402,10 @@ features = [
]
[workspace.dependencies.sentry-tower]
version = "0.42"
version = "0.45"
[workspace.dependencies.sentry-tracing]
version = "0.42"
version = "0.45"
[workspace.dependencies.serde]
version = "1.0"
@@ -430,12 +435,13 @@ version = "0.10"
default-features = false
[workspace.dependencies.similar]
version = "2.7.0"
version = "2.7"
[workspace.dependencies.smallstr]
version = "0.3"
features = [
"ffi",
"serde",
"std",
"union",
]
@@ -462,7 +468,7 @@ features = [
]
[workspace.dependencies.termimad]
version = "0.33"
version = "0.34"
default-features = false
[workspace.dependencies.thiserror]
@@ -494,7 +500,7 @@ features = [
]
[workspace.dependencies.tokio]
version = "1.47"
version = "1.48"
default-features = false
features = [
"fs",
@@ -536,21 +542,21 @@ features = [
]
[workspace.dependencies.tracing]
version = "0.1.41"
version = "0.1"
default-features = false
[workspace.dependencies.tracing-core]
version = "0.1.33"
version = "0.1"
default-features = false
[workspace.dependencies.tracing-flame]
version = "0.2.0"
version = "0.2"
[workspace.dependencies.tracing-opentelemetry]
version = "0.31.0"
version = "0.32"
[workspace.dependencies.tracing-subscriber]
version = "0.3.20"
version = "0.3"
default-features = false
features = [
"ansi",
@@ -699,7 +705,7 @@ inherits = "release-native.build-override"
[profile.bench]
debug = "limited"
strip = false
strip = "none"
#rustflags = [
# "-Cremark=all",
# '-Ztime-passes',
@@ -719,7 +725,7 @@ strip = false
# and can be raised if build times are tolerable.
[profile.dev]
debug = "full"
debug = 0
#rustflags = [
# '--cfg', 'tuwunel_mods',
# '-Ztime-passes',
@@ -779,7 +785,7 @@ inherits = "dev"
[profile.dev.package.'*']
inherits = "dev"
debug = "limited"
debug = 0
incremental = false
codegen-units = 1
opt-level = 'z'
@@ -796,6 +802,11 @@ opt-level = 'z'
# '-Clink-arg=-Wl,-z,nodelete',
#]
# same as dev but slower.
[profile.dbg]
inherits = "dev"
debug = "full"
# primarily used for CI
[profile.test]
debug = "limited"
@@ -887,6 +898,9 @@ multiple_crate_versions = { level = "allow", priority = 1 }
###################
complexity = { level = "warn", priority = -1 }
# promotes forward-compat for literal ..default() construction
needless_update = { level = "allow", priority = 1 }
###################
correctness = { level = "warn", priority = -1 }
@@ -899,6 +913,7 @@ option_if_let_else = { level = "allow", priority = 1 } # TODO
redundant_pub_crate = { level = "allow", priority = 1 } # TODO
significant_drop_in_scrutinee = { level = "allow", priority = 1 } # TODO
significant_drop_tightening = { level = "allow", priority = 1 } # TODO
tuple_array_conversions = { level = "allow", priority = 1 }
###################
pedantic = { level = "warn", priority = -1 }
@@ -913,6 +928,7 @@ 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 }
match_same_arms = { level = "allow", priority = 1 }
missing_docs_in_private_items = { level = "allow", priority = 1 }
missing_errors_doc = { level = "allow", priority = 1 }
missing_panics_doc = { level = "allow", priority = 1 }
@@ -958,7 +974,6 @@ pub_without_shorthand = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
redundant_type_annotations = "warn"
rest_pat_in_fully_bound_structs = "warn"
semicolon_outside_block = "warn"
str_to_string = "warn"
string_lit_chars_any = "warn"
+1 -12
View File
@@ -110,22 +110,11 @@ ### Upgrading & Downgrading Tuwunel
safe but often prevented by a guard. An error will indicate the downgrade is not possible and a
newer version which does not error must be sought.
#### Versioning
Tuwunel uses a semantic version tag in the format of `v<major>.<minor>.<patch>`. The `patch` value will
always correspond to the number of commits from the last `minor` change with significance to developers.
The `minor` version is changed for fixes or features significant to users. The `major` value corresponds
to significant feature evolutions, and does not indicate any "breaking change" nor connote stability.
> [!IMPORTANT]
> **Users should attempt to keep up to date with the latest minor version.**
#### Branches
The main branch is always _reasonably safe_ to run. We understand the propensity for users to simply clone
the main branch to get up and running, and we're obliged to ensure it's always viable. Nevertheless, only
tagged releases are true releases. If you don't care to update often, find the latest `minor` version
change rather than `patch`. We don't recommend simply following `major` version changes at this time.
tagged releases are true releases.
#### Container Tracking
+42 -4
View File
@@ -1,9 +1,47 @@
# Tuwunel 1.4.1
# Tuwunel 1.4.6
September 2, 2025
November 6, 2025
Bridge and Application Service users must update from Tuwunel 1.4.0 to this patch. All other users are encouraged to update at their convenience.
### New Features
- Element Call discovery support was implemented by @tototomate123 in (#209). Adding a `[[global.well_known.rtc_transports]]` section in your [config file](https://github.com/matrix-construct/tuwunel/blob/e1f89b69ea117f166be423f035a5a34f4c0e7366/tuwunel-example.toml#L1835-L1851) enables discovery. More information on setting up Element Call can be found at [Spaetzblog](https://sspaeth.de/2024/11/sfu/), skipping step one, and performing step three in your Tuwunel config.
- Dehydrated Device support (MSC3814) is now available (#200). This feature allows users to receive encrypted messages without being logged in. Supporting clients will setup the dehydrated device automatically and it will "just work" behind the scenes; in fact, these clients will also hide it from the sessions list to avoid confusion. Support is not widespread yet but it has been tested with matrix-js-sdk clients such as Element-web. This feature was commissioned and made public by an enterprise sponsor.
- Notification panel (the 🔔 button) has been implemented in (#201). Even though Element-web now requires enabling it in the Labs menu, the underlying support (`GET /_matrix/client/v3/notifications`) enhances the push-notification handling of other clients.
- Live room previews are now available. This support (`GET /_matrix/client/v3/events`) allows users, including guests, to sync updates for a public room without joining (4afd6f347b1).
- Thanks to a suggestion by @cyberdoors in (#29), the configuration option `encryption_enabled_by_default_for_room_type` is now available. This feature can enable encryption for a room even when the client does not. The values `all` or `invite` are accepted, the latter roughly meaning DM's only. Neither are enabled by default.
### Enhancements
- Thank you @tototomate123 for improving the reverse-proxy docs, adding dedicated pages for both [Caddy](https://github.com/matrix-construct/tuwunel/blob/e0a997c22784b453735b24907e607412b153ba56/docs/deploying/reverse-proxy-caddy.md) and [Nginx](https://github.com/matrix-construct/tuwunel/blob/e0a997c22784b453735b24907e607412b153ba56/docs/deploying/reverse-proxy-nginx.md) in (#209). Thanks to @tycode for pointing out the docs were missing for alternative proxies in (#197).
- Thanks to an observation by @iwalkalone69 in (#40), the last-seen time for a device in the session list is now updated acceptably. This function piggybacks on the presence system to prevent writing too frequently; testing has never shown it more than a minute or few out of date.
- Thanks to an inquiry by @EntityinArray in (#189) guest-accounts can now be enabled while registration tokens are also enabled to prevent fully open account registration. Note that registration tokens don't apply to guest-accounts and those are still fully open.
- Courtesy of @dasha-uwu the list of servers attempted when joining a room is now properly shuffled to increase the odds of finding a viable server, especially if an additional join attempt is made.
### Bug Fixes
- Special thanks to @alaviss for immediately reporting incorrect results from the `/joined_members` endpoint in the v1.4.0 release. This regression primarily affects Application Services and Bridges; the most popular clients have been verified to not make use of this API.
- Special thanks to @BVollmerhaus for finding the TURN secret file configured by `turn_secret_file` was broken in (#211), forcing users to configure `turn_secret` directly. Thank you for fixing this in (#212).
- Thank you @scvalex for updating the nix build for Tuwunel's integration tests and re-enabling all checks. (#215)
- Thanks to a report by @Anagastes in (#146) **Nheko and NeoChat users can now enjoy properly verified devices.** Special thanks for the assistance of @deepbluev7 with diagnosing the cross-signing signature issue.
- Database columns intended for deletion, notably `roomsynctoken_shortstatehash`, never had the deletion command actually invoked on them 😭 explaining the lack of enthusiasm after the 1.4.3 release introduced stateless sync. **Users will now see the free disk space they were promised.** This was uncovered during an unrelated issue investigation courtesy of @frebib.
- Thanks to investigation by @dasha-uwu the pagination tokens in the `/relations` endpoint were buggy and now operate correctly.
- Thanks to @Polve for identifying the `DynamicUser=yes` directive in the systemd files was invalid and advising a replacement in (#207).
- Thanks to @daudix for reporting an edge-case where the server will refuse to start rather than robustly reporting errors during startup checks and recreate a missing media directory (#213).
- Push rule evaluation was never implemented for invites arriving over federation. Notifications are now properly sent in this case.
- Sliding-sync handlers were susceptible to errors under rare circumstances escaping to cause an HTTP 500, which wreaks havoc on the rust-sdk. This has now been prevented.
- Federating with Conduit over several non-essential endpoints was broken. It is unclear whether this affected an actual Conduit release version, but thanks to @kladki a fix is scheduled and we have included a workaround now on this end.
+1 -1
View File
@@ -4,7 +4,6 @@ Wants=network-online.target
After=network-online.target
Documentation=https://tuwunel.chat/
RequiresMountsFor=/var/lib/private/tuwunel
Alias=matrix-tuwunel.service
[Service]
DynamicUser=yes
@@ -77,3 +76,4 @@ StartLimitBurst=5
[Install]
WantedBy=multi-user.target
Alias=matrix-tuwunel.service
+1 -2
View File
@@ -2,11 +2,9 @@
Description=Tuwunel Matrix homeserver
Wants=network-online.target
After=network-online.target
Alias=matrix-tuwunel.service
Documentation=https://tuwunel.chat/
[Service]
DynamicUser=yes
User=tuwunel
Group=tuwunel
Type=notify
@@ -64,3 +62,4 @@ StartLimitBurst=5
[Install]
WantedBy=multi-user.target
Alias=matrix-tuwunel.service
+14 -8
View File
@@ -12,10 +12,12 @@ ARG CARGO_TARGET_DIR
ARG cargo_target_profile
ARG cargo_target_artifact
ARG cargo_target_share
ARG cargo_share
ARG CARGO_TERM_VERBOSE=0
ARG RUST_BACKTRACE
ARG JEMALLOC_OVERRIDE
ARG ROCKSDB_LIB_DIR
ARG VALGRINDFLAGS=""
ARG CARGO_BUILD_RUSTFLAGS
ARG CARGO_PROFILE_TEST_DEBUG
ARG CARGO_PROFILE_TEST_INCREMENTAL
@@ -32,6 +34,8 @@ ARG color_args="--color always"
ARG recipe_args=""
ARG cargo_args=""
ARG git_checkout
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
WORKDIR /
COPY --link --from=input . .
@@ -42,11 +46,13 @@ WORKDIR /usr/lib/${sys_triple}
COPY --link --from=rocksdb . .
WORKDIR /usr/src/tuwunel
SHELL ["/bin/bash", "-c"]
ENV PATH="${CARGO_HOME}/bin:$PATH"
ENV CARGO_TERM_VERBOSE="${CARGO_TERM_VERBOSE}"
ENV RUST_BACKTRACE="${RUST_BACKTRACE}"
ENV JEMALLOC_OVERRIDE="${JEMALLOC_OVERRIDE}"
ENV ROCKSDB_LIB_DIR="${ROCKSDB_LIB_DIR}"
ENV VALGRINDFLAGS="${VALGRINDFLAGS}"
ENV CARGO_PROFILE_TEST_DEBUG="${CARGO_PROFILE_TEST_DEBUG}"
ENV CARGO_PROFILE_TEST_INCREMENTAL="${CARGO_PROFILE_TEST_INCREMENTAL}"
ENV CARGO_PROFILE_BENCH_DEBUG="${CARGO_PROFILE_BENCH_DEBUG}"
@@ -56,17 +62,15 @@ ENV CARGO_PROFILE_RELEASE_DEBUGINFO_DEBUG="${CARGO_PROFILE_RELEASE_DEBUGINFO_DEB
ENV CARGO_PROFILE_RELEASE_DEBUGINFO_LTO="${CARGO_PROFILE_RELEASE_DEBUGINFO_LTO}"
ENV CARGO_BUILD_RUSTFLAGS="${CARGO_BUILD_RUSTFLAGS}"
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 \
--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 \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
@@ -74,7 +78,9 @@ RUN \
--mount=type=cache,dst=${targ_targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
<<EOF
set -eux
rustup run ${rust_toolchain} \
ulimit -n 65535
rustup run "${rust_toolchain}" \
cargo ${cargo_cmd} \
--verbose \
--locked \
+8 -7
View File
@@ -10,9 +10,12 @@ ARG CARGO_TARGET_DIR
ARG cargo_target_profile
ARG cargo_target_artifact
ARG cargo_target_share
ARG cargo_share
ARG cargo_profile
ARG cargo_features
ARG cargo_spec_features
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
ARG pkg_dir
ARG deb_args=""
@@ -20,17 +23,15 @@ WORKDIR /
COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
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 \
--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 \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
+9 -5
View File
@@ -1,6 +1,7 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS build-rpm
ARG rust_target
ARG rust_toolchain
ARG RUSTUP_HOME
ARG CARGO_HOME
@@ -9,9 +10,12 @@ ARG CARGO_TARGET_DIR
ARG cargo_target_profile
ARG cargo_target_artifact
ARG cargo_target_share
ARG cargo_share
ARG cargo_profile
ARG cargo_features
ARG cargo_spec_features
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
ARG pkg_dir
ARG gen_rpm_args=""
@@ -23,11 +27,11 @@ RUN \
--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 \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
+7 -41
View File
@@ -1,22 +1,6 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS key-gen-base
ARG var_cache
ARG var_lib_apt
WORKDIR /
COPY --link --from=input . .
RUN \
--mount=type=cache,dst=/var/cache,id=${var_cache},sharing=locked \
--mount=type=cache,dst=/var/lib/apt,id=${var_lib_apt},sharing=locked \
<<EOF
set -eux
apt-get -y -U install --no-install-recommends openssl gawk
EOF
FROM key-gen-base AS key-gen
FROM input AS key-gen
WORKDIR /complement
COPY <<EOF v3.ext
@@ -62,12 +46,11 @@ RUN [ -f certificate.crt ] && [ -f private_key.pem ]
FROM scratch AS complement-config
WORKDIR /complement
COPY --from=key-gen /complement/* .
COPY --from=source /usr/src/tuwunel/tests/test_results/complement/test_results.jsonl old_results.jsonl
COPY --from=source /usr/src/tuwunel/tests/complement/results.jsonl old_results.jsonl
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
@@ -89,7 +72,6 @@ COPY <<EOF complement.toml
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
query_trusted_key_servers_first = false
@@ -121,9 +103,6 @@ EOF
FROM input AS complement-testee
WORKDIR /
COPY --link --from=input . .
EXPOSE 8008 8448
RUN mkdir /database
COPY --from=complement-config * /complement/
@@ -133,9 +112,6 @@ ENTRYPOINT tuwunel -Oserver_name=\""$SERVER_NAME\""
FROM input AS complement-testee-valgrind
WORKDIR /
COPY --link --from=input . .
EXPOSE 8008 8448
RUN mkdir /database
COPY --from=complement-config * /complement/
@@ -152,22 +128,13 @@ ENTRYPOINT valgrind \
FROM input AS complement-base
ARG var_cache
ARG var_lib_apt
ARG complement_ref="426bc3458e5287398ec39ab7d2b8f2e3103dcf39"
ARG complement_tags="conduwuit_blacklist"
ARG complement_tests="./tests/..."
ARG complement_run=".*"
WORKDIR /
RUN \
--mount=type=cache,dst=/var/cache,id=${var_cache},sharing=locked \
--mount=type=cache,dst=/var/lib/apt,id=${var_lib_apt},sharing=locked \
--mount=type=cache,dst=/go/pkg/mod/cache,sharing=locked \
<<EOF
set -eux
apt-get -y -U install --no-install-recommends golang-go jq
EOF
WORKDIR /usr/src
ADD https://github.com/matrix-construct/complement.git#403840348f6bcc9cc8ed1671dc2f638c2b1ce4ac complement
ADD https://github.com/matrix-construct/complement.git#${complement_ref} complement
WORKDIR /usr/src/complement
ENV COMPLEMENT_BASE_IMAGE="complement-testee"
@@ -183,8 +150,9 @@ EOF
FROM input AS complement-tester
ARG complement_verbose=0
ARG complement_debug=0
ARG complement_dirty=0
ARG complement_count=1
ARG complement_parallel=16
ARG complement_parallel=1
ARG complement_shuffle=1337
ARG complement_timeout="1h"
ARG complement_run=".*"
@@ -193,9 +161,6 @@ ARG complement_tags="conduwuit_blacklist"
ARG complement_tests="./tests/..."
ARG complement_base_image
WORKDIR /
COPY --link --from=input . .
WORKDIR /usr/src/complement
ENV COMPLEMENT_DEBUG=$complement_debug
ENV complement_parallel="$complement_parallel"
@@ -207,6 +172,7 @@ ENV complement_tests="$complement_tests"
ENV complement_skip="$complement_skip"
ENV complement_run="$complement_run"
ENV complement_tests="$complement_tests"
ENV COMPLEMENT_ENABLE_DIRTY_RUNS="$complement_dirty"
ENV COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS="$complement_verbose"
ENV COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT="host.docker.internal"
ENV COMPLEMENT_HOST_MOUNTS="/var/run/docker.sock:/var/run/docker.sock"
+100
View File
@@ -0,0 +1,100 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS rust-sdk-integration
ARG sys_name
ARG sys_version
ARG feat_set
ARG rust_target
ARG rust_toolchain
ARG cargo_profile
ARG RUSTUP_HOME
ARG CARGO_HOME
ARG CARGO_TARGET
ARG MRSDK_TARGET_DIR="/usr/src/matrix-rust-sdk/target"
ARG mrsdk_target_share
#ARG mrsdk_ref="integration"
ARG mrsdk_ref="tuwunel-changes"
ARG mrsdk_test_args=""
ARG mrsdk_test_opts=""
ARG mrsdk_skip_list=""
ARG mrsdk_parallel=2
ARG mrsdk_startup_delay="10s"
ARG mrsdk_testee="/usr/bin/tuwunel"
WORKDIR /usr/src
ADD --link https://github.com/matrix-construct/matrix-rust-sdk.git#${mrsdk_ref} matrix-rust-sdk
WORKDIR /etc
COPY <<EOF tuwunel.toml
[global]
admin_room_notices = false
allow_device_name_federation = true
allow_guest_registration = 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
ip_range_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
query_trusted_key_servers_first = false
query_trusted_key_servers_first_on_join = false
rocksdb_log_level = "debug"
rocksdb_max_log_files = 1
rocksdb_paranoid_file_checks = true
rocksdb_recovery_mode = 0
trusted_servers = []
url_preview_domain_contains_allowlist = ["*"]
url_preview_domain_explicit_denylist = ["*"]
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
EOF
WORKDIR /usr/lib
COPY --link --from=install /usr/lib .
WORKDIR /usr/bin
COPY --link --from=install /usr/bin/tuwunel .
WORKDIR /usr/src/matrix-rust-sdk
SHELL ["/bin/bash", "-c"]
ENV RUST_BACKTRACE="full"
ENV TUWUNEL_CONFIG="/etc/tuwunel.toml"
ENV TUWUNEL_DATABASE_PATH="/var/db/tuwunel"
ENV TUWUNEL_SERVER_NAME="localhost"
ENV TUWUNEL_PORT="[8448]"
ENV HOMESERVER_URL="http://localhost:8448"
RUN \
--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=${MRSDK_TARGET_DIR},id=${mrsdk_target_share},sharing=locked \
<<EOF
set -eux
nohup ${mrsdk_testee[@]} 1> /var/log/tuwunel.log &
PID=$!; trap "sleep 10s; set +e; kill -QUIT ${PID}; wait ${PID}" EXIT
sleep "${mrsdk_startup_delay}"
rustup run ${rust_toolchain} \
cargo test \
--locked \
--release \
"--color=always" \
"--features=default" \
"--target=${rust_target}" \
"--target-dir=${MRSDK_TARGET_DIR}" \
"--package=matrix-sdk-integration-testing" \
${mrsdk_test_args[@]} \
-- \
"--color=always" \
"--test-threads=${mrsdk_parallel}" \
${mrsdk_skip_list[@]} \
${mrsdk_test_opts[@]} \
;
EOF
+91
View File
@@ -0,0 +1,91 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS nix-base
WORKDIR /
COPY --link --from=input . .
RUN \
--mount=type=cache,dst=/nix,sharing=shared \
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
<<EOF
set -eux
curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install > nix-install
sh ./nix-install --daemon
rm nix-install
EOF
FROM nix-base AS build-nix
WORKDIR /usr/src/tuwunel
COPY --link --from=source /usr/src/tuwunel .
RUN \
--mount=type=cache,dst=/nix,sharing=shared \
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
<<EOF
set -eux
nix-build \
--verbose \
--cores 0 \
--max-jobs $(nproc) \
--log-format raw \
.
cp -afRL --copy-contents result /opt/tuwunel
EOF
FROM input AS smoke-nix
WORKDIR /
COPY --link --from=nix-base . .
WORKDIR /usr/src/tuwunel
COPY --link --from=source /usr/src/tuwunel .
ENV TUWUNEL_DATABASE_PATH="/tmp/tuwunel/smoketest.db"
ENV TUWUNEL_LOG="info"
RUN \
--mount=type=cache,dst=/nix,sharing=shared \
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
<<EOF
set -eux
alias nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
nix run \
--verbose \
--cores 0 \
--max-jobs $(nproc) \
--log-format raw \
.#all-features \
-- \
-Otest='["smoke", "fresh"]' \
-Oserver_name=\"localhost\" \
EOF
FROM input AS nix-pkg
WORKDIR /
COPY --link --from=nix-base . .
WORKDIR /usr/src/tuwunel
COPY --link --from=source /usr/src/tuwunel .
RUN \
--mount=type=cache,dst=/nix,sharing=shared \
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
<<EOF
set -eux
alias nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
ID=$(nix-store --realise $(nix path-info --derivation))
mkdir -p tuwunel
nix-store --export $ID > tuwunel/tuwunel.drv
tar -cvf /opt/tuwunel.nix.tar tuwunel
EOF
-6
View File
@@ -6,9 +6,6 @@ ARG rust_target
ARG rustup_version="1.28.2"
ARG rustup_profile="minimal"
WORKDIR /
COPY --link --from=input . .
WORKDIR ${RUST_HOME}
RUN <<EOF
set -eux
@@ -32,9 +29,6 @@ ARG CARGO_TERM_VERBOSE
ARG rustup_components
ARG cargo_installs
WORKDIR /
COPY --link --from=input . .
WORKDIR ${RUST_HOME}
ENV CARGO_TARGET="${rust_target}"
ENV RUSTUP_HOME="${RUSTUP_HOME}"
-7
View File
@@ -3,9 +3,6 @@
FROM input AS source
ARG git_checkout
WORKDIR /
COPY --link --from=input . .
ADD --link --keep-git-dir . /usr/src/tuwunel
WORKDIR /usr/src/tuwunel
RUN <<EOF
@@ -30,7 +27,6 @@ ARG JEMALLOC_OVERRIDE
ARG ROCKSDB_LIB_DIR
WORKDIR /
COPY --link --from=input . .
COPY --link --from=rust ${RUST_HOME} ${RUST_HOME}
COPY --link --from=source /usr/src/tuwunel /usr/src/tuwunel
@@ -67,9 +63,6 @@ ARG RUSTUP_HOME
ARG CARGO_HOME
ARG CARGO_TARGET
WORKDIR /
COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=locked \
-3
View File
@@ -12,9 +12,6 @@ ARG var_lib_apt
ARG packages
ARG DEBIAN_FRONTEND
WORKDIR /
COPY --link --from=input . .
ENV DEBIAN_FRONTEND="${DEBIAN_FRONTEND}"
RUN \
--mount=type=cache,dst=/var/cache,id=${var_cache},sharing=locked \
+253 -35
View File
@@ -1,4 +1,4 @@
variable "CI" {}
variable "GITHUB_ACTOR" {}
variable "GITHUB_REPOSITORY" {}
variable "GITHUB_REF" {}
@@ -29,8 +29,12 @@ variable "git_ref_name" {
cargo_feat_sets = {
none = ""
# Default features
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,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"
# All features sans release_max_log_level
logging = "blurhashing,brotli_compression,bzip2_compression,console,direct_tls,element_hacks,gzip_compression,io_uring,jemalloc,jemalloc_conf,jemalloc_prof,jemalloc_stats,ldap,lz4_compression,media_thumbnail,perf_measurements,sentry_telemetry,systemd,tokio_console,tuwunel_mods,url_preview,zstd_compression"
# All features
all = "blurhashing,brotli_compression,bzip2_compression,console,direct_tls,element_hacks,gzip_compression,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,tuwunel_mods,url_preview,zstd_compression"
}
variable "cargo_features_always" {
default = "direct_tls"
@@ -180,7 +184,6 @@ dynamic_libs = [
nightly_rustflags = [
"--cfg tokio_unstable",
"--cfg tuwunel_bench",
"--allow=unstable-features",
"-Z crate-attr=feature(test)",
"-Z enforce-type-length-limit",
@@ -233,9 +236,18 @@ group "lints" {
group "tests" {
targets = [
"docs",
"unit",
"smoke",
"integration",
"matrix-compliance",
]
}
group "matrix-compliance" {
targets = [
"complement",
"rust-sdk-integ",
]
}
@@ -403,7 +415,7 @@ target "complement-base" {
elem("complement-config", [sys_name, sys_version, sys_target])
]
contexts = {
input = elem("target:base", [sys_name, sys_version, sys_target])
input = elem("target:builder", [sys_name, sys_version, sys_target])
}
args = complement_args
}
@@ -424,6 +436,116 @@ target "complement-config" {
}
}
#
# Integration tests
#
group "integration" {
targets = [
"integ",
"rust-sdk-integ",
]
}
variable "valgrind_flags" {
default = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
}
target "rust-sdk-valgrind" {
name = elem("rust-sdk-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("rust-sdk-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
matrix = cargo_rust_feat_sys
inherits = [
elem("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
]
contexts = {
input = elem("target:rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
install = elem("target:install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
VALGRINDFLAGS = "${valgrind_flags}"
mrsdk_testee = "valgrind ${valgrind_flags} /usr/bin/tuwunel"
mrsdk_test_args = ""
mrsdk_startup_delay = "30s"
mrsdk_skip_list =<<EOF
--skip test_delayed_invite_response_and_sent_message_decryption
--skip test_history_share_on_invite_pin_violation
EOF
}
}
target "rust-sdk-integ" {
name = elem("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
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}"]
target = "rust-sdk-integration"
dockerfile = "${docker_dir}/Dockerfile.matrix-rust-sdk"
matrix = cargo_rust_feat_sys
inherits = [
elem("rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target]),
elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
install = elem("target:install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
mrsdk_target_share = "/usr/src/matrix-rust-sdk/target/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/_shared_cache"
mrsdk_testee = "/usr/bin/tuwunel"
mrsdk_test_args = "--no-fail-fast"
mrsdk_skip_list =<<EOF
--skip test_delayed_invite_response_and_sent_message_decryption
EOF
}
}
target "integ-valgrind" {
name = elem("integ-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("integ-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
matrix = cargo_rust_feat_sys
inherits = [
elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
VALGRINDFLAGS = "${valgrind_flags}"
cargo_cmd = "valgrind test"
cargo_args = "--test=*"
}
}
target "integ" {
name = elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
matrix = cargo_rust_feat_sys
inherits = [
elem("build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
TUWUNEL_DATABASE_PATH = "/tmp/integration.test.db"
cargo_cmd = (cargo_profile == "bench"? "bench": "test")
cargo_args = (cargo_profile == "bench"?
"--no-fail-fast --bench=*": "--no-fail-fast --test=*"
)
}
}
#
# Smoke tests
#
@@ -432,11 +554,26 @@ group "smoke" {
targets = [
"smoke-version",
"smoke-startup",
#"smoke-nix",
#"smoke-valgrind",
#"smoke-perf",
]
}
target "smoke-nix" {
name = elem("smoke-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("smoke-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=cacheonly,compression=zstd,mode=min,compression-level=${cache_compress_level}"]
dockerfile = "${docker_dir}/Dockerfile.nix"
target = "smoke-nix"
matrix = cargo_rust_feat_sys
inherits = [
elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
}
target "smoke-valgrind" {
name = elem("smoke-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
@@ -509,6 +646,70 @@ target "tests-smoke" {
}
}
#
# Unit tests
#
target "unit-valgrind" {
name = elem("unit-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("unit-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
target = "cargo"
matrix = cargo_rust_feat_sys
inherits = [
elem("unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
VALGRINDFLAGS = "${valgrind_flags}"
cargo_cmd = "valgrind test"
cargo_args = "--lib --bins"
}
}
target "unit" {
name = elem("unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
target = "cargo"
matrix = cargo_rust_feat_sys
inherits = [
elem("build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
cargo_cmd = (cargo_profile == "bench"? "bench": "test")
cargo_args = (cargo_profile == "bench"?
"--no-fail-fast --lib": "--no-fail-fast --lib --bins"
)
}
}
target "docs" {
name = elem("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
target = "cargo"
matrix = cargo_rust_feat_sys
inherits = [
elem("build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
cargo_cmd = "test"
cargo_args = "--doc --no-fail-fast"
}
}
#
# Installation
#
@@ -673,6 +874,7 @@ target "install" {
group "pkg" {
targets = [
"nix",
"deb",
"rpm",
"deb-install",
@@ -784,28 +986,36 @@ target "build-deb" {
}
}
#
# Unit tests
#
target "unit" {
name = elem("unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
target "nix" {
name = elem("nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
elem_tag("nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
target = "cargo"
output = ["type=docker,compression=zstd,mode=min,compression-level=${zstd_image_compress_level}"]
target = "nix-pkg"
matrix = cargo_rust_feat_sys
inherits = [
elem("build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
}
target "build-nix" {
name = elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=cacheonly,compression=zstd,mode=min,compression-level=${cache_compress_level}"]
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
dockerfile = "${docker_dir}/Dockerfile.nix"
target = "build-nix"
matrix = cargo_rust_feat_sys
inherits = [
elem("builder", [sys_name, sys_version, sys_target]),
elem("source", [sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
cargo_cmd = (cargo_profile == "bench"? "bench": "test")
cargo_args = (rust_toolchain == "nightly"?
"--no-fail-fast --all-targets": "--no-fail-fast --bins --tests"
)
input = elem("target:builder", [sys_name, sys_version, sys_target]),
source = elem("target:source", [sys_name, sys_version, sys_target]),
}
}
@@ -830,15 +1040,14 @@ target "book" {
}
dockerfile-inline =<<EOF
FROM input AS book
COPY --link --from=input . .
RUN ["mdbook", "build", "-d", "/book", "/usr/src/tuwunel"]
EOF
}
target "docs" {
name = elem("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
target "build-docs" {
name = elem("build-docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
elem_tag("build-docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
matrix = cargo_rust_feat_sys
inherits = [
@@ -898,7 +1107,7 @@ target "build-tests" {
}
args = {
cargo_cmd = (cargo_profile == "bench"? "bench": "test")
cargo_args = "--no-run"
cargo_args = (cargo_profile == "bench"? "--no-run --benches": "--no-run --tests")
}
}
@@ -1035,7 +1244,7 @@ target "fmt" {
input = elem("target:ingredients", [rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
fmt_args = "-- --color always"
fmt_args = "-- --color=always"
}
}
@@ -1176,6 +1385,8 @@ target "deps-base" {
# cache key for unique artifact area
cargo_target_artifact = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${cargo_profile}/${feat_set}/${git_ref_sha}"
# cache key for hashed subdirs
cargo_share = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_toolchain}/${cargo_profile}/_shared_cache"
# cache key for hashed subdirs
cargo_target_share = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${cargo_profile}/_shared_cache"
# cased name of profile subdir within target complex
cargo_target_profile = (
@@ -1186,8 +1397,8 @@ target "deps-base" {
CARGO_PROFILE_TEST_DEBUG = "false"
CARGO_PROFILE_TEST_INCREMENTAL = "false"
CARGO_PROFILE_BENCH_DEBUG = "limited"
CARGO_PROFILE_BENCH_LTO = "false"
CARGO_PROFILE_BENCH_DEBUG = "false"
CARGO_PROFILE_BENCH_LTO = "thin"
CARGO_PROFILE_RELEASE_LTO = "thin"
CARGO_PROFILE_RELEASE_DEBUGINFO_DEBUG = "limited"
CARGO_PROFILE_RELEASE_DEBUGINFO_LTO = "off"
@@ -1214,7 +1425,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
cargo_profile == "release" && rust_toolchain == "nightly"?
(cargo_profile == "release" || cargo_profile == "bench") && substr(rust_toolchain, 0, 7) == "nightly"?
join(" ", [
join(" ", rustflags),
join(" ", nightly_rustflags),
@@ -1236,7 +1447,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
cargo_profile == "release" || cargo_profile == "release-debuginfo"?
cargo_profile == "release" || cargo_profile == "release-debuginfo" || cargo_profile == "bench"?
join(" ", [
join(" ", rustflags),
join(" ", static_rustflags),
@@ -1256,7 +1467,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
rust_toolchain == "stable"?
substr(rust_toolchain, 0, 6) == "stable"?
join(" ", [
join(" ", rustflags),
join(" ", static_rustflags),
@@ -1276,7 +1487,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
rust_toolchain == "nightly"?
substr(rust_toolchain, 0, 7) == "nightly"?
join(" ", [
join(" ", rustflags),
join(" ", nightly_rustflags),
@@ -1499,11 +1710,12 @@ rustup_components = [
]
cargo_installs = [
"cargo-chef",
"cargo-audit",
"cargo-deb",
#"cargo-arch",
"cargo-chef",
"cargo-deb",
"cargo-generate-rpm",
"cargo-valgrind",
#"lychee",
"mdbook",
"typos-cli",
@@ -1587,13 +1799,20 @@ kitchen_packages = [
"clang",
"cmake",
"curl",
"gawk",
"git",
"golang-go",
"gzip",
"jq",
"libc6-dev",
"libssl-dev",
"libsqlite3-dev",
"make",
"nix-bin",
"openssl",
"pkg-config",
"pkgconf",
"valgrind",
"xz-utils",
]
@@ -1613,7 +1832,6 @@ target "kitchen" {
args = {
packages = join(" ", [
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": "",
+4 -4
View File
@@ -16,6 +16,7 @@ default_sys_target="x86_64-v1-linux-gnu"
default_sys_version="testing-slim"
default_complement_verbose=0
default_complement_dirty=0
default_complement_count=1
default_complement_parallel=1
default_complement_shuffle=0
@@ -31,9 +32,7 @@ skip="${skip}|TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic
skip="${skip}|TestLogin/parallel/POST_/"
skip="${skip}|TestUnbanViaInvite"
skip="${skip}|TestRoomState/Parallel/GET_/publicRooms_lists_newly-created_room"
# flakes due to timeout in debug-mode
skip="${skip}|TestMSC4297StateResolutionV2_1_starts_from_empty_set"
skip="${skip}|TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph"
skip="${skip}|TestThreadReceiptsInSyncMSC4102"
set -a
cargo_profile="${cargo_profile:-$default_cargo_profile}"
@@ -53,6 +52,7 @@ 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_dirty=${complement_dirty:-$default_complement_dirty}"
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}"
@@ -88,7 +88,7 @@ extract_output() {
}
result_src="$cid:/usr/src/complement/new_results.jsonl"
result_dst="tests/test_results/complement/test_results.jsonl"
result_dst="tests/complement/results.jsonl"
extract_results() {
docker cp "$result_src" "$result_dst"
}
+1
View File
@@ -0,0 +1 @@
../CODE_OF_CONDUCT.md
+3 -1
View File
@@ -5,6 +5,8 @@ # Summary
- [Examples](configuration/examples.md)
- [Deploying](deploying.md)
- [Generic](deploying/generic.md)
- [Reverse Proxy - Caddy](deploying/reverse-proxy-caddy.md)
- [Reverse Proxy - Nginx](deploying/reverse-proxy-nginx.md)
- [NixOS](deploying/nixos.md)
- [Docker](deploying/docker.md)
- [Kubernetes](deploying/kubernetes.md)
@@ -20,4 +22,4 @@ # Summary
- [Contributing](contributing.md)
- [Testing](development/testing.md)
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
- [Tuwunel Community Code of Conduct](../CODE_OF_CONDUCT.md)
- [Tuwunel Community Code of Conduct](CODE_OF_CONDUCT.md)
+39 -61
View File
@@ -12,24 +12,23 @@ ### Static prebuilt binary
You may simply download the binary that fits your machine architecture (x86_64
or aarch64). Run `uname -m` to see what you need.
Prebuilt fully static musl binaries can be downloaded from the latest tagged
Prebuilt fully static binaries can be downloaded from the latest tagged
release [here](https://github.com/matrix-construct/tuwunel/releases/latest) or
`main` CI branch workflow artifact output. These also include Debian/Ubuntu
packages.
`main` CI branch workflow artifact output. These also include `.deb` packages
for Debian or Ubuntu and `.rpm` packages for Red Hat or Fedora.
These can be curl'd directly from. `ci-bins` are CI workflow binaries by commit
hash/revision, and `releases` are tagged releases. Sort by descending last
modified for the latest.
For the **best** performance; if using an `x86_64` CPU made in the last ~10 years,
we recommend using the `-v3-` optimised packages. See below for a command to check
what your system supports. If the server refuses to start or exits with an "Illegal
Instruction" error you will need `-v2-` or `-v1-` packages instead. The database
backend, RocksDB, benefits from `-v2-` or greater as it features performance
critical hardware accelerated CRC32 hashing/checksumming.
These binaries have jemalloc and io_uring statically linked and included with
them, so no additional dynamic dependencies need to be installed.
For the **best** performance; if using an `x86_64` CPU made in the last ~15 years,
we recommend using the `-haswell-` optimised binaries. This sets
`-march=haswell` which is the most compatible and highest performance with
optimised binaries. The database backend, RocksDB, most benefits from this as it
will then use hardware accelerated CRC32 hashing/checksumming which is critical
for performance.
Linux users can run this script to display which optimization levels they may
choose:
```
cat /proc/cpuinfo | grep -Po '(avx|sse)[235]' | sort -u | sed 's/avx5/v4/;s/avx2/v3/;s/sse3/v2/;s/sse2/v1/' | sort
```
### Compiling
@@ -142,69 +141,48 @@ ## Setting the correct file permissions
## Setting up the Reverse Proxy
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc transparently with proper defaults.
For other software, please refer to their respective documentation or online guides.
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults. However, Nginx is also well-supported and widely used.
### Caddy
**Choose your reverse proxy:**
After installing Caddy via your preferred method, create `/etc/caddy/conf.d/tuwunel_caddyfile`
and enter this (substitute for your server name).
- **[Caddy Setup Guide](reverse-proxy-caddy.md)** - Recommended for ease of use and automatic TLS
- **[Nginx Setup Guide](reverse-proxy-nginx.md)** - Popular choice with extensive documentation
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy localhost:8008
# UNIX socket
#reverse_proxy unix//run/tuwunel/tuwunel.sock
}
```
### Quick Overview
That's it! Just start and enable the service and you're set.
Regardless of which reverse proxy you choose, you will need to:
```bash
sudo systemctl enable --now caddy
```
1. **Reverse proxy the following routes:**
- `/_matrix/` - core Matrix C-S and S-S APIs
- `/_tuwunel/` - ad-hoc Tuwunel routes such as `/local_user_count` and `/server_version`
### Other Reverse Proxies
2. **Optionally reverse proxy (recommended):**
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using Tuwunel to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using Tuwunel to send the homeserver admin contact and support page (formerly known as MSC1929)
- `/` if you would like to see `hewwo from tuwunel woof!` at the root
As we would prefer our users to use Caddy, we will not provide configuration files for other proxys.
3. **Handle ports:**
- Port 443 (HTTPS) for client-server API
- Port 8448 for federation (if federating with other homeservers)
You will need to reverse proxy everything under following routes:
- `/_matrix/` - core Matrix C-S and S-S APIs
- `/_tuwunel/` - ad-hoc Tuwunel routes such as `/local_user_count` and
`/server_version`
You can optionally reverse proxy the following individual routes:
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
Tuwunel to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using Tuwunel to send the homeserver admin
contact and support page (formerly known as MSC1929)
- `/` if you would like to see `hewwo from tuwunel woof!` at the root
See the following spec pages for more details on these files:
See the following spec pages for more details on well-known files:
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
Examples of delegation:
- <https://puppygock.gay/.well-known/matrix/server>
- <https://puppygock.gay/.well-known/matrix/client>
- <https://matrix.org/.well-known/matrix/server>
- <https://matrix.org/.well-known/matrix/client>
For Apache and Nginx there are many examples available online.
### Other Reverse Proxies
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
_Specific contributions for other proxies are welcome!_
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from messing with the `X-Matrix` header (note that Apache isn't very good as a general reverse proxy and we discourage the usage of it if you can).
**Not Recommended:**
- **Apache**: While possible, Apache requires special configuration (`nocanon` in `ProxyPass`) to prevent corruption of the `X-Matrix` header.
- **Lighttpd**: Its proxy module alters the `X-Matrix` authorization header, breaking federation functionality.
If using Nginx, you need to give Tuwunel the request URI using `$request_uri`, or like so:
- `proxy_pass http://127.0.0.1:6167$request_uri;`
- `proxy_pass http://127.0.0.1:6167;`
Nginx users need to increase `client_max_body_size` (default is 1M) to match
`max_request_size` defined in tuwunel.toml.
## You're done
## You are done
Now you can start Tuwunel with:
+2 -1
View File
@@ -2,7 +2,8 @@ # Tuwunel for Kubernetes
Tuwunel doesn't support horizontal scalability or distributed loading
natively, however a community maintained Helm Chart is available here to run
Tuwunel on Kubernetes: <https://gitlab.cronce.io/charts/conduwuit>
Tuwunel on Kubernetes: <https://github.com/AreYouLoco/tuwunel-helm> and the
legacy conduwuit version: <https://gitlab.cronce.io/charts/conduwuit>.
Should changes need to be made, please reach out to the maintainer in our
Matrix room as this is not maintained/controlled by the Tuwunel maintainers.
+48
View File
@@ -0,0 +1,48 @@
# Reverse Proxy Setup - Caddy
[<= Back to Generic Deployment Guide](generic.md#setting-up-the-reverse-proxy)
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults.
## Installation
Install Caddy via your preferred method. Refer to the [official Caddy installation guide](https://caddyserver.com/docs/install) for your distribution.
## Configuration
After installing Caddy, create `/etc/caddy/conf.d/tuwunel_caddyfile` and enter this (substitute `your.server.name` with your actual server name):
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy localhost:8008
# UNIX socket (alternative - comment out the line above and uncomment this)
#reverse_proxy unix//run/tuwunel/tuwunel.sock
}
```
### What this does
- Handles both port 443 (HTTPS) and port 8448 (Matrix federation) automatically
- Automatically provisions and renews TLS certificates via Let's Encrypt
- Sets all necessary reverse proxy headers correctly
- Routes all traffic to Tuwunel listening on `localhost:8008`
That's it! Just start and enable the service and you're set.
```bash
sudo systemctl enable --now caddy
```
## Verification
After starting Caddy, verify it's working by checking:
```bash
curl https://your.server.name/_tuwunel/server_version
curl https://your.server.name:8448/_tuwunel/server_version
```
---
[=> Continue with "You're Done"](generic.md#you-are-done)
+164
View File
@@ -0,0 +1,164 @@
# Reverse Proxy Setup - Nginx
[<= Back to Generic Deployment Guide](generic.md#setting-up-the-reverse-proxy)
This guide shows you how to configure Nginx as a reverse proxy for Tuwunel with TLS support.
## Installation
Install Nginx via your preferred method. Most distributions include Nginx in their package repositories:
```bash
# Debian/Ubuntu
sudo apt install nginx
# Red Hat/Fedora
sudo dnf install nginx
# Arch Linux
sudo pacman -S nginx
```
## Configuration
Create a new configuration file at `/etc/nginx/sites-available/tuwunel` (or `/etc/nginx/conf.d/tuwunel.conf` on some distributions):
```nginx
# Client-Server API over HTTPS (port 443)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name matrix.example.com;
# Nginx standard body size is 1MB, which is quite small for media uploads
# Increase this to match the max_request_size in your tuwunel.toml
client_max_body_size 100M;
# Forward requests to Tuwunel (listening on 127.0.0.1:8008)
location / {
proxy_pass http://127.0.0.1:8008;
# Preserve host and scheme - critical for proper Matrix operation
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
# TLS configuration (Let's Encrypt example using certbot)
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
}
# Matrix Federation over HTTPS (port 8448)
# Only needed if you want to federate with other homeservers
# Don't forget to open port 8448 in your firewall!
server {
listen 8448 ssl http2;
listen [::]:8448 ssl http2;
server_name matrix.example.com;
# Same body size increase for larger files
client_max_body_size 100M;
# Forward to the same local port as client-server API
location / {
proxy_pass http://127.0.0.1:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
# TLS configuration (same certificates as above)
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
}
```
### Important Notes
- **Replace `matrix.example.com`** with your actual server name
- **`client_max_body_size`**: Must match or exceed `max_request_size` in your `tuwunel.toml`
- **Do NOT use `$request_uri`** in `proxy_pass` - while some guides suggest this, it's not necessary for Tuwunel and can cause issues
- **IPv6**: The `listen [::]:443` and `listen [::]:8448` lines enable IPv6 support. Remove them if you don't need IPv6
### TLS Certificates
The example above uses Let's Encrypt certificates via certbot. To obtain certificates:
```bash
sudo certbot certonly --nginx -d matrix.example.com
```
Certbot will automatically handle renewal. Make sure to reload Nginx after certificate renewal:
```bash
sudo systemctl reload nginx
```
### Optional: Timeout Configuration
The default Nginx timeouts are usually sufficient for Matrix operations. Element's long-polling `/sync` requests typically run for 30 seconds, which is within Nginx's default timeouts.
However, if you experience federation retries or dropped long-poll connections, you can extend the timeouts by adding these lines inside your `location /` blocks:
```nginx
location / {
proxy_pass http://127.0.0.1:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
# Optional: Extend timeouts if experiencing issues
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
```
## Enable the Configuration
If using sites-available/sites-enabled structure:
```bash
sudo ln -s /etc/nginx/sites-available/tuwunel /etc/nginx/sites-enabled/
```
Test the configuration:
```bash
sudo nginx -t
```
If the test passes, reload Nginx:
```bash
sudo systemctl reload nginx
```
Enable Nginx to start on boot:
```bash
sudo systemctl enable nginx
```
## Verification
After configuring Nginx, verify it's working by checking:
```bash
curl https://matrix.example.com/_tuwunel/server_version
curl https://matrix.example.com:8448/_tuwunel/server_version
```
## Troubleshooting
### Apache Compatibility Note
If you're considering Apache instead of Nginx: Apache is not well-suited as a reverse proxy for Matrix homeservers. If you must use Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from corrupting the `X-Matrix` authorization header, which will break federation.
### Lighttpd is Not Supported
Lighttpd has known issues with the `X-Matrix` authorization header, making federation non-functional. We do not recommend using Lighttpd with Tuwunel.
---
[=> Continue with "You're Done"](generic.md#you-are-done)
+46 -23
View File
@@ -1,17 +1,11 @@
# Troubleshooting Tuwunel
> ## Docker users ⚠️
>
> Docker is extremely UX unfriendly. Because of this, a ton of issues or support
> is actually Docker support, not tuwunel support. We also cannot document the
> ever-growing list of Docker issues here.
>
> [!IMPORTANT]
> If you intend on asking for support and you are using Docker, **PLEASE**
> triple validate your issues are **NOT** because you have a misconfiguration in
> your Docker setup.
>
> If there are things like Compose file issues or Dockerhub image issues, those
> can still be mentioned as long as they're something we can fix.
> your Docker setup. We must remain focused on supporting Tuwunel issues and
> cannot budget our time for generic Docker support. Compose file issues or
> Dockerhub image issues are okay if they are something we can fix.
## Tuwunel and Matrix issues
@@ -99,13 +93,17 @@ #### Database corruption
which everyone can follow from the top until they have recovered or reach the
end. The details and implications will be explained within each step.
> [!NOTE]
> [!TIP]
> All command-line `-O` options can be expressed as environment variables or in
> the config file based on your deployment's requirements. Note that
> `--maintenance` is only available on the command-line, but is equivalent to
> configuring `startup_netburst = false` and `listening = false`.
> `--maintenance` is equivalent to configuring `startup_netburst = false` and
> `listening = false`.
0. Start the server with the following options:
> [!IMPORTANT]
> Always create a backup of the database before running any operation. This is
> critical for steps 3 and above.
**0. Start the server with the following options:**
`tuwunel --maintenance -O rocksdb_recovery_mode=0`
@@ -115,7 +113,7 @@ #### Database corruption
certain there is deep corruption skip to step 4, otherwise you are finished
without any modifications.
1. Start the server in Tolerate-Corrupted-Tail-Records mode:
**1. Start the server in Tolerate-Corrupted-Tail-Records mode:**
`tuwunel --maintenance -O rocksdb_recovery_mode=1`
@@ -127,7 +125,7 @@ #### Database corruption
worst-case clients may need to clear-cache & reload to guarantee correctness.
If the server starts you are finished.
2. Start the server in Point-In-Time mode:
**2. Start the server in Point-In-Time mode:**
`tuwunel --maintenance -O rocksdb_recovery_mode=2`
@@ -137,11 +135,11 @@ #### Database corruption
loss, but it is more likely than above that clients may need to clear-cache
& reload to correctly resynchronize with the server.
3. Start the server in Skip-Any-Corrupted-Record mode:
**3. Start the server in Skip-Any-Corrupted-Record mode:**
> [!CAUTION]
> [!WARNING]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide any further support for users who have entered this mode.
> We cannot provide support for users who have entered this mode.
`tuwunel --maintenance -O rocksdb_recovery_mode=3`
@@ -152,11 +150,16 @@ #### Database corruption
the server starts you should immediately export your messages, encryption
keys, etc, in a salvage effort and prepare to reinstall.
4. Start the server in repair mode.
**4. Start the server in repair mode.**
> [!WARNING]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide support for users who have entered this mode.
> [!CAUTION]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide any further support for users who have entered this mode.
> Always create a backup of the database before entering this mode. The repair
> is not configurable and not interactive. It may automatically remove more
> data than anticipated, preventing further salvage efforts.
`tuwunel --maintenance -O rocksdb_repair=true`
@@ -167,6 +170,25 @@ #### Database corruption
essential. Nevertheless the impact of this operation is impossible to assess
and a successful recovery should be used to salvage data prior to reinstall.
Once finished, restart the server without `rocksdb_repair`. If no errors
persist, restart the server again without maintenance mode.
**5. Utilize an external repair tool.**
> [!WARNING]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide support for users who have entered this mode.
```
git clone https://github.com/facebook/rocksdb
cd rocksdb
make -j$(nproc) ldb
./ldb repair --db=/var/lib/tuwunel/ 2>./repair-log.txt
```
For situations when the repair mode in step 4 failed or produced unexpected
results.
## Debugging
Note that users should not really be debugging things. If you find yourself
@@ -178,7 +200,8 @@ #### Debug/Trace log level
Tuwunel builds without debug or trace log levels at compile time by default
for substantial performance gains in CPU usage and improved compile times. If
you need to access debug/trace log levels, you will need to build without the
`release_max_log_level` feature or use our provided static debug binaries.
`release_max_log_level` feature or use our provided release-logging binaries
and images.
#### Changing log level dynamically
Generated
+94 -134
View File
@@ -10,11 +10,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1748532342,
"narHash": "sha256-CvaKOUq8G10sghKpZhEB2UYjJoWhEkrDFggDgi7piUI=",
"lastModified": 1758711588,
"narHash": "sha256-0nZlCCDC5PfndsQJXXtcyrtrfW49I3KadGMDlutzaGU=",
"owner": "zhaofengli",
"repo": "attic",
"rev": "ce9373715fe3fac7a174a65a7e6d6baeba8cb4f9",
"rev": "12cbeca141f46e1ade76728bce8adc447f2166c6",
"type": "github"
},
"original": {
@@ -29,14 +29,14 @@
"devenv": "devenv",
"flake-compat": "flake-compat_2",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs_4"
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"lastModified": 1761150191,
"narHash": "sha256-fPQmV+K836jR/Tu6slfMdm9HL3p0IEPB5x5I7Zc6v5E=",
"owner": "cachix",
"repo": "cachix",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"rev": "5629a7d84c91df9c06650b18eb428cce4166c4fc",
"type": "github"
},
"original": {
@@ -58,16 +58,21 @@
],
"git-hooks": [
"cachix",
"devenv"
"devenv",
"git-hooks"
],
"nixpkgs": "nixpkgs_2"
"nixpkgs": [
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744206633,
"narHash": "sha256-pb5aYkE8FOoa4n123slgHiOf1UbNSnKe5pEZC+xXD5g=",
"lastModified": 1752264895,
"narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=",
"owner": "cachix",
"repo": "cachix",
"rev": "8a60090640b96f9df95d1ab99e5763a586be1404",
"rev": "47053aef762f452e816e44eb9a23fbc3827b241a",
"type": "github"
},
"original": {
@@ -80,11 +85,11 @@
"complement": {
"flake": false,
"locked": {
"lastModified": 1741891349,
"narHash": "sha256-YvrzOWcX7DH1drp5SGa+E/fc7wN3hqFtPbqPjZpOu1Q=",
"lastModified": 1756358878,
"narHash": "sha256-H/4TkbFcGsMRDZ7gNaXZ7NxeO3b1YgkVt9zPb+3ATSg=",
"owner": "matrix-construct",
"repo": "complement",
"rev": "e587b3df569cba411aeac7c20b6366d03c143745",
"rev": "403840348f6bcc9cc8ed1671dc2f638c2b1ce4ac",
"type": "github"
},
"original": {
@@ -95,18 +100,12 @@
}
},
"crane": {
"inputs": {
"nixpkgs": [
"attic",
"nixpkgs"
]
},
"locked": {
"lastModified": 1722960479,
"narHash": "sha256-NhCkJJQhD5GUib8zN9JrmYGMwt4lCRp6ZVNzIiYCl0Y=",
"lastModified": 1751562746,
"narHash": "sha256-smpugNIkmDeicNz301Ll1bD7nFOty97T79m4GUMUczA=",
"owner": "ipetkov",
"repo": "crane",
"rev": "4c6c77920b8d44cd6660c1621dea6b3fc4b4c4f4",
"rev": "aed2020fd3dc26e1e857d4107a5a67a33ab6c1fd",
"type": "github"
},
"original": {
@@ -117,11 +116,11 @@
},
"crane_2": {
"locked": {
"lastModified": 1748970125,
"narHash": "sha256-UDyigbDGv8fvs9aS95yzFfOKkEjx1LO3PL3DsKopohA=",
"lastModified": 1760924934,
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
"owner": "ipetkov",
"repo": "crane",
"rev": "323b5746d89e04b22554b061522dfce9e4c49b18",
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
"type": "github"
},
"original": {
@@ -138,6 +137,7 @@
"cachix",
"flake-compat"
],
"flake-parts": "flake-parts_2",
"git-hooks": [
"cachix",
"git-hooks"
@@ -149,11 +149,11 @@
]
},
"locked": {
"lastModified": 1748273445,
"narHash": "sha256-5V0dzpNgQM0CHDsMzh+ludYeu1S+Y+IMjbaskSSdFh0=",
"lastModified": 1760560333,
"narHash": "sha256-goJQdVl9oDgCxF9CggPUw1DvB4gsot1jzMmz9px8Du8=",
"owner": "cachix",
"repo": "devenv",
"rev": "668a50d8b7bdb19a0131f53c9f6c25c9071e1ffb",
"rev": "0a4043938f540027e562c5a0feebbe6be872c3ea",
"type": "github"
},
"original": {
@@ -170,11 +170,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1749883145,
"narHash": "sha256-RlcGw3vAnbI3cfZn8aFaovNUd7312VZh+/FDWkqdA7E=",
"lastModified": 1761547629,
"narHash": "sha256-4OH1CVm2PdjKRqEJ3RLfkQMDSBdn7VId6iyYCwKOK+U=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a804172f150bcf81262655324e583bb0cd0f28dd",
"rev": "d82a7c64ea441e397914577c9a18f2867e5b364b",
"type": "github"
},
"original": {
@@ -187,11 +187,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@@ -241,11 +241,11 @@
]
},
"locked": {
"lastModified": 1722555600,
"narHash": "sha256-XOQkdLafnb/p9ij77byFQjDf5m5QYl9b2REiVClC+x4=",
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "8471fe90ad337a8074e957b69ca4d0089218391d",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {
@@ -259,16 +259,15 @@
"nixpkgs-lib": [
"cachix",
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github"
},
"original": {
@@ -309,11 +308,11 @@
]
},
"locked": {
"lastModified": 1747372754,
"narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=",
"lastModified": 1760392170,
"narHash": "sha256-WftxJgr2MeDDFK47fQKywzC72L2jRc/PWcyGdjaDzkw=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"rev": "46d55f0aeb1d567a78223e69729734f3dca25a85",
"type": "github"
},
"original": {
@@ -344,30 +343,14 @@
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"liburing": {
"flake": false,
"locked": {
"lastModified": 1749816983,
"narHash": "sha256-p5hXfDe53Y4MVwL2+wKZYpy4OPGvqFFnOEvkMsFAO6c=",
"lastModified": 1761487364,
"narHash": "sha256-PmRdt9dQUFGcVrX7XCh7z2FpRm1SqKgiYbIwpkJPjRU=",
"owner": "axboe",
"repo": "liburing",
"rev": "ad83d3ab64894c16eaf21ef869656a5bddb93ca4",
"rev": "65ec4223032a38870dde2bce646ba4de13d2feff",
"type": "github"
},
"original": {
@@ -381,11 +364,24 @@
"inputs": {
"flake-compat": [
"cachix",
"devenv"
"devenv",
"flake-compat"
],
"flake-parts": [
"cachix",
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"cachix",
"devenv",
"git-hooks"
],
"nixpkgs": [
"cachix",
"devenv",
"nixpkgs"
],
"flake-parts": "flake-parts_2",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_3",
"nixpkgs-23-11": [
"cachix",
"devenv"
@@ -393,34 +389,30 @@
"nixpkgs-regression": [
"cachix",
"devenv"
],
"pre-commit-hooks": [
"cachix",
"devenv"
]
},
"locked": {
"lastModified": 1745930071,
"narHash": "sha256-bYyjarS3qSNqxfgc89IoVz8cAFDkF9yPE63EJr+h50s=",
"owner": "domenkozar",
"lastModified": 1758763079,
"narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=",
"owner": "cachix",
"repo": "nix",
"rev": "b455edf3505f1bf0172b39a735caef94687d0d9c",
"rev": "6f0140527c2b0346df4afad7497baa08decb929f",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"owner": "cachix",
"ref": "devenv-2.30.5",
"repo": "nix",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"lastModified": 1757882181,
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
"type": "github"
},
"original": {
@@ -438,11 +430,11 @@
]
},
"locked": {
"lastModified": 1729742964,
"narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
"lastModified": 1737420293,
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
"type": "github"
},
"original": {
@@ -453,11 +445,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1726042813,
"narHash": "sha256-LnNKCCxnwgF+575y0pxUdlGZBO/ru1CtGHIqQVfvjlA=",
"lastModified": 1751949589,
"narHash": "sha256-mgFxAPLWw0Kq+C8P3dRrZrOYEQXOtKuYVlo9xvPntt8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "159be5db480d1df880a0135ca0bfed84c2f88353",
"rev": "9b008d60392981ad674e04016d25619281550a9d",
"type": "github"
},
"original": {
@@ -469,27 +461,27 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1724316499,
"narHash": "sha256-Qb9MhKBUTCfWg/wqqaxt89Xfi6qTD3XpTzQ9eXi3JmE=",
"lastModified": 1751741127,
"narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "797f7dc49e0bc7fab4b57c021cdf68f595e47841",
"rev": "29e290002bfff26af1db6f64d070698019460302",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1733212471,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"lastModified": 1760524057,
"narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5",
"type": "github"
},
"original": {
@@ -501,43 +493,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"lastModified": 1761349956,
"narHash": "sha256-tH3wHnOJms+U4k/rK2Nn1RfBrhffX92jLP/2VndSn0w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1748190013,
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1749871736,
"narHash": "sha256-K9yBph93OLTNw02Q6e9CYFGrUhvEXnh45vrZqIRWfvQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6afe187897bef7933475e6af374c893f4c84a293",
"rev": "02f2cb8e0feb4596d20cc52fda73ccee960e3538",
"type": "github"
},
"original": {
@@ -575,18 +535,18 @@
"flake-utils": "flake-utils",
"liburing": "liburing",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_5",
"nixpkgs": "nixpkgs_3",
"rocksdb": "rocksdb"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1749829309,
"narHash": "sha256-t6x6/PKg8Shnkd3htrxf3WMgycfRLRWvN9JHAmGWf+s=",
"lastModified": 1761500479,
"narHash": "sha256-syeBTCCU96qPJHcVpwHeCwmPCiLTDHHgYQYhpZ0iwLo=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "a497f4114ccf24978accb56190e60d1e1659e0c7",
"rev": "049767e6faa84b2d1a951d8f227e6ebd99d728a2",
"type": "github"
},
"original": {
+1 -55
View File
@@ -26,7 +26,7 @@
file = ./rust-toolchain.toml;
# See also `rust-toolchain.toml`
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE=";
};
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
@@ -204,8 +204,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -218,8 +216,6 @@
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
@@ -231,8 +227,6 @@
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
@@ -246,8 +240,6 @@
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
@@ -260,12 +252,9 @@
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
hmalloc = scopeHost.main.override { features = ["hardened_malloc"]; };
oci-image = scopeHost.oci-image;
oci-image-all-features = scopeHost.oci-image.override {
@@ -278,8 +267,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -294,18 +281,11 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
};
oci-image-hmalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["hardened_malloc"];
};
};
book = scopeHost.book;
@@ -359,8 +339,6 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -379,8 +357,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -400,8 +376,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -420,22 +394,12 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
}
# An output for a statically-linked binary with hardened_malloc
{
name = "${binaryName}-hmalloc";
value = scopeCrossStatic.main.override {
features = ["hardened_malloc"];
};
}
# An output for an OCI image based on that binary
{
name = "oci-image-${crossSystem}";
@@ -478,8 +442,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -501,8 +463,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -523,8 +483,6 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -532,16 +490,6 @@
};
}
# An output for an OCI image based on that binary with hardened_malloc
{
name = "oci-image-${crossSystem}-hmalloc";
value = scopeCrossStatic.oci-image.override {
main = scopeCrossStatic.main.override {
features = ["hardened_malloc"];
};
};
}
# An output for a complement OCI image for the specified platform
{
name = "complement-${crossSystem}";
@@ -571,8 +519,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
+20 -2
View File
@@ -23,8 +23,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
]
@@ -131,6 +129,7 @@ buildDepsOnlyEnv =
buildPackageEnv = {
TUWUNEL_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
TUWUNEL_DATABASE_PATH = "/var/tmp/tuwunel.db";
} // buildDepsOnlyEnv // {
# Only needed in static stdenv because these are transitive dependencies of rocksdb
CARGO_BUILD_RUSTFLAGS = buildDepsOnlyEnv.CARGO_BUILD_RUSTFLAGS
@@ -203,8 +202,27 @@ craneLib.buildPackage ( commonAttrs // {
env = buildDepsOnlyEnv;
});
nativeCheckInputs = [
pkgsBuildHost.libredirect.hook
];
preCheck =
let
fakeResolvConf = pkgsBuildHost.writeTextFile {
name = "resolv.conf";
text = ''
nameserver 0.0.0.0
'';
};
in
''
export NIX_REDIRECTS="/etc/resolv.conf=${fakeResolvConf}"
export TUWUNEL_DATABASE_PATH="$(mktemp -d)/smoketest.db"
'';
doCheck = true;
doBenchmark = false;
cargoExtraArgs = "--no-default-features --locked "
+ lib.optionalString
(features'' != [])
+1 -2
View File
@@ -2,11 +2,9 @@
Description=Tuwunel Matrix homeserver
Wants=network-online.target
After=network-online.target
Alias=matrix-tuwunel.service
Documentation=https://tuwunel.chat/
[Service]
DynamicUser=yes
User=tuwunel
Group=tuwunel
Type=notify
@@ -63,3 +61,4 @@ StartLimitBurst=5
[Install]
WantedBy=multi-user.target
Alias=matrix-tuwunel.service
+1 -1
View File
@@ -9,7 +9,7 @@
# If you're having trouble making the relevant changes, bug a maintainer.
[toolchain]
channel = "1.88.0"
channel = "1.89.0"
profile = "minimal"
components = [
# For rust-analyzer
+1
View File
@@ -11,6 +11,7 @@ version.workspace = true
[lib]
path = "mod.rs"
bench = false
crate-type = [
"rlib",
# "dylib",
+14 -27
View File
@@ -28,10 +28,7 @@
},
warn,
};
use tuwunel_service::rooms::{
short::{ShortEventId, ShortRoomId},
state_compressor::HashSetCompressStateEvent,
};
use tuwunel_service::rooms::{short::ShortRoomId, state_compressor::HashSetCompressStateEvent};
use crate::admin_command;
@@ -138,16 +135,8 @@ pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result {
}
#[admin_command]
pub(super) async fn get_short_pdu(
&self,
shortroomid: ShortRoomId,
shorteventid: ShortEventId,
) -> Result {
let pdu_id: RawPduId = PduId {
shortroomid,
shorteventid: shorteventid.into(),
}
.into();
pub(super) async fn get_short_pdu(&self, shortroomid: ShortRoomId, count: i64) -> Result {
let pdu_id: RawPduId = PduId { shortroomid, count: count.into() }.into();
let pdu_json = self
.services
@@ -304,7 +293,7 @@ pub(super) async fn get_remote_pdu(
#[admin_command]
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.alias.resolve(&room).await?;
let room_id = self.services.alias.maybe_resolve(&room).await?;
let room_state: Vec<Raw<AnyStateEvent>> = self
.services
.state_accessor
@@ -918,7 +907,7 @@ pub(super) async fn database_files(&self, map: Option<String>, level: Option<i32
let mut files: Vec<_> = self
.services
.db
.db
.engine
.file_list()
.collect::<Result<_>>()?;
@@ -974,10 +963,10 @@ pub(super) async fn create_jwt(
#[derive(Serialize)]
struct Claim {
sub: String,
iss: String,
aud: String,
exp: usize,
nbf: usize,
iss: Option<String>,
aud: Option<String>,
exp: Option<usize>,
nbf: Option<usize>,
}
let config = &self.services.config.jwt;
@@ -994,21 +983,19 @@ struct Claim {
let claim = Claim {
sub: user,
iss: issuer.unwrap_or_default(),
iss: issuer,
aud: audience.unwrap_or_default(),
aud: audience,
exp: exp_from_now
.and_then(|val| now_secs().checked_add(val))
.map(TryInto::try_into)
.and_then(Result::ok)
.unwrap_or(usize::MAX),
.and_then(Result::ok),
nbf: nbf_from_now
.and_then(|val| now_secs().checked_add(val))
.map(TryInto::try_into)
.and_then(Result::ok)
.unwrap_or(0),
.and_then(Result::ok),
};
encode(&header, &claim, &key)
@@ -1025,7 +1012,7 @@ pub(super) async fn resync_database(&self) -> Result {
self.services
.db
.db
.engine
.update()
.map_err(|e| err!("Failed to update from primary: {e:?}"))
}
+3 -3
View File
@@ -4,7 +4,7 @@
use clap::Subcommand;
use ruma::{OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName};
use tuwunel_core::Result;
use tuwunel_service::rooms::short::{ShortEventId, ShortRoomId};
use tuwunel_service::rooms::short::ShortRoomId;
use self::tester::TesterCommand;
use crate::admin_command_dispatch;
@@ -43,8 +43,8 @@ pub(super) enum DebugCommand {
/// Shortroomid integer
shortroomid: ShortRoomId,
/// Shorteventid integer
shorteventid: ShortEventId,
/// PduCount integer
count: i64,
},
/// - Attempts to retrieve a PDU from a remote server. Inserts it into our
+5 -3
View File
@@ -2,6 +2,7 @@
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedUserId};
use tuwunel_core::Result;
use tuwunel_database::Deserialized;
use crate::{admin_command, admin_command_dispatch};
@@ -46,7 +47,7 @@ async fn changes_since(
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:?}\n```"))
.await
}
@@ -58,11 +59,12 @@ async fn account_data_get(
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let results = self
let results: serde_json::Value = self
.services
.account_data
.get_raw(room_id.as_deref(), &user_id, &kind)
.await;
.await
.deserialized()?;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
+6 -1
View File
@@ -10,6 +10,7 @@
mod room_timeline;
mod sending;
mod short;
mod sync;
mod users;
use clap::Subcommand;
@@ -20,7 +21,7 @@
presence::PresenceCommand, pusher::PusherCommand, raw::RawCommand, resolver::ResolverCommand,
room_alias::RoomAliasCommand, room_state_cache::RoomStateCacheCommand,
room_timeline::RoomTimelineCommand, sending::SendingCommand, short::ShortCommand,
users::UsersCommand,
sync::SyncCommand, users::UsersCommand,
};
use crate::admin_command_dispatch;
@@ -76,6 +77,10 @@ pub(super) enum QueryCommand {
#[command(subcommand)]
Short(ShortCommand),
/// - sync service
#[command(subcommand)]
Sync(SyncCommand),
/// - raw service
#[command(subcommand)]
Raw(RawCommand),
+55 -23
View File
@@ -16,16 +16,16 @@
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[admin_command_dispatch(handler_prefix = "raw")]
#[derive(Debug, Subcommand)]
#[allow(clippy::enum_variant_names)]
/// Query tables from database
pub(crate) enum RawCommand {
/// - List database maps
RawMaps,
Maps,
/// - Raw database query
RawGet {
Get {
/// Map name
map: String,
@@ -37,17 +37,8 @@ pub(crate) enum RawCommand {
base64: bool,
},
/// - Raw database delete (for string keys)
RawDel {
/// Map name
map: String,
/// Key
key: String,
},
/// - Raw database keys iteration
RawKeys {
Keys {
/// Map name
map: String,
@@ -56,7 +47,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database key size breakdown
RawKeysSizes {
KeysSizes {
/// Map name
map: Option<String>,
@@ -65,7 +56,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database keys total bytes
RawKeysTotal {
KeysTotal {
/// Map name
map: Option<String>,
@@ -74,7 +65,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database values size breakdown
RawValsSizes {
ValsSizes {
/// Map name
map: Option<String>,
@@ -83,7 +74,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database values total bytes
RawValsTotal {
ValsTotal {
/// Map name
map: Option<String>,
@@ -92,7 +83,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database items iteration
RawIter {
Iter {
/// Map name
map: String,
@@ -101,7 +92,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database keys iteration
RawKeysFrom {
KeysFrom {
/// Map name
map: String,
@@ -114,7 +105,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database items iteration
RawIterFrom {
IterFrom {
/// Map name
map: String,
@@ -127,7 +118,7 @@ pub(crate) enum RawCommand {
},
/// - Raw database record count
RawCount {
Count {
/// Map name
map: Option<String>,
@@ -135,7 +126,26 @@ pub(crate) enum RawCommand {
prefix: Option<String>,
},
/// - Compact database
/// - Raw database delete (for string keys) DANGER!!!
Del {
/// Map name
map: String,
/// Key
key: String,
},
/// - Clear database table DANGER!!!
Clear {
/// Map name
map: String,
/// Confirm
#[arg(long)]
confirm: bool,
},
/// - Compact database DANGER!!!
Compact {
#[arg(short, long, alias("column"))]
map: Option<Vec<String>>,
@@ -165,7 +175,7 @@ pub(crate) enum RawCommand {
}
#[admin_command]
pub(super) async fn compact(
pub(super) async fn raw_compact(
&self,
map: Option<Vec<String>>,
start: Option<String>,
@@ -425,6 +435,28 @@ pub(super) async fn raw_del(&self, map: String, key: String) -> Result {
.await
}
#[admin_command]
pub(super) async fn raw_clear(&self, map: String, confirm: bool) -> Result {
let map = self.services.db.get(&map)?;
if !confirm {
return Err!("Are you really sure you want to clear all data? Add the --confirm option.");
}
let timer = Instant::now();
let cork = self.services.db.cork();
map.raw_keys()
.ignore_err()
.ready_for_each(|key| map.remove(&key))
.boxed()
.await;
drop(cork);
let query_time = timer.elapsed();
self.write_str(&format!("Operation completed in {query_time:?}"))
.await
}
#[admin_command]
pub(super) async fn raw_get(&self, map: String, key: String, base64: bool) -> Result {
let map = self.services.db.get(&map)?;
+14
View File
@@ -8,6 +8,13 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/rooms/alias.rs
pub(crate) enum RoomAliasCommand {
/// - Resolve any local or remote alias.
ResolveAlias {
/// Full room alias
alias: OwnedRoomAliasId,
},
/// - Resolve an alias on this server.
ResolveLocalAlias {
/// Full room alias
alias: OwnedRoomAliasId,
@@ -28,6 +35,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
let services = context.services;
match subcommand {
| RoomAliasCommand::ResolveAlias { alias } => {
let timer = tokio::time::Instant::now();
let results = services.alias.resolve_alias(&alias).await;
let query_time = timer.elapsed();
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
},
| RoomAliasCommand::ResolveLocalAlias { alias } => {
let timer = tokio::time::Instant::now();
let results = services.alias.resolve_local_alias(&alias).await;
+22 -2
View File
@@ -74,6 +74,10 @@ pub(crate) enum RoomStateCacheCommand {
user_id: OwnedUserId,
room_id: OwnedRoomId,
},
UserMemberships {
user_id: OwnedUserId,
},
}
pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context<'_>) -> Result {
@@ -282,7 +286,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.state_cache
.rooms_invited(&user_id)
.rooms_invited_state(&user_id)
.collect()
.await;
let query_time = timer.elapsed();
@@ -297,7 +301,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.state_cache
.rooms_left(&user_id)
.rooms_left_state(&user_id)
.collect()
.await;
let query_time = timer.elapsed();
@@ -316,6 +320,22 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.await;
let query_time = timer.elapsed();
context
.write_str(&format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
))
.await
},
| RoomStateCacheCommand::UserMemberships { user_id } => {
let timer = tokio::time::Instant::now();
let results = services
.state_cache
.all_user_memberships(&user_id)
.map(|(membership, room_id)| (membership, room_id.to_owned()))
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
context
.write_str(&format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
+10 -2
View File
@@ -25,7 +25,11 @@ pub(crate) enum RoomTimelineCommand {
#[admin_command]
pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.alias.resolve(&room_id).await?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
let result = self
.services
@@ -43,7 +47,11 @@ pub(super) async fn pdus(
from: Option<String>,
limit: Option<usize>,
) -> Result {
let room_id = self.services.alias.resolve(&room_id).await?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
let from: Option<PduCount> = from.as_deref().map(str::parse).transpose()?;
+5 -1
View File
@@ -30,7 +30,11 @@ pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result {
#[admin_command]
pub(super) async fn short_room_id(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.alias.resolve(&room_id).await?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
let shortid = self
.services
+82
View File
@@ -0,0 +1,82 @@
use clap::Subcommand;
use ruma::{OwnedDeviceId, OwnedUserId};
use tuwunel_core::Result;
use tuwunel_service::sync::into_connection_key;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// Query sync service state
pub(crate) enum SyncCommand {
/// List sliding-sync connections.
ListConnections,
/// Show details of sliding sync connection by ID.
ShowConnection {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
conn_id: Option<String>,
},
/// Drop connections for a user, device, or all.
DropConnections {
user_id: Option<OwnedUserId>,
device_id: Option<OwnedDeviceId>,
conn_id: Option<String>,
},
}
#[admin_command]
pub(super) async fn list_connections(&self) -> Result {
let connections = self.services.sync.list_loaded_connections().await;
for connection_key in connections {
self.write_str(&format!("{connection_key:?}\n"))
.await?;
}
Ok(())
}
#[admin_command]
pub(super) async fn show_connection(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
conn_id: Option<String>,
) -> Result {
let key = into_connection_key(user_id, device_id, conn_id);
let cache = self
.services
.sync
.get_loaded_connection(&key)
.await?;
let out;
{
let cached = cache.lock().await;
out = format!("{cached:#?}");
};
self.write_str(out.as_str()).await
}
#[admin_command]
pub(super) async fn drop_connections(
&self,
user_id: Option<OwnedUserId>,
device_id: Option<OwnedDeviceId>,
conn_id: Option<String>,
) -> Result {
self.services
.sync
.clear_connections(
user_id.as_deref(),
device_id.as_deref(),
conn_id.map(Into::into).as_ref(),
)
.await;
Ok(())
}
+2 -2
View File
@@ -67,7 +67,7 @@ pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result {
}
#[admin_command]
pub(super) async fn delete_room(&self, room_id: OwnedRoomId) -> Result {
pub(super) async fn delete_room(&self, room_id: OwnedRoomId, force: bool) -> Result {
if self.services.admin.is_admin_room(&room_id).await {
return Err!("Cannot delete admin room");
}
@@ -76,7 +76,7 @@ pub(super) async fn delete_room(&self, room_id: OwnedRoomId) -> Result {
self.services
.delete
.delete_room(&room_id, state_lock)
.delete_room(&room_id, force, state_lock)
.await?;
self.write_str("Successfully deleted the room from our database.")
+3
View File
@@ -60,5 +60,8 @@ pub(super) enum RoomCommand {
/// - Delete room
DeleteRoom {
room_id: OwnedRoomId,
#[arg(short, long)]
force: bool,
},
}
+7 -7
View File
@@ -48,7 +48,7 @@ pub(crate) enum RoomModerationCommand {
async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
debug!("Got room alias or ID: {}", room);
let admin_room_alias = &self.services.globals.admin_alias;
let admin_room_alias = &self.services.admin.admin_alias;
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) {
@@ -105,7 +105,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
match self
.services
.alias
.resolve_alias(room_alias, None)
.resolve_alias(room_alias)
.await
{
| Ok((room_id, servers)) => {
@@ -156,7 +156,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
if let Err(e) = self
.services
.membership
.leave(user_id, &room_id, None, &state_lock)
.leave(user_id, &room_id, None, false, &state_lock)
.boxed()
.await
{
@@ -209,7 +209,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.drain(1..self.body.len().saturating_sub(1))
.collect::<Vec<_>>();
let admin_room_alias = &self.services.globals.admin_alias;
let admin_room_alias = &self.services.admin.admin_alias;
let mut room_ban_count: usize = 0;
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
@@ -260,7 +260,7 @@ async fn ban_list_of_rooms(&self) -> Result {
match self
.services
.alias
.resolve_alias(room_alias, None)
.resolve_alias(room_alias)
.await
{
| Ok((room_id, servers)) => {
@@ -331,7 +331,7 @@ async fn ban_list_of_rooms(&self) -> Result {
if let Err(e) = self
.services
.membership
.leave(user_id, &room_id, None, &state_lock)
.leave(user_id, &room_id, None, false, &state_lock)
.boxed()
.await
{
@@ -423,7 +423,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
match self
.services
.alias
.resolve_alias(room_alias, None)
.resolve_alias(room_alias)
.await
{
| Ok((room_id, servers)) => {
+4 -4
View File
@@ -67,7 +67,7 @@ pub(super) async fn list_features(&self, available: bool, enabled: bool, comma:
#[admin_command]
pub(super) async fn memory_usage(&self) -> Result {
let services_usage = self.services.memory_usage().await?;
let database_usage = self.services.db.db.memory_usage()?;
let database_usage = self.services.db.engine.memory_usage()?;
let allocator_usage = tuwunel_core::alloc::memory_usage()
.map_or(String::new(), |s| format!("\nAllocator:\n{s}"));
@@ -88,7 +88,7 @@ pub(super) async fn clear_caches(&self) -> Result {
pub(super) async fn list_backups(&self) -> Result {
self.services
.db
.db
.engine
.backup_list()?
.try_stream()
.try_for_each(|result| write!(self, "{result}"))
@@ -102,13 +102,13 @@ pub(super) async fn backup_database(&self) -> Result {
.services
.server
.runtime()
.spawn_blocking(move || match db.db.backup() {
.spawn_blocking(move || match db.engine.backup() {
| Ok(()) => "Done".to_owned(),
| Err(e) => format!("Failed: {e}"),
})
.await?;
let count = self.services.db.db.backup_count()?;
let count = self.services.db.engine.backup_count()?;
self.write_str(&format!("{result}. Currently have {count} backups."))
.await
}
+40 -9
View File
@@ -2,7 +2,7 @@
use futures::{FutureExt, StreamExt};
use ruma::{
Int, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
Int, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
events::{
RoomAccountDataEventType, StateEventType,
room::{
@@ -119,7 +119,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
.is_empty()
{
for room in &self.services.server.config.auto_join_rooms {
let Ok(room_id) = self.services.alias.resolve(room).await else {
let Ok(room_id) = self.services.alias.maybe_resolve(room).await else {
error!(
%user_id,
"Failed to resolve room alias to room ID when attempting to auto join {room}, skipping"
@@ -224,6 +224,25 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
.await
}
#[admin_command]
pub(super) async fn delete_device(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
) -> Result {
if !self.services.globals.user_is_local(&user_id) {
return Err!("Cannot delete device of remote user");
}
self.services
.users
.remove_device(&user_id, &device_id)
.await;
self.write_str(&format!("User {user_id}'s device {device_id} removed."))
.await
}
#[admin_command]
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
@@ -408,7 +427,7 @@ pub(super) async fn force_join_list_of_local_users(
let (room_id, servers) = self
.services
.alias
.resolve_with_servers(&room_id, None)
.maybe_resolve_with_servers(&room_id, None)
.await?;
if !self
@@ -532,7 +551,7 @@ pub(super) async fn force_join_all_local_users(
let (room_id, servers) = self
.services
.alias
.resolve_with_servers(&room_id, None)
.maybe_resolve_with_servers(&room_id, None)
.await?;
if !self
@@ -617,7 +636,7 @@ pub(super) async fn force_join_room(
let (room_id, servers) = self
.services
.alias
.resolve_with_servers(&room_id, None)
.maybe_resolve_with_servers(&room_id, None)
.await?;
assert!(
@@ -645,7 +664,11 @@ pub(super) async fn force_leave_room(
room_id: OwnedRoomOrAliasId,
) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
let room_id = self.services.alias.resolve(&room_id).await?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
assert!(
self.services.globals.user_is_local(&user_id),
@@ -665,7 +688,7 @@ pub(super) async fn force_leave_room(
self.services
.membership
.leave(&user_id, &room_id, None, &state_lock)
.leave(&user_id, &room_id, None, false, &state_lock)
.boxed()
.await?;
@@ -678,7 +701,11 @@ pub(super) async fn force_leave_room(
#[admin_command]
pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAliasId) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
let room_id = self.services.alias.resolve(&room_id).await?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
assert!(
self.services.globals.user_is_local(&user_id),
@@ -744,7 +771,11 @@ pub(super) async fn force_promote(
room_id: OwnedRoomOrAliasId,
) -> Result {
let target_id = parse_user_id(self.services, &target_id)?;
let room_id = self.services.alias.resolve(&room_id).await?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
let state_lock = self.services.state.mutex.lock(&room_id).await;
+7 -1
View File
@@ -1,7 +1,7 @@
mod commands;
use clap::Subcommand;
use ruma::{OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId};
use ruma::{OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId};
use tuwunel_core::Result;
use crate::admin_command_dispatch;
@@ -59,6 +59,12 @@ pub(super) enum UserCommand {
force: bool,
},
/// - Deletes a user's device.
DeleteDevice {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
},
/// - List local users in the database
#[clap(alias = "list")]
ListUsers,
+1
View File
@@ -11,6 +11,7 @@ version.workspace = true
[lib]
path = "mod.rs"
bench = false
crate-type = [
"rlib",
# "dylib",
+8 -89
View File
@@ -1,18 +1,13 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, StreamExt};
use ruma::api::client::{
account::{
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
whoami,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
use ruma::api::client::account::{
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
};
use tuwunel_core::{Err, Error, Result, err, info, utils, utils::ReadyExt};
use tuwunel_core::{Err, Result, info, utils::ReadyExt};
use super::SESSION_ID_LENGTH;
use crate::Ruma;
use crate::{Ruma, router::auth_uiaa};
/// # `POST /_matrix/client/r0/account/password`
///
@@ -37,45 +32,7 @@ pub(crate) async fn change_password_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
// Authentication for this endpoint was made optional, but we need
// authentication currently
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), 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(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.users
@@ -87,7 +44,7 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(sender_user)
.ready_filter(|id| *id != body.sender_device())
.ready_filter(|&id| Some(id) != body.sender_device.as_deref())
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
}
@@ -140,44 +97,7 @@ pub(crate) async fn deactivate_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
// Authentication for this endpoint was made optional, but we need
// authentication currently
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), 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(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.deactivate
@@ -186,7 +106,6 @@ pub(crate) async fn deactivate_route(
.await?;
info!("User {sender_user} deactivated their account.");
if services.server.config.admin_room_notices {
services
.admin
+6 -8
View File
@@ -5,7 +5,7 @@
OwnedServerName, RoomAliasId, RoomId,
api::client::alias::{create_alias, delete_alias, get_alias},
};
use tuwunel_core::{Err, Result, debug};
use tuwunel_core::{Err, Result, debug, err};
use tuwunel_service::Services;
use crate::Ruma;
@@ -26,8 +26,8 @@ pub(crate) async fn create_alias_route(
// this isn't apart of alias_checks or delete alias route because we should
// allow removing forbidden room aliases
if services
.globals
.forbidden_alias_names()
.config
.forbidden_alias_names
.is_match(body.room_alias.alias())
{
return Err!(Request(Forbidden("Room alias is forbidden.")));
@@ -83,13 +83,11 @@ pub(crate) async fn get_alias_route(
) -> Result<get_alias::v3::Response> {
let room_alias = body.body.room_alias;
let Ok((room_id, servers)) = services
let (room_id, servers) = services
.alias
.resolve_alias(&room_alias, None)
.resolve_alias(&room_alias)
.await
else {
return Err!(Request(NotFound("Room with alias not found.")));
};
.map_err(|_| err!(Request(NotFound("Room with alias not found."))))?;
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
debug!(?room_alias, ?room_id, "available servers: {servers:?}");
+2 -2
View File
@@ -100,7 +100,7 @@ pub(crate) async fn get_context_route(
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit / 2)
.take(limit.div_ceil(2))
.collect();
let (base_event, events_before, events_after): (_, Vec<_>, Vec<_>) =
@@ -134,7 +134,7 @@ pub(crate) async fn get_context_route(
.map_or_else(|| body.event_id.as_ref(), |pdu| pdu.event_id.as_ref());
let state_ids = services
.state_accessor
.state
.pdu_shortstatehash(state_at)
.or_else(|_| services.state.get_room_shortstatehash(room_id))
.map_ok(|shortstatehash| {
+132
View File
@@ -0,0 +1,132 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::StreamExt;
use ruma::api::client::dehydrated_device::{
delete_dehydrated_device::unstable as delete_dehydrated_device,
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
put_dehydrated_device::unstable as put_dehydrated_device,
};
use tuwunel_core::{Err, Result, at, utils::result::IsErrOr};
use crate::Ruma;
const MAX_BATCH_EVENTS: usize = 50;
/// # `PUT /_matrix/client/../dehydrated_device`
///
/// Creates or overwrites the user's dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn put_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let sender_user = body
.sender_user
.as_deref()
.expect("AccessToken authentication required");
let device_id = body.body.device_id.clone();
services
.users
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response { device_id })
}
/// # `DELETE /_matrix/client/../dehydrated_device`
///
/// Deletes the user's dehydrated device without replacement.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn delete_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device_id = services
.users
.get_dehydrated_device_id(sender_user)
.await?;
services
.users
.remove_device(sender_user, &device_id)
.await;
Ok(delete_dehydrated_device::Response { device_id })
}
/// # `GET /_matrix/client/../dehydrated_device`
///
/// Gets the user's dehydrated device
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device = services
.users
.get_dehydrated_device(sender_user)
.await?;
Ok(get_dehydrated_device::Response {
device_id: device.device_id,
device_data: device.device_data,
})
}
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
///
/// Paginates the events of the dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_events_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.sender_user();
let device_id = &body.body.device_id;
let existing_id = services
.users
.get_dehydrated_device_id(sender_user)
.await;
if existing_id
.as_ref()
.is_err_or(|existing_id| existing_id != device_id)
{
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
}
let since: Option<u64> = body
.body
.next_batch
.as_deref()
.map(str::parse)
.transpose()?;
let mut next_batch: Option<u64> = None;
let events = services
.users
.get_to_device_events(sender_user, device_id, since, None)
.take(MAX_BATCH_EVENTS)
.inspect(|&(count, _)| {
next_batch.replace(count);
})
.map(at!(1))
.collect()
.await;
Ok(get_events::Response {
events,
next_batch: next_batch.as_ref().map(ToString::to_string),
})
}
+16 -77
View File
@@ -3,16 +3,13 @@
use futures::StreamExt;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
api::client::device::{
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use tuwunel_core::{Err, Error, Result, debug, err, utils};
use tuwunel_core::{Err, Result, debug, err, utils, utils::string::to_small_string};
use super::SESSION_ID_LENGTH;
use crate::{Ruma, client::DEVICE_ID_LENGTH};
use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa};
/// # `GET /_matrix/client/r0/devices`
///
@@ -64,18 +61,21 @@ pub(crate) async fn update_device_route(
.await
{
| Ok(mut device) => {
let notify = device.display_name != body.display_name;
device.display_name.clone_from(&body.display_name);
device
.last_seen_ip
.clone_from(&Some(client.to_string()));
.clone_from(&Some(to_small_string(client)));
device
.last_seen_ts
.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));
assert_eq!(device.device_id, body.device_id, "device_id mismatch");
services
.users
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
.put_device_metadata(sender_user, notify, &device);
Ok(update_device::v3::Response {})
},
@@ -83,6 +83,7 @@ pub(crate) async fn update_device_route(
let Some(appservice) = appservice else {
return Err!(Request(NotFound("Device not found.")));
};
if !appservice.registration.device_management {
return Err!(Request(NotFound("Device not found.")));
}
@@ -100,7 +101,7 @@ pub(crate) async fn update_device_route(
.create_device(
sender_user,
&device_id,
(&appservice.registration.as_token, None),
(Some(&appservice.registration.as_token), None),
None,
None,
Some(client.to_string()),
@@ -126,10 +127,10 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> {
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
let sender_user = body.sender_user();
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
@@ -142,38 +143,7 @@ pub(crate) async fn delete_device_route(
return Ok(delete_device::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err!(Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("Not json.")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.users
@@ -200,10 +170,10 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> {
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
let sender_user = body.sender_user();
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
@@ -218,38 +188,7 @@ pub(crate) async fn delete_devices_route(
return Ok(delete_devices::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, 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(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
for device_id in &body.devices {
services
+18 -3
View File
@@ -403,9 +403,24 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
let canonical_alias = services
.state_accessor
.get_canonical_alias(&room_id)
.ok();
.ok()
.then(async |alias| {
if let Some(alias) = alias
&& services.globals.alias_is_local(&alias)
&& let Ok(alias_room_id) = services.alias.resolve_local_alias(&alias).await
&& alias_room_id == room_id
{
Some(alias)
} else {
None
}
});
let avatar_url = services.state_accessor.get_avatar(&room_id);
let avatar_url = services
.state_accessor
.get_avatar(&room_id)
.map_ok(|content| content.url)
.ok();
let topic = services
.state_accessor
@@ -441,7 +456,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
.await;
PublicRoomsChunk {
avatar_url: avatar_url.into_option().unwrap_or_default().url,
avatar_url: avatar_url.flatten(),
canonical_alias,
guest_can_join,
join_rule: join_rule.unwrap_or_default(),
+116
View File
@@ -0,0 +1,116 @@
use std::iter::once;
use axum::extract::State;
use futures::StreamExt;
use ruma::api::client::peeking::listen_to_new_events::v3::{Request, Response};
use tokio::time::{Duration, Instant, timeout_at};
use tuwunel_core::{
Err, Event, Result, at,
matrix::PduCount,
utils::{
BoolExt,
result::FlatOk,
stream::{IterStream, ReadyExt},
},
};
use crate::Ruma;
const EVENT_LIMIT: usize = 50;
/// GET `/_matrix/client/v3/events`
pub(crate) async fn events_route(
State(services): State<crate::State>,
body: Ruma<Request>,
) -> Result<Response> {
let (sender_user, sender_device) = body.sender();
let from = body
.body
.from
.as_deref()
.map(str::parse)
.flat_ok()
.unwrap_or_default();
let timeout = body
.body
.timeout
.as_ref()
.map(Duration::as_millis)
.map(TryInto::try_into)
.flat_ok()
.unwrap_or(services.config.client_sync_timeout_default)
.max(services.config.client_sync_timeout_min)
.min(services.config.client_sync_timeout_max);
let Some(room_id) = body.room_id.as_deref() else {
//TODO: upgrade ruma
return Err!(Request(InvalidParam("Missing RoomId parameter.")));
};
if !services
.state_accessor
.user_can_see_state_events(sender_user, room_id)
.await
{
return Err!(Request(Forbidden("No room preview available.")));
}
let stop_at = Instant::now()
.checked_add(Duration::from_millis(timeout))
.expect("configuration must limit maximum timeout");
loop {
let watchers = services
.sync
.watch(sender_user, sender_device, once(room_id).stream());
let next_batch = services.globals.wait_pending().await?;
let events = services
.timeline
.pdus(Some(sender_user), room_id, Some(PduCount::Normal(from)))
.ready_filter_map(Result::ok)
.ready_take_while(|(count, _)| PduCount::Normal(next_batch).ge(count))
.take(EVENT_LIMIT)
.collect::<Vec<_>>()
.await;
if !events.is_empty() {
return Ok(Response {
start: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
end: events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
});
}
if timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
return Ok(Response {
chunk: Default::default(),
start: body.body.from,
end: services
.server
.is_stopping()
.is_false()
.then_some(next_batch)
.as_ref()
.map(ToString::to_string),
});
}
}
}
+53 -82
View File
@@ -6,14 +6,10 @@
CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
UserId,
api::{
client::{
error::ErrorKind,
keys::{
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
client::keys::{
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
},
federation,
},
@@ -21,11 +17,12 @@
serde::Raw,
};
use serde_json::json;
use tuwunel_core::{Err, Error, Result, debug, debug_warn, err, result::NotFound, utils};
use tuwunel_core::{
Err, Result, debug, debug_error, debug_warn, err, result::NotFound, utils::json,
};
use tuwunel_service::{Services, users::parse_master_key};
use super::SESSION_ID_LENGTH;
use crate::Ruma;
use crate::{Ruma, router::auth_uiaa};
/// # `POST /_matrix/client/r0/keys/upload`
///
@@ -40,26 +37,16 @@ pub(crate) async fn upload_keys_route(
) -> Result<upload_keys::v3::Response> {
let (sender_user, sender_device) = body.sender();
for (key_id, one_time_key) in &body.one_time_keys {
if one_time_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
?key_id,
?one_time_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
})
.is_err()
{
continue;
}
let one_time_keys = body
.one_time_keys
.iter()
.take(services.config.one_time_key_limit)
.map(|(id, val)| (id.as_ref(), val));
services
.users
.add_one_time_key(sender_user, sender_device, key_id, one_time_key)
.await?;
}
services
.users
.add_one_time_keys(sender_user, sender_device, one_time_keys)
.await?;
if let Some(device_keys) = &body.device_keys {
let deser_device_keys = device_keys.deserialize().map_err(|e| {
@@ -84,8 +71,11 @@ pub(crate) async fn upload_keys_route(
.users
.get_device_keys(sender_user, sender_device)
.await
.and_then(|keys| keys.deserialize().map_err(Into::into))
{
if existing_keys.json().get() == device_keys.json().get() {
// NOTE: also serves as a workaround for a nheko bug which omits cross-signing
// NOTE: signatures when re-uploading the same DeviceKeys.
if existing_keys.keys == deser_device_keys.keys {
debug!(
?sender_user,
?sender_device,
@@ -158,23 +148,17 @@ pub(crate) async fn upload_signing_keys_route(
State(services): State<crate::State>,
body: Ruma<upload_signing_keys::v3::Request>,
) -> Result<upload_signing_keys::v3::Response> {
let (sender_user, sender_device) = body.sender();
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
// Access token is required for this endpoint regardless of conditional UIAA so
// we'll always have a sender_user.
match check_for_new_keys(
services,
sender_user,
body.sender_user(),
body.self_signing_key.as_ref(),
body.user_signing_key.as_ref(),
body.master_key.as_ref(),
)
.await
.inspect_err(|e| debug!(?e))
.inspect_err(|e| debug_error!(?e))
{
| Ok(exists) => {
if let Some(result) = exists {
@@ -182,45 +166,25 @@ pub(crate) async fn upload_signing_keys_route(
// (lost connection for example)
return Ok(result);
}
debug!(
"Skipping UIA in accordance with MSC3967, the user didn't have any existing keys"
);
// Some of the keys weren't found, so we let them upload
debug!("Skipping UIA in accordance with MSC3967, user had no existing keys");
},
| _ => {
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body.as_ref() {
| Some(json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
},
},
}
let authed_user = auth_uiaa(&services, &body).await?;
assert_eq!(
body.sender_user(),
authed_user,
"Expected UIAA of {0} and not {authed_user}",
body.sender_user(),
);
},
}
services
.users
.add_cross_signing_keys(
sender_user,
body.sender_user(),
&body.master_key,
&body.self_signing_key,
&body.user_signing_key,
@@ -246,6 +210,7 @@ async fn check_for_new_keys(
.users
.get_master_key(None, user_id, &|_| true)
.await;
if result.is_not_found() {
empty = true;
} else {
@@ -258,6 +223,7 @@ async fn check_for_new_keys(
}
}
}
if let Some(user_signing_key) = user_signing_key {
let key = services.users.get_user_signing_key(user_id).await;
if key.is_not_found() && !empty {
@@ -265,6 +231,7 @@ async fn check_for_new_keys(
"Tried to update an existing user signing key, UIA required"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != user_signing_key.deserialize()? {
@@ -274,17 +241,20 @@ async fn check_for_new_keys(
}
}
}
if let Some(self_signing_key) = self_signing_key {
let key = services
.users
.get_self_signing_key(None, user_id, &|_| true)
.await;
if key.is_not_found() && !empty {
debug!(?key);
debug_error!(?key);
return Err!(Request(Forbidden(
"Tried to add a new signing key independently from the master key"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != self_signing_key.deserialize()? {
@@ -294,6 +264,7 @@ async fn check_for_new_keys(
}
}
}
if empty {
return Ok(None);
}
@@ -377,12 +348,12 @@ pub(crate) async fn get_key_changes_route(
let from = body
.from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?;
.map_err(|_| err!(Request(InvalidParam("Invalid `from`."))))?;
let to = body
.to
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?;
.map_err(|_| err!(Request(InvalidParam("Invalid `to`."))))?;
device_list_updates.extend(
services
@@ -557,16 +528,16 @@ pub(crate) async fn get_keys_helper<F>(
.signatures
.append(&mut our_master_key.signatures);
}
let json = serde_json::to_value(master_key).expect("to_value always works");
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
// Dont notify. A notification would trigger another key request resulting in
// an endless loop.
let notify = false;
let raw = Some(json::to_raw(master_key)?);
services
.users
.add_cross_signing_keys(
&user, &raw, &None, &None,
false, /* Dont notify. A notification would trigger another key
* request resulting in an endless loop */
)
.add_cross_signing_keys(&user, &raw, &None, &None, notify)
.await?;
if let Some(raw) = raw {
master_keys.insert(user.clone(), raw);
}
@@ -603,7 +574,7 @@ fn add_unsigned_device_display_name(
.or_insert_with(|| CanonicalJsonObject::default().into())
{
let display_name = if include_display_names {
CanonicalJsonValue::String(display_name)
CanonicalJsonValue::String(display_name.to_string())
} else {
CanonicalJsonValue::String(metadata.device_id.into())
};
+13 -8
View File
@@ -22,6 +22,7 @@
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
matrix::{
PduCount,
event::{Event, gen_event_id},
pdu::{PduBuilder, PduEvent},
},
@@ -209,11 +210,11 @@ async fn knock_room_helper_local(
})?;
knock_event_stub.insert(
"origin".to_owned(),
"origin".into(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
"origin_server_ts".into(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
@@ -221,7 +222,7 @@ async fn knock_room_helper_local(
),
);
knock_event_stub.insert(
"content".to_owned(),
"content".into(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
@@ -243,7 +244,7 @@ async fn knock_room_helper_local(
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
@@ -276,6 +277,7 @@ async fn knock_room_helper_local(
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
info!("Updating membership locally to knock state with provided stripped state events");
let count = services.globals.next_count();
services
.state_cache
.update_membership(
@@ -294,6 +296,7 @@ async fn knock_room_helper_local(
),
None,
false,
PduCount::Normal(*count),
)
.await?;
@@ -343,11 +346,11 @@ async fn knock_room_helper_remote(
})?;
knock_event_stub.insert(
"origin".to_owned(),
"origin".into(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
"origin_server_ts".into(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
@@ -355,7 +358,7 @@ async fn knock_room_helper_remote(
),
);
knock_event_stub.insert(
"content".to_owned(),
"content".into(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
@@ -377,7 +380,7 @@ async fn knock_room_helper_remote(
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
@@ -490,6 +493,7 @@ async fn knock_room_helper_remote(
.await?;
info!("Updating membership locally to knock state with provided stripped state events");
let count = services.globals.next_count();
services
.state_cache
.update_membership(
@@ -508,6 +512,7 @@ async fn knock_room_helper_remote(
),
None,
false,
PduCount::Normal(*count),
)
.await?;
+1 -1
View File
@@ -18,7 +18,7 @@ pub(crate) async fn leave_room_route(
services
.membership
.leave(body.sender_user(), &body.room_id, body.reason.clone(), &state_lock)
.leave(body.sender_user(), &body.room_id, body.reason.clone(), false, &state_lock)
.boxed()
.await?;
+41 -23
View File
@@ -1,16 +1,26 @@
use axum::extract::State;
use futures::{FutureExt, StreamExt};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
api::client::membership::{
get_member_events::{self, v3::MembershipEventFilter},
get_member_events::{self},
joined_members::{self, v3::RoomMember},
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
room::{
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
};
use tuwunel_core::{
Err, Result, at,
matrix::Event,
utils::{
future::{BoolExt, TryExtExt},
stream::ReadyExt,
},
};
use tuwunel_core::{Err, Result, at, matrix::Event, utils::stream::ReadyExt};
use crate::Ruma;
@@ -29,7 +39,9 @@ pub(crate) async fn get_member_events_route(
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
return Err!(Request(Forbidden(
"You aren't a member of the room and weren't previously a member of the room."
)));
}
let membership = body.membership.as_ref();
@@ -59,12 +71,20 @@ pub(crate) async fn joined_members_route(
State(services): State<crate::State>,
body: Ruma<joined_members::v3::Request>,
) -> Result<joined_members::v3::Response> {
if !services
let is_joined = services
.state_cache
.is_joined(body.sender_user(), &body.room_id);
let is_world_readable = services
.state_accessor
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
.room_state_get_content(&body.room_id, &StateEventType::RoomHistoryVisibility, "")
.map_ok_or(false, |c: RoomHistoryVisibilityEventContent| {
c.history_visibility == HistoryVisibility::WorldReadable
});
pin_mut!(is_joined, is_world_readable);
if !is_joined.or(is_world_readable).await {
return Err!(Request(Forbidden("You aren't a member of the room.")));
}
Ok(joined_members::v3::Response {
@@ -74,9 +94,7 @@ pub(crate) async fn joined_members_route(
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| {
membership_filter(pdu, Some(&MembershipEventFilter::Join), None)
})
.ready_filter_map(|pdu| membership_filter(pdu, Some(&MembershipState::Join), None))
.ready_filter_map(|pdu| {
let content = pdu.get_content::<RoomMemberEventContent>().ok()?;
let sender = pdu.sender().to_owned();
@@ -95,22 +113,22 @@ pub(crate) async fn joined_members_route(
fn membership_filter<Pdu: Event>(
pdu: Pdu,
for_membership: Option<&MembershipEventFilter>,
not_membership: Option<&MembershipEventFilter>,
for_membership: Option<&MembershipState>,
not_membership: Option<&MembershipState>,
) -> Option<impl Event> {
let membership_state_filter = match for_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
| Some(MembershipState::Ban) => MembershipState::Ban,
| Some(MembershipState::Invite) => MembershipState::Invite,
| Some(MembershipState::Knock) => MembershipState::Knock,
| Some(MembershipState::Leave) => MembershipState::Leave,
| Some(_) | None => MembershipState::Join,
};
let not_membership_state_filter = match not_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Join) => MembershipState::Join,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(MembershipState::Ban) => MembershipState::Ban,
| Some(MembershipState::Invite) => MembershipState::Invite,
| Some(MembershipState::Join) => MembershipState::Join,
| Some(MembershipState::Knock) => MembershipState::Knock,
| Some(_) | None => MembershipState::Leave,
};
+3 -8
View File
@@ -165,10 +165,7 @@ async fn get_join_params(
},
// ... if room alias, resolve and don't shuffle ...
| Err(room_alias) => {
let (room_id, servers) = services
.alias
.resolve_alias(&room_alias, Some(via.to_vec()))
.await?;
let (room_id, servers) = services.alias.resolve_alias(&room_alias).await?;
(room_id, servers, Vec::new())
},
@@ -198,12 +195,10 @@ async fn get_join_params(
);
// shuffle additionals, append to base servers
additional_servers.sort_unstable();
additional_servers.dedup();
shuffle(&mut additional_servers);
servers.append(&mut additional_servers);
servers.sort_unstable();
servers.dedup();
servers.append(&mut additional_servers);
shuffle(&mut servers);
// sort deprioritized servers last
servers.sort_by(|a, b| {
+4
View File
@@ -5,8 +5,10 @@
pub(super) mod backup;
pub(super) mod capabilities;
pub(super) mod context;
pub(super) mod dehydrated_device;
pub(super) mod device;
pub(super) mod directory;
pub(super) mod events;
pub(super) mod filter;
pub(super) mod keys;
pub(super) mod media;
@@ -49,8 +51,10 @@
pub(super) use backup::*;
pub(super) use capabilities::*;
pub(super) use context::*;
pub(super) use dehydrated_device::*;
pub(super) use device::*;
pub(super) use directory::*;
pub(super) use events::*;
pub(super) use filter::*;
pub(super) use keys::*;
pub(super) use media::*;
+11 -15
View File
@@ -46,13 +46,11 @@ pub(crate) async fn set_displayname_route(
.update_displayname(&body.user_id, body.displayname.clone(), &all_joined_rooms)
.await;
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(set_display_name::v3::Response {})
}
@@ -148,14 +146,12 @@ pub(crate) async fn set_avatar_url_route(
)
.await;
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await
.ok();
}
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await
.ok();
Ok(set_avatar_url::v3::Response {})
}
+108 -5
View File
@@ -1,10 +1,11 @@
use axum::extract::State;
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue,
CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch,
api::client::{
error::ErrorKind,
push::{
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions,
delete_pushrule, get_notifications, get_pushers, get_pushrule, get_pushrule_actions,
get_pushrule_enabled, get_pushrules_all, get_pushrules_global_scope, set_pusher,
set_pushrule, set_pushrule_actions, set_pushrule_enabled,
},
@@ -14,15 +15,117 @@
push_rules::{PushRulesEvent, PushRulesEventContent},
},
push::{
InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
Action, InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
RemovePushRuleError, Ruleset,
},
};
use tuwunel_core::{Err, Error, Result, err};
use tuwunel_core::{
Err, Error, Result, at, err,
matrix::{Event, PduId},
utils::{
stream::{ReadyExt, WidebandExt},
string::to_small_string,
},
};
use tuwunel_service::Services;
use crate::Ruma;
/// # `GET /_matrix/client/r0/notifications/`
///
/// Paginate through the list of events the user has been, or would have been
/// notified about.
pub(crate) async fn get_notifications_route(
State(services): State<crate::State>,
body: Ruma<get_notifications::v3::Request>,
) -> Result<get_notifications::v3::Response> {
use get_notifications::v3::Notification;
let sender_user = body.sender_user();
let from = body
.body
.from
.as_deref()
.map(str::parse)
.transpose()
.map_err(|e| err!(Request(InvalidParam("Invalid `from' parameter: {e}"))))?;
let limit: usize = body
.body
.limit
.map(TryInto::try_into)
.transpose()?
.unwrap_or(50)
.clamp(1, 100);
let only_highlight = body
.body
.only
.as_deref()
.is_some_and(|only| only.contains("highlight"));
let mut next_token: Option<u64> = None;
let notifications = services
.pusher
.get_notifications(sender_user, from)
.ready_filter(|(_, notify)| {
if only_highlight && !notify.actions.iter().any(Action::is_highlight) {
return false;
}
true
})
.wide_filter_map(async |(count, notify)| {
let pdu_id = PduId {
shortroomid: notify.sroomid,
count: count.into(),
};
let event = services
.timeline
.get_pdu_from_id(&pdu_id.into())
.await
.ok()
.filter(|event| !event.is_redacted())?;
let read = services
.pusher
.last_notification_read(sender_user, event.room_id())
.await
.is_ok_and(|last_read| last_read.ge(&count));
let ts = notify
.ts
.try_into()
.map(MilliSecondsSinceUnixEpoch)
.ok()?;
let notification = Notification {
room_id: event.room_id().into(),
event: event.into_format(),
ts,
read,
profile_tag: notify.tag,
actions: notify.actions,
};
Some((count, notification))
})
.take(limit)
.inspect(|(count, _)| {
next_token.replace(*count);
})
.map(at!(1))
.collect::<Vec<_>>()
.await;
Ok(get_notifications::v3::Response {
next_token: next_token.map(to_small_string),
notifications,
})
}
/// # `GET /_matrix/client/r0/pushrules/`
///
/// Retrieves the push rules event for this user.
@@ -335,7 +438,7 @@ pub(crate) async fn set_pushrule_actions_route(
if account_data
.content
.global
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone().into())
.is_err()
{
return Err!(Request(NotFound("Push rule not found.")));
+41 -36
View File
@@ -26,6 +26,12 @@ pub(crate) async fn set_read_marker_route(
) -> Result<set_read_marker::v3::Response> {
let sender_user = body.sender_user();
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
services
.pusher
.reset_notification_counts(sender_user, &body.room_id);
}
if let Some(event) = &body.fully_read {
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
content: ruma::events::fully_read::FullyReadEventContent { event_id: event.clone() },
@@ -39,21 +45,26 @@ pub(crate) async fn set_read_marker_route(
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event)?,
)
.await?;
.await
.ok();
}
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
services
.user
.reset_notification_counts(sender_user, &body.room_id);
}
if let Some(event) = &body.private_read_receipt {
let count = services
.timeline
.get_pdu_count(event)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
let PduCount::Normal(count) = count else {
return Err!(Request(InvalidParam(
"Event is a backfilled PDU and cannot be marked as read."
)));
};
// ping presence
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
}
if let Some(event) = &body.read_receipt {
@@ -79,24 +90,16 @@ pub(crate) async fn set_read_marker_route(
},
)
.await;
}
if let Some(event) = &body.private_read_receipt {
let count = services
.timeline
.get_pdu_count(event)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
let PduCount::Normal(count) = count else {
return Err!(Request(InvalidParam(
"Event is a backfilled PDU and cannot be marked as read."
)));
};
services
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
.presence
.maybe_ping_presence(
sender_user,
body.sender_device.as_deref(),
&ruma::presence::PresenceState::Online,
)
.await
.ok();
}
Ok(set_read_marker::v3::Response {})
@@ -116,18 +119,10 @@ pub(crate) async fn create_receipt_route(
create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
) {
services
.user
.pusher
.reset_notification_counts(sender_user, &body.room_id);
}
// ping presence
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
match body.receipt_type {
| create_receipt::v3::ReceiptType::FullyRead => {
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
@@ -171,6 +166,16 @@ pub(crate) async fn create_receipt_route(
},
)
.await;
services
.presence
.maybe_ping_presence(
sender_user,
body.sender_device.as_deref(),
&ruma::presence::PresenceState::Online,
)
.await
.ok();
},
| create_receipt::v3::ReceiptType::ReadPrivate => {
let count = services
+30 -24
View File
@@ -16,7 +16,9 @@
events::GlobalAccountDataEventType,
push,
};
use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn};
use tuwunel_core::{
Err, Error, Result, debug_info, debug_warn, error, info, is_equal_to, utils, warn,
};
use tuwunel_service::users::device::generate_refresh_token;
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH};
@@ -58,8 +60,8 @@ pub(crate) async fn get_register_available_route(
});
if services
.globals
.forbidden_usernames()
.config
.forbidden_usernames
.is_match(&body.username)
{
return Err!(Request(Forbidden("Username is forbidden")));
@@ -181,31 +183,33 @@ pub(crate) async fn register_route(
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!(
if is_guest && !services.config.allow_guest_registration {
let display_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
debug_warn!(
"Guest registration disabled / registration enabled with token configured, \
rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
rejecting guest registration attempt, initial device name: \"{display_name}\""
);
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 {
let display_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
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("")
rejecting registration. Guest's initial device name: \"{display_name}\""
);
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
@@ -228,8 +232,8 @@ pub(crate) async fn register_route(
});
if services
.globals
.forbidden_usernames()
.config
.forbidden_usernames
.is_match(username)
&& !emergency_mode_enabled
{
@@ -309,7 +313,7 @@ pub(crate) async fn register_route(
// UIAA
let mut uiaainfo;
let skip_auth = if services.globals.registration_token.is_some() {
let skip_auth = if services.globals.registration_token.is_some() && !is_guest {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
@@ -320,6 +324,7 @@ pub(crate) async fn register_route(
session: None,
auth_error: None,
};
body.appservice_info.is_some()
} else {
// No registration token necessary, but clients must still go through the flow
@@ -330,6 +335,7 @@ pub(crate) async fn register_route(
session: None,
auth_error: None,
};
body.appservice_info.is_some() || is_guest
};
@@ -384,8 +390,8 @@ pub(crate) async fn register_route(
// 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()
.config
.new_user_displayname_suffix
.is_empty()
&& body.appservice_info.is_none()
{
@@ -446,7 +452,7 @@ pub(crate) async fn register_route(
.create_device(
&user_id,
&device_id,
(&access_token, expires_in),
(Some(&access_token), expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.clone(),
Some(client.to_string()),
@@ -535,7 +541,7 @@ pub(crate) async fn register_route(
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.alias.resolve(room).await else {
let Ok(room_id) = services.alias.maybe_resolve(room).await else {
error!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}, skipping"
+5 -7
View File
@@ -154,13 +154,11 @@ async fn paginate_relations_with_filter(
.collect()
.await;
let next_batch = match dir {
| Direction::Forward => events.last(),
| Direction::Backward => events.first(),
}
.map(at!(0))
.as_ref()
.map(ToString::to_string);
let next_batch = events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string);
Ok(get_relating_events::v1::Response {
next_batch,
+60 -6
View File
@@ -3,7 +3,8 @@
use axum::extract::State;
use futures::{FutureExt, future::OptionFuture};
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
CanonicalJsonObject, EventEncryptionAlgorithm, Int, OwnedRoomAliasId, OwnedRoomId,
OwnedUserId, RoomId, RoomVersionId,
api::client::room::{
self, create_room,
create_room::v3::{CreationContent, RoomPreset},
@@ -13,6 +14,7 @@
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
encryption::RoomEncryptionEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
@@ -262,6 +264,7 @@ pub(crate) async fn create_room_route(
.await?;
// 6. Events listed in initial_state
let mut is_encrypted = false;
for event in &body.initial_state {
let mut pdu_builder = event
.deserialize_as_unchecked::<PduBuilder>()
@@ -292,6 +295,10 @@ pub(crate) async fn create_room_route(
continue;
}
if pdu_builder.event_type == TimelineEventType::RoomEncryption {
is_encrypted = true;
}
services
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
@@ -299,6 +306,33 @@ pub(crate) async fn create_room_route(
.await?;
}
if services.config.allow_encryption && !is_encrypted {
use RoomPreset::*;
let config = services
.config
.encryption_enabled_by_default_for_room_type
.as_deref()
.unwrap_or("off");
let invite = matches!(config, "invite");
let always = matches!(config, "all" | "invite");
if always || (invite && matches!(preset, PrivateChat | TrustedPrivateChat)) {
let algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
let content = RoomEncryptionEventContent::new(algorithm);
services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &content),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
}
}
// 7. Events implied by name and topic
if let Some(name) = &body.name {
services
@@ -413,6 +447,12 @@ async fn create_create_event(
))))
})?;
if !services.config.federate_created_rooms {
if !services.config.allow_federation || !content.contains_key("m.federate") {
content.insert("m.federate".into(), json!(false).try_into()?);
}
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
@@ -428,6 +468,10 @@ async fn create_create_event(
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
if !services.config.federate_created_rooms {
content.insert("m.federate".into(), json!(false).try_into()?);
}
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
@@ -535,6 +579,12 @@ async fn create_create_event_legacy(
},
}
if !services.config.federate_created_rooms {
if !services.config.allow_federation || !content.contains_key("m.federate") {
content.insert("m.federate".into(), json!(false).try_into()?);
}
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
@@ -556,6 +606,10 @@ async fn create_create_event_legacy(
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
if !services.config.federate_created_rooms {
content.insert("m.federate".into(), json!(false).try_into()?);
}
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
@@ -658,8 +712,8 @@ async fn room_alias_check(
// check if room alias is forbidden
if services
.globals
.forbidden_alias_names()
.config
.forbidden_alias_names
.is_match(room_alias_name)
{
return Err!(Request(Unknown("Room alias name is forbidden.")));
@@ -705,8 +759,8 @@ async fn room_alias_check(
async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
if services
.globals
.forbidden_alias_names()
.config
.forbidden_alias_names
.is_match(custom_room_id)
{
return Err!(Request(Unknown("Custom room ID is forbidden.")));
@@ -778,7 +832,7 @@ async fn can_create_room_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services.globals.allow_room_creation()
if !services.config.allow_room_creation
&& body.appservice_info.is_none()
&& !services.users.is_admin(body.sender_user()).await
{
+53 -36
View File
@@ -1,15 +1,20 @@
use axum::extract::State;
use futures::{FutureExt, TryStreamExt, future::try_join4};
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join5};
use ruma::{
api::client::room::initial_sync::v3::{PaginationChunk, Request, Response},
events::AnyRawAccountDataEvent,
};
use tuwunel_core::{
Err, Event, Result, at,
utils::{BoolExt, stream::TryTools},
Err, Event, Result, at, extract_variant,
matrix::PduCount,
utils::stream::{ReadyExt, TryTools},
};
use crate::Ruma;
const LIMIT_MAX: usize = 100;
const LIMIT_MAX: usize = 50;
/// GET `/_matrix/client/v3/rooms/{roomId}/initialSync`
pub(crate) async fn room_initial_sync_route(
State(services): State<crate::State>,
body: Ruma<Request>,
@@ -24,13 +29,15 @@ pub(crate) async fn room_initial_sync_route(
return Err!(Request(Forbidden("No room preview available.")));
}
let next_batch = services.globals.current_count();
let visibility = services.directory.visibility(room_id).map(Ok);
let membership = services
.state_cache
.user_membership(body.sender_user(), room_id)
.map(Ok);
let visibility = services.directory.visibility(room_id).map(Ok);
let state = services
.state_accessor
.room_state_full_pdus(room_id)
@@ -40,42 +47,52 @@ pub(crate) async fn room_initial_sync_route(
let limit = LIMIT_MAX;
let events = services
.timeline
.pdus_rev(None, room_id, None)
.pdus_rev(None, room_id, Some(PduCount::Normal(next_batch).saturating_add(1)))
.try_take(limit)
.try_collect::<Vec<_>>();
.try_collect()
.map_ok(|mut vec: Vec<_>| {
vec.reverse();
vec
});
let (membership, visibility, state, events) =
try_join4(membership, visibility, state, events)
let account_data = services
.account_data
.changes_since(Some(room_id), body.sender_user(), 0, Some(next_batch))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect::<Vec<_>>()
.map(Ok);
let (membership, visibility, state, events, account_data) =
try_join5(membership, visibility, state, events, account_data)
.boxed()
.await?;
let messages = PaginationChunk {
start: events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
end: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
};
Ok(Response {
room_id: room_id.to_owned(),
account_data: None,
state: state.into(),
messages: messages.chunk.is_empty().or_some(messages),
visibility: visibility.into(),
membership,
visibility: visibility.into(),
account_data: Some(account_data),
state: state.into(),
messages: PaginationChunk {
start: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
end: events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
}
.into(),
})
}
+5 -3
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{
FutureExt, StreamExt,
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join3},
stream::FuturesUnordered,
};
@@ -55,7 +55,7 @@ pub(crate) async fn get_room_summary(
) -> Result<get_summary::v1::Response> {
let (room_id, servers) = services
.alias
.resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone()))
.maybe_resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone()))
.await?;
if services.metadata.is_banned(&room_id).await {
@@ -142,7 +142,9 @@ async fn local_room_summary_response(
let avatar_url = services
.state_accessor
.get_avatar(room_id)
.map(|res| res.into_option().unwrap_or_default().url);
.map_ok(|content| content.url)
.ok()
.map(Option::flatten);
let room_version = services.state.get_room_version(room_id).ok();
+18 -13
View File
@@ -22,20 +22,9 @@ pub(super) async fn handle_login(
_body: &Ruma<Request>,
info: &Token,
) -> Result<OwnedUserId> {
let config = &services.config.jwt;
if !config.enable {
return Err!(Request(Unknown("JWT login is not enabled.")));
}
let claim = validate(config, &info.token)?;
let local = claim.sub.to_lowercase();
let server = &services.server.name;
let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
})?;
let user_id = validate_user(services, &info.token)?;
if !services.users.exists(&user_id).await {
let config = &services.config.jwt;
if !config.register_user {
return Err!(Request(NotFound("User {user_id} is not registered on this server.")));
}
@@ -49,6 +38,22 @@ pub(super) async fn handle_login(
Ok(user_id)
}
pub(crate) fn validate_user(services: &Services, token: &str) -> Result<OwnedUserId> {
let config = &services.config.jwt;
if !config.enable {
return Err!(Request(Unauthorized("JWT login is not enabled.")));
}
let claim = validate(config, token)?;
let local = claim.sub.to_lowercase();
let server = &services.server.name;
let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
})?;
Ok(user_id)
}
fn validate(config: &JwtConfig, token: &str) -> Result<Claim> {
let verifier = init_verifier(config)?;
let validator = init_validator(config)?;
+2 -2
View File
@@ -1,5 +1,5 @@
mod appservice;
mod jwt;
pub(crate) mod jwt;
mod ldap;
mod logout;
mod password;
@@ -116,7 +116,7 @@ pub(crate) async fn login_route(
.create_device(
&user_id,
&device_id,
(&access_token, expires_in),
(Some(&access_token), expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.clone(),
Some(client.to_string()),
+1 -1
View File
@@ -44,7 +44,7 @@ pub(crate) async fn get_hierarchy_route(
.as_ref()
.and_then(|s| PaginationToken::from_str(s).ok());
// Should prevent unexpeded behaviour in (bad) clients
// Should prevent unexpected behaviour in (bad) clients
if let Some(ref token) = key {
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
return Err!(Request(InvalidParam(
+2 -1
View File
@@ -195,6 +195,7 @@ async fn send_state_event_for_key_helper(
room_id,
&state_lock,
)
.boxed()
.await?;
Ok(event_id)
@@ -328,7 +329,7 @@ async fn allowed_to_send_state_event(
for alias in aliases {
let (alias_room_id, _servers) = services
.alias
.resolve_alias(&alias, None)
.resolve_alias(&alias)
.await
.map_err(|e| {
err!(Request(BadAlias("Failed resolving alias \"{alias}\": {e}")))
+6 -12
View File
@@ -1,13 +1,8 @@
mod v3;
mod v5;
use futures::{StreamExt, pin_mut};
use ruma::{
RoomId, UserId,
events::TimelineEventType::{
self, Beacon, CallInvite, PollStart, RoomEncrypted, RoomMessage, Sticker,
},
};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{RoomId, UserId};
use tuwunel_core::{
Error, PduCount, Result,
matrix::pdu::PduEvent,
@@ -17,9 +12,6 @@
pub(crate) use self::{v3::sync_events_route, v5::sync_events_v5_route};
pub(crate) const DEFAULT_BUMP_TYPES: &[TimelineEventType; 6] =
&[CallInvite, PollStart, Beacon, RoomEncrypted, RoomMessage, Sticker];
async fn load_timeline(
services: &Services,
sender_user: &UserId,
@@ -50,10 +42,12 @@ async fn load_timeline(
.by_ref()
.take(limit)
.collect()
.map(|mut pdus: Vec<_>| {
pdus.reverse();
pdus
})
.await;
let timeline_pdus: Vec<_> = timeline_pdus.into_iter().rev().collect();
// They /sync response doesn't always return all messages, so we say the output
// is limited unless there are events in non_timeline_pdus
let limited = non_timeline_pdus.next().await.is_some();
+400 -311
View File
File diff suppressed because it is too large Load Diff
+171 -980
View File
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
mod account_data;
mod e2ee;
mod receipts;
mod to_device;
mod typing;
use std::fmt::Debug;
use futures::{
FutureExt,
future::{OptionFuture, join5},
};
use ruma::{
RoomId,
api::client::sync::sync_events::v5::{ListId, request::ExtensionRoomConfig, response},
};
use tuwunel_core::{Result, apply, at, extract_variant, utils::BoolExt};
use tuwunel_service::sync::Connection;
use super::{SyncInfo, Window, share_encrypted_room};
#[tracing::instrument(
name = "extensions",
level = "debug",
skip_all,
fields(
next_batch = conn.next_batch,
window = window.len(),
rooms = conn.rooms.len(),
subs = conn.subscriptions.len(),
)
)]
pub(super) async fn handle(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<response::Extensions> {
let SyncInfo { .. } = sync_info;
let account_data: OptionFuture<_> = conn
.extensions
.account_data
.enabled
.unwrap_or(false)
.then(|| account_data::collect(sync_info, conn, window))
.into();
let receipts: OptionFuture<_> = conn
.extensions
.receipts
.enabled
.unwrap_or(false)
.then(|| receipts::collect(sync_info, conn, window))
.into();
let typing: OptionFuture<_> = conn
.extensions
.typing
.enabled
.unwrap_or(false)
.then(|| typing::collect(sync_info, conn, window))
.into();
let to_device: OptionFuture<_> = conn
.extensions
.to_device
.enabled
.unwrap_or(false)
.then(|| to_device::collect(sync_info, conn))
.into();
let e2ee: OptionFuture<_> = conn
.extensions
.e2ee
.enabled
.unwrap_or(false)
.then(|| e2ee::collect(sync_info, conn))
.into();
let (account_data, receipts, typing, to_device, e2ee) =
join5(account_data, receipts, typing, to_device, e2ee)
.map(apply!(5, |t: Option<_>| t.unwrap_or(Ok(Default::default()))))
.await;
Ok(response::Extensions {
account_data: account_data?,
receipts: receipts?,
typing: typing?,
to_device: to_device?,
e2ee: e2ee?,
})
}
#[tracing::instrument(
name = "selector",
level = "trace",
skip_all,
fields(?implicit, ?explicit),
)]
fn selector<'a, ListIter, SubsIter>(
SyncInfo { .. }: SyncInfo<'a>,
conn: &'a Connection,
window: &'a Window,
implicit: Option<ListIter>,
explicit: Option<SubsIter>,
) -> impl Iterator<Item = &'a RoomId> + Send + Sync + 'a
where
ListIter: Iterator<Item = &'a ListId> + Clone + Debug + Send + Sync + 'a,
SubsIter: Iterator<Item = &'a ExtensionRoomConfig> + Clone + Debug + Send + Sync + 'a,
{
let has_all_subscribed = explicit
.clone()
.into_iter()
.flatten()
.any(|erc| matches!(erc, ExtensionRoomConfig::AllSubscribed));
let all_subscribed = has_all_subscribed
.then(|| conn.subscriptions.keys())
.into_iter()
.flatten()
.map(AsRef::as_ref);
let rooms_explicit = has_all_subscribed
.is_false()
.then(move || {
explicit
.into_iter()
.flatten()
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
.map(AsRef::as_ref)
})
.into_iter()
.flatten();
let rooms_selected = window
.iter()
.filter(move |(_, room)| {
implicit.as_ref().is_none_or(|lists| {
lists
.clone()
.any(|list| room.lists.contains(list))
})
})
.map(at!(0))
.map(AsRef::as_ref);
all_subscribed
.chain(rooms_explicit)
.chain(rooms_selected)
}
@@ -0,0 +1,60 @@
use futures::{StreamExt, future::join};
use ruma::{api::client::sync::sync_events::v5::response, events::AnyRawAccountDataEvent};
use tuwunel_core::{
Result, extract_variant,
utils::{IterStream, ReadyExt, stream::BroadbandExt},
};
use tuwunel_service::sync::Room;
use super::{Connection, SyncInfo, Window, selector};
#[tracing::instrument(name = "account_data", level = "trace", skip_all)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<response::AccountData> {
let SyncInfo { services, sender_user, .. } = sync_info;
let implicit = conn
.extensions
.account_data
.lists
.as_deref()
.map(<[_]>::iter);
let explicit = conn
.extensions
.account_data
.rooms
.as_deref()
.map(<[_]>::iter);
let rooms = selector(sync_info, conn, window, implicit, explicit)
.stream()
.broad_filter_map(async |room_id| {
let &Room { roomsince, .. } = conn.rooms.get(room_id)?;
let changes: Vec<_> = services
.account_data
.changes_since(Some(room_id), sender_user, roomsince, Some(conn.next_batch))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await;
changes
.is_empty()
.eq(&false)
.then(move || (room_id.to_owned(), changes))
})
.collect();
let global = services
.account_data
.changes_since(None, sender_user, conn.globalsince, Some(conn.next_batch))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Global))
.collect();
let (global, rooms) = join(global, rooms).await;
Ok(response::AccountData { global, rooms })
}
+218
View File
@@ -0,0 +1,218 @@
use std::collections::HashSet;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join, join3},
stream::once,
};
use ruma::{
OwnedUserId, RoomId,
api::client::sync::sync_events::{DeviceLists, v5::response},
events::{
StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use tuwunel_core::{
Result, error,
matrix::{Event, pdu::PduCount},
pair_of,
utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt, future::OptionStream,
stream::BroadbandExt,
},
};
use tuwunel_service::sync::Connection;
use super::{SyncInfo, share_encrypted_room};
#[tracing::instrument(name = "e2ee", level = "trace", skip_all)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
) -> Result<response::E2EE> {
let SyncInfo { services, sender_user, sender_device, .. } = sync_info;
let keys_changed = services
.users
.keys_changed(sender_user, conn.globalsince, Some(conn.next_batch))
.map(ToOwned::to_owned)
.collect::<HashSet<_>>()
.map(|changed| (changed, HashSet::new()));
let (changed, left) = (HashSet::new(), HashSet::new());
let (changed, left) = services
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
.broad_filter_map(async |room_id| collect_room(sync_info, conn, &room_id).await.ok())
.chain(once(keys_changed))
.ready_fold((changed, left), |(mut changed, mut left), room| {
changed.extend(room.0);
left.extend(room.1);
(changed, left)
})
.await;
let left = left
.into_iter()
.stream()
.filter_map(async |user_id| {
share_encrypted_room(services, sender_user, &user_id, None)
.await
.is_false()
.then_some(user_id)
})
.collect();
let device_one_time_keys_count = services
.users
.last_one_time_keys_update(sender_user)
.then(|since| -> OptionFuture<_> {
since
.gt(&conn.globalsince)
.then(|| {
services
.users
.count_one_time_keys(sender_user, sender_device)
})
.into()
})
.map(Option::unwrap_or_default);
let (left, device_one_time_keys_count) = join(left, device_one_time_keys_count)
.boxed()
.await;
Ok(response::E2EE {
device_one_time_keys_count,
device_unused_fallback_key_types: None,
device_lists: DeviceLists {
changed: changed.into_iter().collect(),
left,
},
})
}
#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
async fn collect_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
conn: &Connection,
room_id: &RoomId,
) -> Result<pair_of!(HashSet<OwnedUserId>)> {
let current_shortstatehash = services
.state
.get_room_shortstatehash(room_id)
.inspect_err(|e| error!("Room {room_id} has no state: {e}"));
let room_keys_changed = services
.users
.room_keys_changed(room_id, conn.globalsince, Some(conn.next_batch))
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<HashSet<_>>();
let (current_shortstatehash, device_list_changed) =
join(current_shortstatehash, room_keys_changed)
.boxed()
.await;
let lists = (device_list_changed, HashSet::new());
let Ok(current_shortstatehash) = current_shortstatehash else {
return Ok(lists);
};
if current_shortstatehash <= conn.globalsince {
return Ok(lists);
}
let Ok(since_shortstatehash) = services
.timeline
.prev_shortstatehash(room_id, PduCount::Normal(conn.globalsince).saturating_add(1))
.await
else {
return Ok(lists);
};
if since_shortstatehash == current_shortstatehash {
return Ok(lists);
}
let encrypted_room = services
.state_accessor
.state_get(current_shortstatehash, &StateEventType::RoomEncryption, "")
.is_ok();
let since_encryption = services
.state_accessor
.state_get(since_shortstatehash, &StateEventType::RoomEncryption, "")
.is_ok();
let sender_joined_count = services
.state_cache
.get_joined_count(room_id, sender_user);
let (encrypted_room, since_encryption, sender_joined_count) =
join3(encrypted_room, since_encryption, sender_joined_count).await;
if !encrypted_room {
return Ok(lists);
}
let encrypted_since_last_sync = !since_encryption;
let joined_since_last_sync = sender_joined_count.is_ok_and(|count| count > conn.globalsince);
let joined_members_burst: OptionFuture<_> = (joined_since_last_sync
|| encrypted_since_last_sync)
.then(|| {
services
.state_cache
.room_members(room_id)
.ready_filter(|&user_id| user_id != sender_user)
.map(ToOwned::to_owned)
.map(|user_id| (MembershipState::Join, user_id))
.into_future()
})
.into();
services
.state_accessor
.state_added((since_shortstatehash, current_shortstatehash))
.broad_filter_map(async |(_shortstatekey, shorteventid)| {
services
.timeline
.get_pdu_from_shorteventid(shorteventid)
.ok()
.await
})
.ready_filter(|event| *event.kind() == TimelineEventType::RoomMember)
.ready_filter(|event| {
event
.state_key()
.is_some_and(|state_key| state_key != sender_user)
})
.ready_filter_map(|event| {
let content: RoomMemberEventContent = event.get_content().ok()?;
let user_id: OwnedUserId = event.state_key()?.parse().ok()?;
Some((content.membership, user_id))
})
.chain(joined_members_burst.stream())
.fold(lists, async |(mut changed, mut left), (membership, user_id)| {
use MembershipState::*;
let should_add = async |user_id| {
!share_encrypted_room(services, sender_user, user_id, Some(room_id)).await
};
match membership {
| Join if should_add(&user_id).await => changed.insert(user_id),
| Leave => left.insert(user_id),
| _ => false,
};
(changed, left)
})
.map(Ok)
.boxed()
.await
}
@@ -0,0 +1,93 @@
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomId, RoomId,
api::client::sync::sync_events::v5::response,
events::{AnySyncEphemeralRoomEvent, receipt::SyncReceiptEvent},
serde::Raw,
};
use tuwunel_core::{
Result,
utils::{BoolExt, IterStream, stream::BroadbandExt},
};
use tuwunel_service::{rooms::read_receipt::pack_receipts, sync::Room};
use super::{Connection, SyncInfo, Window, selector};
#[tracing::instrument(name = "receipts", level = "trace", skip_all)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<response::Receipts> {
let SyncInfo { .. } = sync_info;
let implicit = conn
.extensions
.receipts
.lists
.as_deref()
.map(<[_]>::iter);
let explicit = conn
.extensions
.receipts
.rooms
.as_deref()
.map(<[_]>::iter);
let rooms = selector(sync_info, conn, window, implicit, explicit)
.stream()
.broad_filter_map(|room_id| collect_room(sync_info, conn, window, room_id))
.collect()
.await;
Ok(response::Receipts { rooms })
}
#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
async fn collect_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
conn: &Connection,
_window: &Window,
room_id: &RoomId,
) -> Option<(OwnedRoomId, Raw<SyncReceiptEvent>)> {
let &Room { roomsince, .. } = conn.rooms.get(room_id)?;
let private_receipt = services
.read_receipt
.last_privateread_update(sender_user, room_id)
.then(async |last_private_update| {
if last_private_update <= roomsince || last_private_update > conn.next_batch {
return None;
}
services
.read_receipt
.private_read_get(room_id, sender_user)
.map(Some)
.await
})
.map(Option::into_iter)
.map(Iterator::flatten)
.map(IterStream::stream)
.flatten_stream();
let receipts: Vec<Raw<AnySyncEphemeralRoomEvent>> = services
.read_receipt
.readreceipts_since(room_id, roomsince, Some(conn.next_batch))
.filter_map(async |(read_user, _ts, v)| {
services
.users
.user_is_ignored(read_user, sender_user)
.await
.or_some(v)
})
.chain(private_receipt)
.collect()
.boxed()
.await;
receipts
.is_empty()
.is_false()
.then(|| (room_id.to_owned(), pack_receipts(receipts.into_iter())))
}
@@ -0,0 +1,33 @@
use futures::StreamExt;
use ruma::api::client::sync::sync_events::v5::response;
use tuwunel_core::{self, Result, at};
use super::{Connection, SyncInfo};
#[tracing::instrument(name = "to_device", level = "trace", skip_all, ret)]
pub(super) async fn collect(
SyncInfo { services, sender_user, sender_device, .. }: SyncInfo<'_>,
conn: &Connection,
) -> Result<Option<response::ToDevice>> {
services
.users
.remove_to_device_events(sender_user, sender_device, conn.globalsince)
.await;
let events: Vec<_> = services
.users
.get_to_device_events(sender_user, sender_device, None, Some(conn.next_batch))
.map(at!(1))
.collect()
.await;
let to_device = events
.is_empty()
.eq(&false)
.then(|| response::ToDevice {
next_batch: conn.next_batch.to_string().into(),
events,
});
Ok(to_device)
}
@@ -0,0 +1,63 @@
use std::collections::BTreeMap;
use futures::{FutureExt, StreamExt, TryFutureExt};
use ruma::{
api::client::sync::sync_events::v5::response,
events::typing::{SyncTypingEvent, TypingEventContent},
serde::Raw,
};
use tuwunel_core::{
Result, debug_error,
utils::{IterStream, ReadyExt},
};
use super::{Connection, SyncInfo, Window, selector};
#[tracing::instrument(name = "typing", level = "trace", skip_all, ret)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<response::Typing> {
use response::Typing;
let SyncInfo { services, sender_user, .. } = sync_info;
let implicit = conn
.extensions
.typing
.lists
.as_deref()
.map(<[_]>::iter);
let explicit = conn
.extensions
.typing
.rooms
.as_deref()
.map(<[_]>::iter);
selector(sync_info, conn, window, implicit, explicit)
.stream()
.filter_map(async |room_id| {
services
.typing
.typing_users_for_user(room_id, sender_user)
.inspect_err(|e| debug_error!(%room_id, "Failed to get typing events: {e}"))
.await
.ok()
.filter(|users| !users.is_empty())
.map(|users| (room_id, users))
})
.ready_filter_map(|(room_id, users)| {
let content = TypingEventContent::new(users);
let event = SyncTypingEvent { content };
let event = Raw::new(&event);
Some((room_id.to_owned(), event.ok()?))
})
.collect::<BTreeMap<_, _>>()
.map(|rooms| Typing { rooms })
.map(Ok)
.await
}
+153
View File
@@ -0,0 +1,153 @@
use futures::{StreamExt, future::OptionFuture, pin_mut};
use ruma::{
RoomId, api::client::sync::sync_events::v5::request::ListFilters, directory::RoomTypeFilter,
events::room::member::MembershipState,
};
use tuwunel_core::{
is_equal_to, is_true,
utils::{
BoolExt, FutureBoolExt, IterStream, ReadyExt,
future::{self, OptionExt, ReadyEqExt},
},
};
use super::SyncInfo;
#[tracing::instrument(name = "filter", level = "trace", skip_all)]
pub(super) async fn filter_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
filter: &ListFilters,
room_id: &RoomId,
membership: Option<&MembershipState>,
) -> bool {
let match_invite: OptionFuture<_> = filter
.is_invite
.map(async |is_invite| match (membership, is_invite) {
| (Some(MembershipState::Invite), true) => true,
| (Some(MembershipState::Invite), false) => false,
| (Some(_), true) => false,
| (Some(_), false) => true,
| _ =>
services
.state_cache
.is_invited(sender_user, room_id)
.await == is_invite,
})
.into();
let match_direct: OptionFuture<_> = filter
.is_dm
.map(async |is_dm| {
services
.account_data
.is_direct(sender_user, room_id)
.await == is_dm
})
.into();
let match_direct_member: OptionFuture<_> = filter
.is_dm
.map(async |is_dm| {
services
.state_accessor
.is_direct(room_id, sender_user)
.await == is_dm
})
.into();
let match_encrypted: OptionFuture<_> = filter
.is_encrypted
.map(async |is_encrypted| {
services
.state_accessor
.is_encrypted_room(room_id)
.await == is_encrypted
})
.into();
let match_space_child: OptionFuture<_> = filter
.spaces
.is_empty()
.is_false()
.then(async || {
filter
.spaces
.iter()
.stream()
.flat_map(|room_id| services.spaces.get_space_children(room_id))
.ready_any(is_equal_to!(room_id))
.await
})
.into();
let fetch_tags = !filter.tags.is_empty() || !filter.not_tags.is_empty();
let match_room_tag: OptionFuture<_> = fetch_tags
.then(async || {
if let Some(tags) = services
.account_data
.get_room_tags(sender_user, room_id)
.await
.ok()
.filter(|tags| !tags.is_empty())
{
tags.keys().any(|tag| {
(filter.not_tags.is_empty() || !filter.not_tags.contains(tag))
|| (!filter.tags.is_empty() && filter.tags.contains(tag))
})
} else {
filter.tags.is_empty()
}
})
.into();
let fetch_room_type = !filter.room_types.is_empty() || !filter.not_room_types.is_empty();
let match_room_type: OptionFuture<_> = fetch_room_type
.then(async || {
let room_type = services
.state_accessor
.get_room_type(room_id)
.await
.ok();
let room_type = RoomTypeFilter::from(room_type);
(filter.not_room_types.is_empty() || !filter.not_room_types.contains(&room_type))
&& (filter.room_types.is_empty() || filter.room_types.contains(&room_type))
})
.into();
future::and7(
match_invite.is_none_or(is_true!()),
match_encrypted.is_none_or(is_true!()),
match_direct.is_none_or(is_true!()),
match_direct_member.is_none_or(is_true!()),
match_space_child.is_none_or(is_true!()),
match_room_type.is_none_or(is_true!()),
match_room_tag.is_none_or(is_true!()),
)
.await
}
#[tracing::instrument(name = "filter_meta", level = "trace", skip_all)]
pub(super) async fn filter_room_meta(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
room_id: &RoomId,
) -> bool {
let not_exists = services.metadata.exists(room_id).eq(&false);
let is_disabled = services.metadata.is_disabled(room_id);
let is_banned = services.metadata.is_banned(room_id);
let not_visible = services
.state_accessor
.user_can_see_state_events(sender_user, room_id)
.eq(&false);
pin_mut!(not_visible, not_exists, is_disabled, is_banned);
not_visible
.or(not_exists)
.or(is_disabled)
.or(is_banned)
.await
.eq(&false)
}
+447
View File
@@ -0,0 +1,447 @@
use std::{
cmp::Ordering,
collections::{BTreeMap, HashSet},
};
use futures::{
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join3, join4},
};
use ruma::{
JsOption, MxcUri, OwnedMxcUri, OwnedRoomId, RoomId, UserId,
api::client::sync::sync_events::{
UnreadNotificationsCount,
v5::{DisplayName, response, response::Heroes},
},
events::{
StateEventType,
TimelineEventType::{
self, Beacon, CallInvite, PollStart, RoomEncrypted, RoomMessage, Sticker,
},
room::member::MembershipState,
},
};
use tuwunel_core::{
Result, at, err, error, is_equal_to,
matrix::{Event, StateKey, pdu::PduCount},
ref_at,
utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt, math::usize_from_ruma, result::FlatOk,
stream::BroadbandExt,
},
};
use tuwunel_service::{Services, sync::Room};
use super::{super::load_timeline, Connection, SyncInfo, Window, WindowRoom};
use crate::client::ignored_filter;
static DEFAULT_BUMP_TYPES: [TimelineEventType; 6] =
[CallInvite, PollStart, Beacon, RoomEncrypted, RoomMessage, Sticker];
#[tracing::instrument(
name = "rooms",
level = "debug",
skip_all,
fields(
next_batch = conn.next_batch,
window = window.len(),
)
)]
pub(super) async fn handle(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<BTreeMap<OwnedRoomId, response::Room>> {
window
.iter()
.stream()
.broad_filter_map(async |(room_id, room)| {
handle_room(sync_info, conn, room)
.map_ok(move |room| (room_id.clone(), room))
.inspect_err(|e| error!(?room_id, "sync handler: {e:?}"))
.await
.ok()
})
.collect()
.map(Ok)
.await
}
#[tracing::instrument(
name = "room",
level = "debug",
skip_all,
fields(room_id, roomsince)
)]
#[allow(clippy::too_many_arguments)]
async fn handle_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
conn: &Connection,
WindowRoom {
lists, membership, room_id, last_count, ..
}: &WindowRoom,
) -> Result<response::Room> {
debug_assert!(
DEFAULT_BUMP_TYPES.is_sorted(),
"DEFAULT_BUMP_TYPES must be sorted for binary search"
);
let &Room { roomsince, .. } = conn
.rooms
.get(room_id)
.ok_or_else(|| err!("Missing connection state for {room_id}"))?;
debug_assert!(
*last_count > roomsince || *last_count == 0 || roomsince == 0,
"Stale room shouldn't be in the window"
);
if *membership == Some(MembershipState::Leave) {
return Ok(response::Room {
initial: roomsince.eq(&0).then_some(true),
lists: lists.clone(),
membership: membership.clone(),
prev_batch: Some(conn.next_batch.to_string().into()),
limited: true,
required_state: vec![
services
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, sender_user.as_str())
.map_ok(Event::into_format)
.await?,
],
..Default::default()
});
}
let is_invite = *membership == Some(MembershipState::Invite);
let default_details = (0_usize, HashSet::new());
let (timeline_limit, required_state) = lists
.iter()
.filter_map(|list_id| conn.lists.get(list_id))
.map(|list| &list.room_details)
.chain(conn.subscriptions.get(room_id).into_iter())
.fold(default_details, |(mut timeline_limit, mut required_state), config| {
let limit = usize_from_ruma(config.timeline_limit);
timeline_limit = timeline_limit.max(limit);
required_state.extend(config.required_state.clone());
(timeline_limit, required_state)
});
let timeline: OptionFuture<_> = is_invite
.is_false()
.then(|| {
load_timeline(
services,
sender_user,
room_id,
PduCount::Normal(roomsince),
Some(PduCount::from(conn.next_batch)),
timeline_limit,
)
})
.into();
let (timeline_pdus, limited, _lastcount) = timeline
.await
.flat_ok()
.unwrap_or_else(|| (Vec::new(), true, PduCount::default()));
let required_state = required_state
.into_iter()
.filter(|_| !timeline_pdus.is_empty())
.collect::<Vec<_>>();
let prev_batch = timeline_pdus
.first()
.map(at!(0))
.map(PduCount::into_unsigned)
.as_ref()
.map(ToString::to_string);
let bump_stamp = timeline_pdus
.iter()
.filter(|(_, pdu)| {
if *pdu.event_type() == TimelineEventType::RoomMember {
return pdu
.state_key()
.is_some_and(is_equal_to!(sender_user.as_str()));
}
DEFAULT_BUMP_TYPES
.binary_search(pdu.event_type())
.is_ok()
})
.filter(|(_, pdu)| !pdu.is_redacted())
.map(at!(0))
.map(PduCount::into_signed)
.max()
.map(TryInto::try_into)
.flat_ok();
let num_live: OptionFuture<_> = roomsince
.ne(&0)
.and_is(limited || timeline_pdus.len() >= timeline_limit)
.then(|| {
services
.timeline
.pdus(None, room_id, Some(roomsince.into()))
.count()
.map(TryInto::try_into)
.map(Result::ok)
})
.into();
let lazy = required_state
.iter()
.any(is_equal_to!(&(StateEventType::RoomMember, "$LAZY".into())));
let mut timeline_senders: Vec<_> = timeline_pdus
.iter()
.filter(|_| lazy)
.map(ref_at!(1))
.map(Event::sender)
.collect();
timeline_senders.sort();
timeline_senders.dedup();
let timeline_senders = timeline_senders
.iter()
.map(|sender| (StateEventType::RoomMember, StateKey::from_str(sender.as_str())))
.stream();
let wildcard_state = required_state
.iter()
.filter(|(_, state_key)| state_key == "*")
.map(|(event_type, _)| {
services
.state_accessor
.room_state_keys(room_id, event_type)
.map_ok(|state_key| (event_type.clone(), state_key))
.ready_filter_map(Result::ok)
})
.stream()
.flatten();
let required_state = required_state
.iter()
.cloned()
.stream()
.chain(wildcard_state)
.chain(timeline_senders)
.broad_filter_map(async |state| {
let state_key: StateKey = match state.1.as_str() {
| "$LAZY" | "*" => return None,
| "$ME" => sender_user.as_str().into(),
| _ => state.1.clone(),
};
services
.state_accessor
.room_state_get(room_id, &state.0, &state_key)
.map_ok(Event::into_format)
.ok()
.await
})
.collect();
// TODO: figure out a timestamp we can use for remote invites
let invite_state: OptionFuture<_> = is_invite
.then(|| {
services
.state_cache
.invite_state(sender_user, room_id)
.ok()
})
.into();
let room_name = services
.state_accessor
.get_name(room_id)
.map_ok(Into::into)
.map(Result::ok);
let room_avatar = services
.state_accessor
.get_avatar(room_id)
.map_ok(|content| content.url)
.ok()
.map(Option::flatten);
let highlight_count = services
.pusher
.highlight_count(sender_user, room_id)
.map(TryInto::try_into)
.map(Result::ok);
let notification_count = services
.pusher
.notification_count(sender_user, room_id)
.map(TryInto::try_into)
.map(Result::ok);
let joined_count = services
.state_cache
.room_joined_count(room_id)
.map_ok(TryInto::try_into)
.map_ok(Result::ok)
.map(FlatOk::flat_ok);
let invited_count = services
.state_cache
.room_invited_count(room_id)
.map_ok(TryInto::try_into)
.map_ok(Result::ok)
.map(FlatOk::flat_ok);
let is_dm = services
.state_accessor
.is_direct(room_id, sender_user)
.map(|is_dm| is_dm.then_some(is_dm));
let last_read_count = services
.pusher
.last_notification_read(sender_user, room_id);
let timeline = timeline_pdus
.iter()
.stream()
.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
.map(at!(1))
.map(Event::into_format)
.collect();
let meta = join3(room_name, room_avatar, is_dm);
let events = join4(timeline, num_live, required_state, invite_state);
let member_counts = join(joined_count, invited_count);
let notification_counts = join3(highlight_count, notification_count, last_read_count);
let (
(room_name, room_avatar, is_dm),
(timeline, num_live, required_state, invite_state),
(joined_count, invited_count),
(highlight_count, notification_count, _last_notification_read),
) = join4(meta, events, member_counts, notification_counts)
.boxed()
.await;
let (heroes, hero_name, heroes_avatar) = calculate_heroes(
services,
sender_user,
room_id,
room_name.as_ref(),
room_avatar.as_deref(),
)
.await?;
Ok(response::Room {
initial: roomsince.eq(&0).then_some(true),
lists: lists.clone(),
membership: membership.clone(),
name: room_name.or(hero_name),
avatar: JsOption::from_option(room_avatar.or(heroes_avatar)),
is_dm,
heroes,
required_state,
invite_state: invite_state.flatten(),
prev_batch: prev_batch.as_deref().map(Into::into),
num_live: num_live.flatten(),
limited,
timeline,
bump_stamp,
joined_count,
invited_count,
unread_notifications: UnreadNotificationsCount { highlight_count, notification_count },
})
}
#[tracing::instrument(name = "heroes", level = "trace", skip_all)]
#[allow(clippy::type_complexity)]
async fn calculate_heroes(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
room_name: Option<&DisplayName>,
room_avatar: Option<&MxcUri>,
) -> Result<(Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>)> {
const MAX_HEROES: usize = 5;
let heroes: Heroes = services
.state_cache
.room_members(room_id)
.ready_filter(|&member| member != sender_user)
.ready_filter_map(|member| room_name.is_none().then_some(member))
.map(ToOwned::to_owned)
.broadn_filter_map(MAX_HEROES, async |user_id| {
let content = services
.state_accessor
.get_member(room_id, &user_id)
.await
.ok()?;
let name: OptionFuture<_> = content
.displayname
.is_none()
.then(|| services.users.displayname(&user_id).ok())
.into();
let avatar: OptionFuture<_> = content
.avatar_url
.is_none()
.then(|| services.users.avatar_url(&user_id).ok())
.into();
let (name, avatar) = join(name, avatar).await;
let hero = response::Hero {
user_id,
avatar: avatar.unwrap_or(content.avatar_url),
name: name
.unwrap_or(content.displayname)
.map(Into::into),
};
Some(hero)
})
.take(MAX_HEROES)
.collect()
.await;
let hero_name = match heroes.len().cmp(&(1_usize)) {
| Ordering::Less => None,
| Ordering::Equal => Some(
heroes[0]
.name
.clone()
.unwrap_or_else(|| heroes[0].user_id.as_str().into()),
),
| Ordering::Greater => {
let firsts = heroes[1..]
.iter()
.map(|h| {
h.name
.clone()
.unwrap_or_else(|| h.user_id.as_str().into())
})
.collect::<Vec<_>>()
.join(", ");
let last = heroes[0]
.name
.clone()
.unwrap_or_else(|| heroes[0].user_id.as_str().into());
Some(format!("{firsts} and {last}")).map(Into::into)
},
};
let heroes_avatar = (room_avatar.is_none() && room_name.is_none())
.then(|| {
heroes
.first()
.and_then(|hero| hero.avatar.clone())
})
.flatten();
Ok((Some(heroes), hero_name, heroes_avatar))
}
+264
View File
@@ -0,0 +1,264 @@
use std::cmp::Ordering;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join5},
};
use ruma::{OwnedRoomId, UInt, events::room::member::MembershipState, uint};
use tuwunel_core::{
apply, is_true,
matrix::PduCount,
trace,
utils::{
BoolExt, TryFutureExtExt,
math::usize_from_ruma,
stream::{BroadbandExt, IterStream},
},
};
use tuwunel_service::sync::Connection;
use super::{
ListIds, ResponseLists, SyncInfo, Window, WindowRoom,
filter::{filter_room, filter_room_meta},
};
#[tracing::instrument(level = "debug", skip_all)]
pub(super) async fn selector(
conn: &mut Connection,
sync_info: SyncInfo<'_>,
) -> (Window, ResponseLists) {
use MembershipState::*;
let SyncInfo { services, sender_user, .. } = sync_info;
let mut rooms = services
.state_cache
.user_memberships(sender_user, Some(&[Join, Invite, Knock]))
.map(|(membership, room_id)| (room_id.to_owned(), Some(membership)))
.broad_filter_map(|(room_id, membership)| matcher(sync_info, conn, room_id, membership))
.collect::<Vec<_>>()
.await;
rooms.sort_by(room_sort);
rooms
.iter_mut()
.enumerate()
.for_each(|(i, room)| {
room.ranked = i;
conn.rooms
.entry(room.room_id.clone())
.or_default();
});
trace!(?rooms);
let lists = response_lists(rooms.iter());
trace!(?lists);
let window = window(sync_info, conn, rooms.iter(), &lists).await;
trace!(?window);
(window, lists)
}
#[tracing::instrument(
name = "matcher",
level = "trace",
skip_all,
fields(?room_id, ?membership)
)]
async fn matcher(
sync_info: SyncInfo<'_>,
conn: &Connection,
room_id: OwnedRoomId,
membership: Option<MembershipState>,
) -> Option<WindowRoom> {
let SyncInfo { services, sender_user, .. } = sync_info;
let (matched, lists) = conn
.lists
.iter()
.stream()
.filter_map(async |(id, list)| {
let filter: OptionFuture<_> = list
.filters
.clone()
.map(async |filters| {
filter_room(sync_info, &filters, &room_id, membership.as_ref()).await
})
.into();
filter
.await
.is_none_or(is_true!())
.then(|| id.clone())
})
.collect::<ListIds>()
.map(|lists| (lists.is_empty().is_false(), lists))
.await;
let last_notification: OptionFuture<_> = matched
.then(|| {
services
.pusher
.last_notification_read(sender_user, &room_id)
.unwrap_or_default()
})
.into();
let last_privateread: OptionFuture<_> = matched
.then(|| {
services
.read_receipt
.last_privateread_update(sender_user, &room_id)
})
.into();
let last_receipt: OptionFuture<_> = matched
.then(|| {
services
.read_receipt
.last_receipt_count(&room_id, sender_user.into(), None)
.unwrap_or_default()
})
.into();
let last_account: OptionFuture<_> = matched
.then(|| {
services
.account_data
.last_count(Some(room_id.as_ref()), sender_user, Some(conn.next_batch))
.unwrap_or_default()
})
.into();
let last_timeline: OptionFuture<_> = matched
.then(|| {
services
.timeline
.last_timeline_count(None, &room_id, Some(conn.next_batch.into()))
.map_ok(PduCount::into_unsigned)
.unwrap_or_default()
})
.into();
let (last_timeline, last_notification, last_account, last_receipt, last_privateread) =
join5(last_timeline, last_notification, last_account, last_receipt, last_privateread)
.await;
Some(WindowRoom {
room_id: room_id.clone(),
membership,
lists,
ranked: 0,
last_count: [
last_timeline,
last_notification,
last_account,
last_receipt,
last_privateread,
]
.into_iter()
.map(Option::unwrap_or_default)
.filter(|count| conn.next_batch.ge(count))
.max()
.unwrap_or_default(),
})
}
#[tracing::instrument(
level = "debug",
skip_all,
fields(rooms = rooms.clone().count())
)]
async fn window<'a, Rooms>(
sync_info: SyncInfo<'_>,
conn: &Connection,
rooms: Rooms,
lists: &ResponseLists,
) -> Window
where
Rooms: Iterator<Item = &'a WindowRoom> + Clone + Send + Sync,
{
static FULL_RANGE: (UInt, UInt) = (UInt::MIN, UInt::MAX);
let SyncInfo { services, sender_user, .. } = sync_info;
let selections = lists
.keys()
.cloned()
.filter_map(|id| conn.lists.get(&id).map(|list| (id, list)))
.flat_map(|(id, list)| {
let full_range = list
.ranges
.is_empty()
.then_some(&FULL_RANGE)
.into_iter();
list.ranges
.iter()
.chain(full_range)
.map(apply!(2, usize_from_ruma))
.map(move |range| (id.clone(), range))
})
.flat_map(|(id, (start, end))| {
rooms
.clone()
.filter(move |&room| room.lists.contains(&id))
.filter(|&room| {
conn.rooms
.get(&room.room_id)
.is_some_and(|conn_room| {
conn_room.roomsince == 0 || room.last_count > conn_room.roomsince
})
})
.enumerate()
.skip_while(move |&(i, _)| i < start)
.take(end.saturating_add(1).saturating_sub(start))
.map(|(_, room)| (room.room_id.clone(), room.clone()))
})
.stream();
let subscriptions = conn
.subscriptions
.iter()
.stream()
.broad_filter_map(async |(room_id, _)| {
filter_room_meta(sync_info, room_id)
.await
.into_option()?;
Some(WindowRoom {
room_id: room_id.clone(),
lists: Default::default(),
ranked: usize::MAX,
last_count: 0,
membership: services
.state_cache
.user_membership(sender_user, room_id)
.await,
})
})
.map(|room| (room.room_id.clone(), room));
subscriptions.chain(selections).collect().await
}
fn response_lists<'a, Rooms>(rooms: Rooms) -> ResponseLists
where
Rooms: Iterator<Item = &'a WindowRoom>,
{
rooms
.flat_map(|room| room.lists.iter())
.fold(ResponseLists::default(), |mut lists, id| {
let list = lists.entry(id.clone()).or_default();
list.count = list
.count
.checked_add(uint!(1))
.expect("list count must not overflow JsInt");
lists
})
}
fn room_sort(a: &WindowRoom, b: &WindowRoom) -> Ordering { b.last_count.cmp(&a.last_count) }
+9 -7
View File
@@ -28,7 +28,7 @@ pub(crate) async fn create_typing_event_route(
match body.state {
| Typing::Yes(duration) => {
let duration = utils::clamp(
let duration = Ord::clamp(
duration
.as_millis()
.try_into()
@@ -64,12 +64,14 @@ pub(crate) async fn create_typing_event_route(
}
// ping presence
if services.config.allow_local_presence {
services
.presence
.ping_presence(&body.user_id, &ruma::presence::PresenceState::Online)
.await?;
}
services
.presence
.maybe_ping_presence(
&body.user_id,
body.sender_device.as_deref(),
&ruma::presence::PresenceState::Online,
)
.await?;
Ok(create_typing_event::v3::Response {})
}
+20 -28
View File
@@ -73,13 +73,11 @@ pub(crate) async fn delete_timezone_key_route(
services.users.set_timezone(&body.user_id, None);
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(delete_timezone_key::unstable::Response {})
}
@@ -103,13 +101,11 @@ pub(crate) async fn set_timezone_key_route(
.users
.set_timezone(&body.user_id, body.tz.clone());
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(set_timezone_key::unstable::Response {})
}
@@ -171,13 +167,11 @@ pub(crate) async fn set_profile_field_route(
);
}
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(set_profile_field::v3::Response {})
}
@@ -227,13 +221,11 @@ pub(crate) async fn delete_profile_field_route(
.set_profile_key(&body.user_id, body.field.as_str(), None);
}
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(delete_profile_field::v3::Response {})
}
+5 -3
View File
@@ -42,17 +42,19 @@ pub(crate) async fn get_supported_versions_route(
unstable_features: BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
("fi.mau.msc2659.stable".to_owned(), true), /* appservice ping https://github.com/matrix-org/matrix-spec-proposals/pull/2659) */
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("org.matrix.msc3575".to_owned(), true), /* sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1588877046) */
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
]),
};
+5 -8
View File
@@ -25,12 +25,12 @@ pub(crate) async fn turn_server_route(
return Err!(Request(NotFound("Not Found")));
}
let turn_secret = services.globals.turn_secret.clone();
let turn_secret = &services.globals.turn_secret;
let (username, password) = if !turn_secret.is_empty() {
let expiry = SecondsSinceUnixEpoch::from_system_time(
SystemTime::now()
.checked_add(Duration::from_secs(services.globals.turn_ttl()))
.checked_add(Duration::from_secs(services.config.turn_ttl))
.expect("TURN TTL should not get this high"),
)
.expect("time is valid");
@@ -53,16 +53,13 @@ pub(crate) async fn turn_server_route(
(username, password)
} else {
(
services.globals.turn_username().clone(),
services.globals.turn_password().clone(),
)
(services.config.turn_username.clone(), services.config.turn_password.clone())
};
Ok(get_turn_server_info::v3::Response {
username,
password,
uris: services.globals.turn_uris().to_vec(),
ttl: Duration::from_secs(services.globals.turn_ttl()),
uris: services.config.turn_uris.clone(),
ttl: Duration::from_secs(services.config.turn_ttl),
})
}
+39 -8
View File
@@ -1,28 +1,59 @@
use axum::{Json, extract::State, response::IntoResponse};
use ruma::api::client::discovery::{
discover_homeserver::{self, HomeserverInfo},
discover_homeserver::{self, HomeserverInfo, RtcFocusInfo},
discover_support::{self, Contact},
};
use tuwunel_core::{Err, Result};
use serde_json::Value as JsonValue;
use tuwunel_core::{Err, Result, err, error::inspect_log};
use crate::Ruma;
/// # `GET /.well-known/matrix/client`
///
/// Returns the .well-known URL if it is configured, otherwise returns 404.
/// Also includes RTC transport configuration for Element Call (MSC4143).
pub(crate) async fn well_known_client(
State(services): State<crate::State>,
_body: Ruma<discover_homeserver::Request>,
) -> Result<discover_homeserver::Response> {
let client_url = match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err!(Request(NotFound("Not found."))),
let homeserver = HomeserverInfo {
base_url: match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err!(Request(NotFound("Not found."))),
},
};
// Add RTC transport configuration if available (MSC4143 / Element Call)
// Element Call has evolved through several versions with different field
// expectations
let rtc_foci = services
.server
.config
.well_known
.rtc_transports
.iter()
.map(|transport| {
let focus_type = transport
.get("type")
.and_then(JsonValue::as_str)
.ok_or_else(|| err!("`type` is not a valid string"))?;
let transport = transport
.as_object()
.cloned()
.ok_or_else(|| err!("`rtc_transport` is not a valid object"))?;
RtcFocusInfo::new(focus_type, transport).map_err(Into::into)
})
.collect::<Result<_>>()
.map_err(|e| {
err!(Config("global.well_known.rtc_transports", "Malformed value(s): {e:?}"))
})
.inspect_err(inspect_log)?;
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url },
identity_server: None,
tile_server: None,
rtc_foci,
..discover_homeserver::Response::new(homeserver)
})
}
+1 -1
View File
@@ -1,4 +1,4 @@
#![type_length_limit = "163840"] //TODO: REDUCE ME
#![type_length_limit = "262144"] //TODO: REDUCE ME
#![allow(clippy::toplevel_ref_arg)]
pub mod client;
+9 -1
View File
@@ -16,7 +16,9 @@
use tuwunel_core::{Server, err};
use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse, state::State};
pub(super) use self::{
args::Args as Ruma, auth::auth_uiaa, response::RumaResponse, state::State,
};
use crate::{client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
@@ -45,6 +47,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::request_3pid_management_token_via_email_route)
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
.ruma_route(&client::check_registration_token_validity)
.ruma_route(&client::get_notifications_route)
.ruma_route(&client::get_capabilities_route)
.ruma_route(&client::get_pushrules_all_route)
.ruma_route(&client::get_pushrules_global_route)
@@ -143,6 +146,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
get(client::get_state_events_for_empty_key_route)
.put(client::send_state_event_for_empty_key_route),
)
.ruma_route(&client::events_route)
.ruma_route(&client::sync_events_route)
.ruma_route(&client::sync_events_v5_route)
.ruma_route(&client::get_context_route)
@@ -161,6 +165,10 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::update_device_route)
.ruma_route(&client::delete_device_route)
.ruma_route(&client::delete_devices_route)
.ruma_route(&client::put_dehydrated_device_route)
.ruma_route(&client::delete_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_events_route)
.ruma_route(&client::get_tags_route)
.ruma_route(&client::update_tag_route)
.ruma_route(&client::delete_tag_route)
+5 -11
View File
@@ -6,7 +6,7 @@
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
OwnedUserId, ServerName, UserId, api::IncomingRequest,
};
use tuwunel_core::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
use tuwunel_core::{Error, Result, debug_warn, err, trace, utils::string::EMPTY};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use super::{auth, auth::Auth, request, request::Request};
@@ -95,6 +95,7 @@ async fn from_request(
) -> Result<Self, Self::Rejection> {
let mut request = request::from(services, request).await?;
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&request.body).ok();
trace!(?request);
// while very unusual and really shouldn't be recommended, Synapse accepts POST
// requests with a completely empty body. very old clients, libraries, and some
@@ -104,13 +105,13 @@ async fn from_request(
&& request.parts.method == http::Method::POST
&& !request.parts.uri.path().contains("/media/")
{
trace!("json_body from_request: {:?}", json_body.clone());
debug_warn!(
"received a POST request with an empty body, defaulting/assuming to {{}} like \
Synapse does"
);
json_body = Some(CanonicalJsonValue::Object(CanonicalJsonObject::new()));
}
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
Ok(Self {
body: make_body::<T>(services, &mut request, json_body.as_mut(), &auth)?,
@@ -147,16 +148,9 @@ fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
.headers_mut()
.expect("mutable http headers") = request.parts.headers.clone();
let http_request = http_request
.body(body)
.expect("http request body");
let headers = http_request.headers();
let method = http_request.method();
let uri = http_request.uri();
debug!("{method:?} {uri:?} {headers:?}");
http_request
.body(body)
.expect("http request body")
}
#[allow(clippy::needless_pass_by_value)]
+10 -162
View File
@@ -1,10 +1,13 @@
mod appservice;
mod server;
mod uiaa;
use std::{fmt::Debug, time::SystemTime};
use axum::RequestPartsExt;
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
typed_header::TypedHeaderRejectionReason,
};
use futures::{
TryFutureExt,
@@ -15,7 +18,7 @@
pin_mut,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId,
api::{
AuthScheme, IncomingRequest, Metadata,
client::{
@@ -27,18 +30,14 @@
},
voip::get_turn_server_info,
},
federation::{authentication::XMatrix, openid::get_openid_userinfo},
federation::openid::get_openid_userinfo,
},
};
use tuwunel_core::{
Err, Error, Result, debug_error, err, is_less_than, utils::result::LogDebugErr, warn,
};
use tuwunel_service::{
Services,
appservice::RegistrationInfo,
server_keys::{PubKeyMap, PubKeys},
};
use tuwunel_core::{Err, Error, Result, is_less_than, utils::result::LogDebugErr};
use tuwunel_service::{Services, appservice::RegistrationInfo};
pub(crate) use self::uiaa::auth_uiaa;
use self::{appservice::auth_appservice, server::auth_server};
use super::request::Request;
enum Token {
@@ -223,154 +222,3 @@ async fn find_token(services: &Services, token: Option<&str>) -> Result<Token> {
| _ => Ok(Token::Invalid),
}
}
async fn auth_appservice(
services: &Services,
request: &Request,
info: Box<RegistrationInfo>,
) -> Result<Auth> {
let user_id_default = || {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services.globals.server_name(),
)
};
let Ok(user_id) = request
.query
.user_id
.clone()
.map_or_else(user_id_default, OwnedUserId::parse)
else {
return Err!(Request(InvalidUsername("Username is invalid.")));
};
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("User is not in namespace.")));
}
Ok(Auth {
sender_user: Some(user_id),
appservice_info: Some(*info),
..Auth::default()
})
}
async fn auth_server(
services: &Services,
request: &mut Request,
body: Option<&CanonicalJsonValue>,
) -> Result<Auth> {
type Member = (String, CanonicalJsonValue);
type Object = CanonicalJsonObject;
type Value = CanonicalJsonValue;
let x_matrix = parse_x_matrix(request).await?;
auth_server_checks(services, &x_matrix)?;
let destination = services.globals.server_name();
let origin = &x_matrix.origin;
let signature_uri = request
.parts
.uri
.path_and_query()
.expect("all requests have a path")
.to_string();
let signature: [Member; 1] =
[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
let signatures: [Member; 1] = [(origin.as_str().into(), Value::Object(signature.into()))];
let authorization: Object = if let Some(body) = body.cloned() {
let authorization: [Member; 6] = [
("content".into(), body),
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
} else {
let authorization: [Member; 5] = [
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
};
let key = services
.server_keys
.get_verify_key(origin, &x_matrix.key)
.await
.map_err(|e| {
err!(Request(Forbidden(debug_warn!("Failed to fetch signing keys: {e}"))))
})?;
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
debug_error!("Failed to verify federation request from {origin}: {e}");
if request.parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your reverse proxy gives \
tuwunel the raw uri (apache: use nocanon)"
);
}
return Err!(Request(Forbidden("Failed to verify X-Matrix signatures.")));
}
Ok(Auth {
origin: origin.to_owned().into(),
..Auth::default()
})
}
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result {
if !services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled."));
}
let destination = services.globals.server_name();
if x_matrix.destination.as_deref() != Some(destination) {
return Err!(Request(Forbidden("Invalid destination.")));
}
let origin = &x_matrix.origin;
if services
.config
.forbidden_remote_server_names
.is_match(origin.host())
{
return Err!(Request(Forbidden(debug_warn!(
"Federation requests from {origin} denied."
))));
}
Ok(())
}
async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
let TypedHeader(Authorization(x_matrix)) = request
.parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
let msg = match e.reason() {
| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
| _ => "Unknown header-related error",
};
err!(Request(Forbidden(debug_warn!("{msg}: {e}"))))
})?;
Ok(x_matrix)
}
+37
View File
@@ -0,0 +1,37 @@
use ruma::{OwnedUserId, UserId};
use tuwunel_core::{Err, Result};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use super::{Auth, Request};
pub(super) async fn auth_appservice(
services: &Services,
request: &Request,
info: Box<RegistrationInfo>,
) -> Result<Auth> {
let user_id_default = || {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services.globals.server_name(),
)
};
let Ok(user_id) = request
.query
.user_id
.as_deref()
.map_or_else(user_id_default, OwnedUserId::parse)
else {
return Err!(Request(InvalidUsername("Username is invalid.")));
};
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("User is not in namespace.")));
}
Ok(Auth {
sender_user: Some(user_id),
appservice_info: Some(*info),
..Auth::default()
})
}
+132
View File
@@ -0,0 +1,132 @@
use axum::RequestPartsExt;
use axum_extra::{TypedHeader, headers::Authorization, typed_header::TypedHeaderRejectionReason};
use ruma::{
CanonicalJsonName, CanonicalJsonObject, CanonicalJsonValue,
api::federation::authentication::XMatrix,
};
use tuwunel_core::{Err, Result, debug_error, err, warn};
use tuwunel_service::{
Services,
server_keys::{PubKeyMap, PubKeys},
};
use super::{Auth, Request};
pub(super) async fn auth_server(
services: &Services,
request: &mut Request,
body: Option<&CanonicalJsonValue>,
) -> Result<Auth> {
type Member = (CanonicalJsonName, CanonicalJsonValue);
type Object = CanonicalJsonObject;
type Value = CanonicalJsonValue;
let x_matrix = parse_x_matrix(request).await?;
auth_server_checks(services, &x_matrix)?;
let destination = services.globals.server_name();
let origin = &x_matrix.origin;
let signature_uri = request
.parts
.uri
.path_and_query()
.expect("all requests have a path")
.to_string();
let signature: [Member; 1] =
[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
let signatures: [Member; 1] = [(origin.as_str().into(), Value::Object(signature.into()))];
let authorization: Object = if let Some(body) = body.cloned() {
let authorization: [Member; 6] = [
("content".into(), body),
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
} else {
let authorization: [Member; 5] = [
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
};
let key = services
.server_keys
.get_verify_key(origin, &x_matrix.key)
.await
.map_err(|e| {
err!(Request(Forbidden(debug_warn!("Failed to fetch signing keys: {e}"))))
})?;
let keys: PubKeys = [(x_matrix.key.as_str().into(), key.key)].into();
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
debug_error!("Failed to verify federation request from {origin}: {e}");
if request.parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your reverse proxy gives \
tuwunel the raw uri (apache: use nocanon)"
);
}
return Err!(Request(Forbidden("Failed to verify X-Matrix signatures.")));
}
Ok(Auth {
origin: origin.to_owned().into(),
..Auth::default()
})
}
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result {
if !services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled."));
}
let destination = services.globals.server_name();
if x_matrix.destination.as_deref() != Some(destination) {
return Err!(Request(Forbidden("Invalid destination.")));
}
let origin = &x_matrix.origin;
if services
.config
.forbidden_remote_server_names
.is_match(origin.host())
{
return Err!(Request(Forbidden(debug_warn!(
"Federation requests from {origin} denied."
))));
}
Ok(())
}
async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
let TypedHeader(Authorization(x_matrix)) = request
.parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
let msg = match e.reason() {
| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
| _ => "Unknown header-related error",
};
err!(Request(Forbidden(debug_warn!("{msg}: {e}"))))
})?;
Ok(x_matrix)
}

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