Compare commits

...
53 Commits
Author SHA1 Message Date
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
Jason Volk a1677f05d3 Bump 1.4.1
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-02 09:43:50 +00:00
Jason Volk 1f77200299 Mark MSC4297 compliance as flake.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-02 06:29:40 +00:00
Jason Volk 888b93df91 Fix /joined_members results. (regression a3666fe672) (fixes #147)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-02 05:34:37 +00:00
75 changed files with 2725 additions and 1920 deletions
+15 -3
View File
@@ -129,17 +129,21 @@ 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": "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"},
@@ -174,15 +178,17 @@ jobs:
machines: ${{needs.init.outputs.machines}}
excludes: >
[
{"feat_set": "logging"},
{"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"},
@@ -228,6 +234,7 @@ jobs:
{"cargo_profile": "test"},
{"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 +243,7 @@ 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", "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,10 +253,13 @@ 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-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"},
]
publish:
@@ -273,6 +284,7 @@ jobs:
excludes: >
[
{"feat_set": "none"},
{"feat_set": "logging"},
{"cargo_profile": "test"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native"},
+29
View File
@@ -45,6 +45,28 @@ on:
type: string
jobs:
docs:
if: >
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: Docs
uses: ./.github/workflows/bake.yml
with:
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(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
@@ -89,6 +111,7 @@ 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"},
@@ -97,6 +120,7 @@ jobs:
{"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 +128,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 +136,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,13 +147,16 @@ 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"},
]
Generated
+286 -231
View File
File diff suppressed because it is too large Load Diff
+10 -18
View File
@@ -29,7 +29,7 @@ license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-construct/tuwunel"
rust-version = "1.88.0"
version = "1.4.0"
version = "1.4.2"
[workspace.metadata.crane]
name = "tuwunel"
@@ -163,15 +163,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
@@ -220,7 +211,7 @@ features = [
]
[workspace.dependencies.insta]
version = "1.43.1"
version = "1.43"
features = [
"json",
]
@@ -317,7 +308,7 @@ default-features = false
[workspace.dependencies.ruma]
git = "https://github.com/matrix-construct/ruma"
rev = "8bc15ba4f145e7b995d36e82e8624c3ac3ce0ef6"
rev = "5682b88cf1bcaf0f47805d614b476b242ef075d4"
features = [
"__compat",
"appservice-api-c",
@@ -430,12 +421,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",
]
@@ -536,21 +528,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.31"
[workspace.dependencies.tracing-subscriber]
version = "0.3.20"
version = "0.3"
default-features = false
features = [
"ansi",
+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
+38 -19
View File
@@ -1,38 +1,57 @@
# Tuwunel 1.4.0
# Tuwunel 1.4.2
September 1, 2025
September 12, 2025
#### Room Version 12 is now stable.
You can freely create these rooms and join them over the federation. Administrators should take note of the following:
- The default room version is still 11. This is due to Matrix compliance tests not yet providing total coverage to make formal assurances about the new version.
- Hydra-backports are not enabled by default. Administrators of high risk servers can enable `hydra_backports` to continue using their pre-v12 rooms with increased security. This will be enabled by default very shortly.
- The ability to upgrade from existing rooms to version 12 is not yet complete as of this release. This decision was purely economical: we provide `hydra_backports` in the interim.
Users running maubot, neochat, or any client or bridge not excluded below should update to this patch as soon as possible to reduce unnecessary resource consumption. (see: Bug Fixes)
### New Features
- **Deleting Rooms** is now possible thanks to a generous effort by @dasha_uwu. Admins can use the `!admin rooms delete-room` command to force all local users to leave and erase the room from the database.
- Requested by @alaviss an alternative DNS resolver has been implemented for use with appservices and other configured targets intended for local networks. This passthru performs minimal caching and cannot be used for federation. Enable with `dns_passthru_appservices = true` or specifying hosts in `dns_passthru_domains` (#158)
- An idea by @korewaChino (#136) taken up by serial contribtor @dasha_uwu now grants admins the power to access rooms using `!admin users force-promote` if at least one user on the server has federation-level access to the room. This feature is a major Trust & Safety enhancement.
- Contributed by @tototomate123 a nifty experimental feature can disable push notifications when you're active on one device from being sent to others. This can be enabled with `suppress_push_when_active`. Please thank them when your pocket stops vibrating while chatting on your desktop! (#150)
- Thanks to an idea by @obioma (#118) with an implementation contributed by @dasha_uwu the admin room can be de-federated by setting `federate_admin_room = false` when first setting up a new Tuwunel server. This option is only available for fresh installs.
- Thanks to a report by @DetermineAbsurd the `m.federate` field can be defaulted to false when creating a room using the new `federate_created_rooms` config option. (#151)
- At the request of @grinapo verbose logging builds are now bundled with this release. These builds are found with the feature-set `-logging-` which is otherwise similar to `-all-`. This contains more messages at all levels optimized away in other release modes; it comes at some performance penalty.
- JWT tokens can now be used for authentication on any endpoint which supports UIA. For example: an external forgot-password service can send a token to the `client/account/password` endpoint to reset a user's password. This feature was commissioned and made public by an enterprise sponsor.
### Enhancements
- Based on a help request by @mageslayer (#138) the docs for configuring Caddy have been graciously improved by @itzk0tlin (#139).
- Sliding-sync has been significantly refactored. Performance has massively increased with many bugs and compliance issues also fixed. Please be aware we are tracking an issue related to read-marker behavior in Element X. The 🟢 dot does not unconditionally clear at every touch. Whether this is a feature or a bug, or both, is being investigated for v1.5.
- Spaces pageloads have been optimized. This primarily affects large and multi-level spaces such as the Matrix Community.
- Hydra backports are now enabled by default. The change should be completely transparent. If you do notice any increased load try to increase the `cache_capacity_modifier` above default.
- Building on the room deletion infrastructure contributed by @dasha_uwu an experimental addon can automatically delete empty rooms after the last local user leaves. This is not enabled by default and highly experimental and will not be considered stable until the next release.
- Room deletions now also purge synctokens which can be significant to the overall storage consumed by a room. Users who have already deleted rooms please be assured an update planned for v1.5 will deal with cleansing synctokens in general.
- Room version 1 and 2 support took a step forward, possibly working for some rooms but is not yet considered adequately supported and the ticket remains open. (#12)
- Thanks to @AreYouLoco for contributing an updated Kubernetes [Helm Chart](https://github.com/AreYouLoco/tuwunel-helm); link added to docs.
### Bug Fixes
- Thanks to a concise report by @alaviss the `/joined_members` and `/members` endpoints now return consistent profile data for each user. Previously the former returned "global profile" data rather than room membership. (#121)
- **Special thanks to @frebib for investigating a bug which triggers the uploading of unnecessary encryption one-time-keys.** Running over ten maubot instances it became obvious after observing increased resources and laggy bot response. This update removes any excess keys for a device. Thanks to @duckbuster for confirming neochat is affected. Clients confirmed unaffected include: Element, Element X, Nheko. Fractal, Cinny, matrix-rust-sdk and matrix-js-sdk clients and bots are probably unaffected. Mautrix-based bridges are probably affected. Users of unaffected clients should still upgrade.
- After a diagnosis by @gardiol the pushers set by a client are now deleted when the associated device logs out. (#120)
- Thanks @dasha_uwu for refactoring alias resolution logic with fixes to remain compatible with the upcoming element-web release. This was an incredibly valuable contribution which will spare all of us from impending grief; the kind of ahead-of-the-game initiative I don't think a project like this could exist without. (adadafa88f3)
- Sync longpoll loop properly terminates for server shutdown thanks to @dasha_uwu.
- Room deletions now preserve a small number of records to properly synchronize with local clients and remote servers after the room vanishes. Prior behavior is maintained with a `--force` flag added to the command.
- Joining restricted rooms with an invite has been fixed by @dasha_uwu.
- Thanks @scvalex for once again cleaning up our mess after Nix found the github CI was not running doctests. Thank you for contributing the patch 🙏 (#152).
- Thanks @obioma for making corrections to documentation.
- Thanks @Tronde for reporting a broken link to the CoC in the mdbook documentation. (#155)
- Specification compliance required the `/joined_rooms` endpoint be restricted to current members rather than including past members. (4b49aaad53a)
- Specification compliance required state events be made visible to prior members of a room where `history_visibility=shared`. (86781522b68)
- The `limit` parameter to the `/context` endpoint is now divided with de facto compatibility (matrix-org/matrix-spec#2202)
- The room avatar in sliding sync is now computed with greater compliance to the specification (3deebeab78f). This builds off earlier work done by @tmayoff in (a340e6786db).
- The canonical alias for a room is considered invalid if the primary alias is missing or removed (7221d466ce8). This is a T&S concern and we encourage reports for any other contexts where this condition should be applied.
- Presence is no longer updated by the private read-receipt or read-marker paths, only public receipts.
### Deprecations
- Hardened Malloc support had to be removed after the build broke. We will gladly add support back upon request or contribution.
+29 -6
View File
@@ -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"
@@ -233,6 +237,7 @@ group "lints" {
group "tests" {
targets = [
"docs",
"unit",
"smoke",
"complement",
@@ -809,6 +814,25 @@ target "unit" {
}
}
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"
}
}
#
# Workspace builds
#
@@ -835,10 +859,10 @@ target "book" {
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 = [
@@ -1035,7 +1059,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"
}
}
@@ -1613,7 +1637,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": "",
+3
View File
@@ -31,6 +31,9 @@ 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"
set -a
cargo_profile="${cargo_profile:-$default_cargo_profile}"
+1
View File
@@ -0,0 +1 @@
../CODE_OF_CONDUCT.md
+1 -1
View File
@@ -20,4 +20,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)
+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.
+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
-54
View File
@@ -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"
];
-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"
]
+8 -10
View File
@@ -974,10 +974,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 +994,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)
+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,
},
}
+2 -2
View File
@@ -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
{
@@ -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
{
+1 -1
View File
@@ -665,7 +665,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?;
+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
+1 -1
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<_>) =
+8 -73
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};
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`
///
@@ -126,10 +123,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 +139,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 +166,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 +184,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(),
+5 -1
View File
@@ -40,7 +40,11 @@ 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 {
for (key_id, one_time_key) in body
.one_time_keys
.iter()
.take(services.config.one_time_key_limit)
{
if one_time_key
.deserialize()
.inspect_err(|e| {
+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?;
+32 -9
View File
@@ -1,5 +1,5 @@
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},
@@ -7,10 +7,20 @@
},
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,6 +94,9 @@ 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| {
let content = pdu.get_content::<RoomMemberEventContent>().ok()?;
let sender = pdu.sender().to_owned();
+14 -16
View File
@@ -48,15 +48,14 @@ pub(crate) async fn set_read_marker_route(
.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?;
}
if let Some(event) = &body.read_receipt {
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
let receipt_content = BTreeMap::from_iter([(
event.to_owned(),
BTreeMap::from_iter([(
@@ -120,14 +119,6 @@ pub(crate) async fn create_receipt_route(
.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 {
@@ -146,6 +137,13 @@ pub(crate) async fn create_receipt_route(
.await?;
},
| create_receipt::v3::ReceiptType::Read => {
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
let receipt_content = BTreeMap::from_iter([(
body.event_id.clone(),
BTreeMap::from_iter([(
+20
View File
@@ -413,6 +413,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 +434,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 +545,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 +572,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
},
+4 -2
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,
};
@@ -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)?;
+1 -1
View File
@@ -1,5 +1,5 @@
mod appservice;
mod jwt;
pub(crate) mod jwt;
mod ldap;
mod logout;
mod password;
+22 -1
View File
@@ -133,6 +133,11 @@ pub(crate) async fn sync_events_route(
.await
.log_err()
.ok();
// Record user as actively syncing for push suppression heuristic.
if services.config.suppress_push_when_active {
services.presence.note_sync(sender_user).await;
}
}
let mut since = body
@@ -178,8 +183,9 @@ pub(crate) async fn sync_events_route(
// Wait for activity
if time::timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
let response = build_empty_response(&services, &body, next_batch).await;
trace!(since, next_batch, "empty response");
return Ok(sync_events::v3::Response::new(next_batch.to_string()));
return Ok(response);
}
trace!(
@@ -194,6 +200,21 @@ pub(crate) async fn sync_events_route(
}
}
async fn build_empty_response(
services: &Services,
body: &Ruma<sync_events::v3::Request>,
next_batch: u64,
) -> sync_events::v3::Response {
sync_events::v3::Response {
device_one_time_keys_count: services
.users
.count_one_time_keys(body.sender_user(), body.sender_device())
.await,
..sync_events::v3::Response::new(next_batch.to_string())
}
}
#[tracing::instrument(
name = "build",
level = "debug",
+893 -618
View File
File diff suppressed because it is too large Load Diff
+3 -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> {
+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
.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()
})
}
+129
View File
@@ -0,0 +1,129 @@
use axum::RequestPartsExt;
use axum_extra::{TypedHeader, headers::Authorization, typed_header::TypedHeaderRejectionReason};
use ruma::{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 = (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)
}
+81
View File
@@ -0,0 +1,81 @@
use ruma::{
CanonicalJsonValue, OwnedUserId,
api::{
IncomingRequest,
client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo},
},
};
use tuwunel_core::{Err, Error, Result, err, utils};
use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH};
use crate::{Ruma, client::jwt};
pub(crate) async fn auth_uiaa<T>(services: &Services, body: &Ruma<T>) -> Result<OwnedUserId>
where
T: IncomingRequest + Send + Sync,
{
let flows = [
AuthFlow::new([AuthType::Password].into()),
AuthFlow::new([AuthType::Jwt].into()),
];
let mut uiaainfo = UiaaInfo {
flows: flows.into(),
..Default::default()
};
match body
.json_body
.as_ref()
.and_then(CanonicalJsonValue::as_object)
.and_then(|body| body.get("auth"))
.cloned()
.map(CanonicalJsonValue::into)
.map(serde_json::from_value)
.transpose()?
{
| Some(AuthData::Jwt(Jwt { ref token, .. })) => {
let sender_user = jwt::validate_user(services, token)?;
if !services.users.exists(&sender_user).await {
return Err!(Request(NotFound("User {sender_user} is not registered.")));
}
// Success!
Ok(sender_user)
},
| Some(ref auth) => {
let sender_user = body
.sender_user
.as_deref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
Ok(sender_user.to_owned())
},
| _ => match body.json_body {
| Some(ref json) => {
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
Err(Error::Uiaa(uiaainfo))
},
| _ => Err!(Request(NotJson("JSON body is not valid"))),
},
}
}
-7
View File
@@ -26,9 +26,6 @@ tuwunel_mods = [
gzip_compression = [
"reqwest/gzip",
]
hardened_malloc = [
"dep:hardened_malloc-rs"
]
jemalloc = [
"dep:tikv-jemalloc-sys",
"dep:tikv-jemalloc-ctl",
@@ -114,10 +111,6 @@ url.workspace = true
[target.'cfg(unix)'.dependencies]
nix.workspace = true
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
hardened_malloc-rs.workspace = true
hardened_malloc-rs.optional = true
[dev-dependencies]
insta.workspace = true
maplit.workspace = true
-15
View File
@@ -1,15 +0,0 @@
//! hmalloc allocator
#[global_allocator]
static HMALLOC: hardened_malloc_rs::HardenedMalloc = hardened_malloc_rs::HardenedMalloc;
pub fn trim<I: Into<Option<usize>>>(_: I) -> crate::Result { Ok(()) }
#[must_use]
//TODO: get usage
pub fn memory_usage() -> Option<String> { None }
#[must_use]
pub fn memory_stats(_opts: &str) -> Option<String> {
Some("Extended statistics are not available from hardened_malloc.".to_owned())
}
+2 -21
View File
@@ -6,26 +6,7 @@
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
pub use je::{memory_stats, memory_usage, trim};
#[cfg(all(
not(target_env = "msvc"),
feature = "hardened_malloc",
not(feature = "jemalloc")
))]
pub mod hardened;
#[cfg(all(
not(target_env = "msvc"),
feature = "hardened_malloc",
not(feature = "jemalloc")
))]
pub use hardened::{memory_stats, memory_usage, trim};
#[cfg(any(
target_env = "msvc",
all(not(feature = "hardened_malloc"), not(feature = "jemalloc"))
))]
#[cfg(any(target_env = "msvc", not(feature = "jemalloc")))]
pub mod default;
#[cfg(any(
target_env = "msvc",
all(not(feature = "hardened_malloc"), not(feature = "jemalloc"))
))]
#[cfg(any(target_env = "msvc", not(feature = "jemalloc")))]
pub use default::{memory_stats, memory_usage, trim};
+7
View File
@@ -229,6 +229,13 @@ pub fn check(config: &Config) -> Result {
));
}
if config.suppress_push_when_active {
warn!(
"Push suppression when active is enabled (EXPERIMENTAL): behavior may change or be \
unstable. Disable by removing or setting suppress_push_when_active to false."
);
}
if config
.url_preview_domain_contains_allowlist
.contains(&"*".to_owned())
+59 -5
View File
@@ -285,9 +285,10 @@ pub struct Config {
/// Minimum time-to-live in seconds for entries in the DNS cache. The
/// default may appear high to most administrators; this is by design as the
/// majority of NXDOMAINs are correct for a long time (e.g. the server is no
/// longer running Matrix). Only decrease this if you are using an external
/// DNS cache.
/// exotic loads of federating to many other servers require a higher TTL
/// than many domains have set. Even when using an external DNS cache the
/// problem is shifted to that cache which is ignorant of its role for
/// this application and can adhere to many low TTL's increasing its load.
///
/// default: 10800
#[serde(default = "default_dns_min_ttl")]
@@ -365,6 +366,25 @@ pub struct Config {
#[serde(default = "default_ip_lookup_strategy")]
pub ip_lookup_strategy: u8,
/// List of domain patterns resolved via the alternative path without any
/// persistent cache, very small memory cache, and no enforced TTL. This
/// is intended for internal network and application services which require
/// these specific properties. This path does not support federation or
/// general purposes.
///
/// example: ["*\.dns\.podman$"]
///
/// default: []
#[serde(default, with = "serde_regex")]
pub dns_passthru_domains: RegexSet,
/// Whether to resolve appservices via the alternative path; setting this is
/// superior to providing domains in `dns_passthru_domains` if all
/// appservices intend to be matched anyway. The overhead of matching regex
/// and maintaining the list of domains can be avoided.
#[serde(default)]
pub dns_passthru_appservices: bool,
/// Max request size for file uploads in bytes. Defaults to 20MB.
///
/// default: 20971520
@@ -559,10 +579,21 @@ pub struct Config {
pub allow_encryption: bool,
/// Controls whether federation is allowed or not. It is not recommended to
/// disable this after the fact due to potential federation breakage.
/// disable this after installation due to potential federation breakage but
/// this is technically not a permanent setting.
#[serde(default = "true_fn")]
pub allow_federation: bool,
/// Sets the default `m.federate` property for newly created rooms when the
/// client does not request one. If `allow_federation` is set to false at
/// the same this value is set to false it then always overrides the client
/// requested `m.federate` value to false.
///
/// Rooms are fixed to the setting at the time of their creation and can
/// never be changed; changing this value only affects new rooms.
#[serde(default = "true_fn")]
pub federate_created_rooms: bool,
/// Allows federation requests to be made to itself
///
/// This isn't intended and is very likely a bug if federation requests are
@@ -1287,6 +1318,17 @@ pub struct Config {
#[serde(default = "true_fn")]
pub presence_timeout_remote_users: bool,
/// Suppresses push notifications for users marked as active. (Experimental)
///
/// When enabled, users with `Online` presence and recent activity
/// (based on presence state and sync activity) wont receive push
/// notifications, reducing duplicate alerts while they're active
/// on another client.
///
/// Disabled by default to preserve legacy behavior.
#[serde(default)]
pub suppress_push_when_active: bool,
/// Allow receiving incoming read receipts from remote servers.
#[serde(default = "true_fn")]
pub allow_incoming_read_receipts: bool,
@@ -1941,7 +1983,7 @@ pub struct Config {
/// real-world testing can shake out any implementation issues rather than
/// jeopardize existing rooms, but otherwise will default to true at the
/// next point release or patch.
#[serde(default)]
#[serde(default = "true_fn")]
pub hydra_backports: bool,
/// Delete rooms when the last user from this server leaves. This feature is
@@ -1957,6 +1999,16 @@ pub struct Config {
#[serde(default)]
pub delete_rooms_after_leave: bool,
/// Limits the number of One Time Keys per device (not per-algorithm). The
/// reference implementation maintains 50 OTK's at any given time, therefor
/// our default is at least five times that. There is no known reason for an
/// administrator to adjust this value; it is provided here rather than
/// hardcoding it.
///
/// default: 256
#[serde(default = "default_one_time_key_limit")]
pub one_time_key_limit: usize,
// external structure; separate section
#[serde(default)]
pub blurhashing: BlurhashConfig,
@@ -2780,3 +2832,5 @@ fn default_access_token_ttl() -> u64 { 604_800 }
fn default_deprioritize_joins_through_servers() -> RegexSet {
RegexSet::new([r"matrix\.org"]).unwrap()
}
fn default_one_time_key_limit() -> usize { 256 }
+10 -6
View File
@@ -53,11 +53,7 @@ macro_rules! debug_info {
}
}
pub const INFO_SPAN_LEVEL: Level = if cfg!(debug_assertions) {
Level::INFO
} else {
Level::DEBUG
};
pub const INFO_SPAN_LEVEL: Level = if logging() { Level::INFO } else { Level::DEBUG };
pub static DEBUGGER: LazyLock<bool> =
LazyLock::new(|| env::var("_").unwrap_or_default().ends_with("gdb"));
@@ -114,6 +110,14 @@ pub fn rttype_name<T: ?Sized>(_: &T) -> &'static str { type_name::<T>() }
#[must_use]
pub fn type_name<T: ?Sized>() -> &'static str { std::any::type_name::<T>() }
/// Returns true if debug logging is enabled. In this mode extra logging calls
/// are made at all log levels, not just DEBUG and TRACE. These logs are demoted
/// to DEBUG level when this function returns false; as a consequence they will
/// be elided by `release_max_log_level` when featured.
#[must_use]
#[inline]
pub const fn logging() -> bool { cfg!(debug_assertions) }
pub const fn logging() -> bool {
cfg!(debug_assertions)
|| cfg!(tuwunel_debug_logging)
|| !cfg!(feature = "release_max_log_level")
}
+29 -5
View File
@@ -1,7 +1,7 @@
use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId};
use ruma::{CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomVersionId};
use serde_json::value::RawValue as RawJsonValue;
use crate::{Result, err, matrix::room_version};
use crate::{Result, debug_error, err, matrix::room_version};
/// Generates a correct eventId for the incoming pdu.
///
@@ -12,20 +12,44 @@ pub fn gen_event_id_canonical_json(
room_version_id: &RoomVersionId,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get())
.map_err(|e| err!(BadServerResponse(warn!("Error parsing incoming event: {e:?}"))))?;
.map_err(|e| err!(BadServerResponse(warn!("Error parsing canonical event: {e}"))))
.inspect_err(|e| debug_error!("{pdu:#?} {e:?}"))?;
let event_id = gen_event_id(&value, room_version_id)?;
Ok((event_id, value))
}
/// Generates a correct eventId for the incoming pdu.
/// Generates a correct eventId for the PDU. For v1/v2 incoming PDU's the
/// value's event_id is passed through. For all outgoing PDU's and for v3+
/// incoming PDU's it is generated.
pub fn gen_event_id(
value: &CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
let room_version_rules = room_version::rules(room_version_id)?;
let require_event_id = room_version_rules.event_format.require_event_id;
// We don't actually generate any event_id for incoming events in v1/v2 rooms,
// just pass them through.
if let Some(event_id) = require_event_id
.then(|| value.get("event_id"))
.flatten()
.and_then(CanonicalJsonValue::as_str)
.map(OwnedEventId::try_from)
.transpose()?
{
return Ok(event_id);
}
// For outgoing v1/v2 add the server part. This has to be our origin but we
// can't assert that here.
let server_name = require_event_id
.then(|| value.get("origin"))
.flatten()
.and_then(CanonicalJsonValue::as_str);
let reference_hash = ruma::signatures::reference_hash(value, &room_version_rules)?;
OwnedEventId::from_parts('$', &reference_hash, None).map_err(Into::into)
OwnedEventId::from_parts('$', &reference_hash, server_name).map_err(Into::into)
}
+4
View File
@@ -103,6 +103,10 @@ pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<
let event_id = CanonicalJsonValue::String(event_id.into());
json.insert("event_id".into(), event_id);
Self::from_val(&json)
}
pub fn from_val(json: &CanonicalJsonObject) -> Result<Self> {
serde_json::to_value(json)
.and_then(serde_json::from_value)
.map_err(Into::into)
+80 -2
View File
@@ -1,6 +1,12 @@
use ruma::{CanonicalJsonObject, CanonicalJsonValue, RoomVersionId};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, RoomVersionId,
room_version_rules::{EventsReferenceFormatVersion, RoomVersionRules},
};
use crate::{is_equal_to, matrix::room_version};
use crate::{
Result, extract_variant, is_equal_to,
matrix::{PduEvent, room_version},
};
pub fn into_outgoing_federation(
mut pdu_json: CanonicalJsonObject,
@@ -35,5 +41,77 @@ pub fn into_outgoing_federation(
}
}
if matches!(room_rules.events_reference_format, EventsReferenceFormatVersion::V1) {
if let Some(value) = pdu_json.get_mut("auth_events") {
mutate_outgoing_reference_format(value);
}
if let Some(value) = pdu_json.get_mut("prev_events") {
mutate_outgoing_reference_format(value);
}
}
pdu_json
}
fn mutate_outgoing_reference_format(value: &mut CanonicalJsonValue) {
value
.as_array_mut()
.into_iter()
.flatten()
.for_each(|value| {
if let Some(event_id) = value.as_str().map(ToOwned::to_owned) {
*value = CanonicalJsonValue::Array(vec![
CanonicalJsonValue::String(event_id),
CanonicalJsonValue::Object([(String::new(), "".into())].into()),
]);
}
});
}
pub fn from_incoming_federation(
room_id: &RoomId,
event_id: &EventId,
pdu_json: &mut CanonicalJsonObject,
room_rules: &RoomVersionRules,
) -> Result<PduEvent> {
if matches!(room_rules.events_reference_format, EventsReferenceFormatVersion::V1) {
if let Some(value) = pdu_json.get_mut("auth_events") {
mutate_incoming_reference_format(value);
}
if let Some(value) = pdu_json.get_mut("prev_events") {
mutate_incoming_reference_format(value);
}
}
if !room_rules
.event_format
.require_room_create_room_id
&& pdu_json["type"] == "m.room.create"
{
pdu_json.insert("room_id".into(), CanonicalJsonValue::String(room_id.as_str().into()));
}
if !room_rules.event_format.require_event_id {
pdu_json.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.into()));
}
PduEvent::from_val(pdu_json)
}
fn mutate_incoming_reference_format(value: &mut CanonicalJsonValue) {
value
.as_array_mut()
.into_iter()
.flat_map(|vec| vec.iter_mut())
.for_each(|value| {
let event_id = value
.as_array()
.into_iter()
.find_map(|vec| vec.first())
.and_then(|val| extract_variant!(val, CanonicalJsonValue::String))
.cloned()
.unwrap_or_default();
*value = CanonicalJsonValue::String(event_id);
});
}
+5 -4
View File
@@ -4,7 +4,7 @@
mod tests;
use futures::{
FutureExt, TryFutureExt, TryStreamExt,
FutureExt, TryStreamExt,
future::{join3, try_join},
};
use ruma::{
@@ -119,9 +119,10 @@ pub(super) async fn check_state_independent_auth_rules<Fetch, Fut, Pdu>(
let seen_auth_types = incoming_event
.auth_events()
.try_stream()
.and_then(|event_id: &EventId| {
fetch_event(event_id.to_owned())
.map_err(|_| err!(Request(NotFound("failed to find auth event"))))
.and_then(async |event_id: &EventId| match fetch_event(event_id.to_owned()).await {
| Ok(auth_event) => Ok(auth_event),
| Err(e) if e.is_not_found() => Err!(Request(NotFound("auth event {event_id}: {e}"))),
| Err(e) => Err(e),
})
.ready_try_fold(seen_auth_types, |mut seen_auth_types, auth_event| {
let event_id = auth_event.event_id();
+1 -1
View File
@@ -28,7 +28,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// use tuwunel_core::utils::debug::slice_truncated;
///
/// #[tracing::instrument(fields(foos = slice_truncated(foos, 42)))]
/// fn bar(foos: &[&str]);
/// fn bar(foos: &[&str]) {}
/// ```
pub fn slice_truncated<T: fmt::Debug>(
slice: &[T],
+5 -1
View File
@@ -81,7 +81,11 @@ macro_rules! apply {
};
(4, $($idx:tt)+) => {
|t| (($($idx)+)(t.0), ($($idx)+)(t.1), ($($idx)+)(t.2), ($($idx)+4)(t.3))
|t| (($($idx)+)(t.0), ($($idx)+)(t.1), ($($idx)+)(t.2), ($($idx)+)(t.3),)
};
(5, $($idx:tt)+) => {
|t| (($($idx)+)(t.0), ($($idx)+)(t.1), ($($idx)+)(t.2), ($($idx)+)(t.3), ($($idx)+)(t.4),)
};
}
-7
View File
@@ -100,9 +100,6 @@ gzip_compression = [
"tuwunel-router/gzip_compression",
"tuwunel-service/gzip_compression",
]
hardened_malloc = [
"tuwunel-core/hardened_malloc",
]
io_uring = [
"tuwunel-admin/io_uring",
"tuwunel-api/io_uring",
@@ -236,10 +233,6 @@ tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
hardened_malloc-rs.workspace = true
hardened_malloc-rs.optional = true
[lints]
workspace = true
+18 -10
View File
@@ -5,7 +5,7 @@
use either::Either;
use ipaddress::IPAddress;
use reqwest::redirect;
use reqwest::{dns::Resolve, redirect};
use tuwunel_core::{Config, Result, err, implement, trace};
use crate::{service, services::OnceServices};
@@ -42,7 +42,7 @@ fn make($services: Arc<OnceServices>) -> Result<reqwest::Client> {
Ok(Arc::new(Self {
default: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver))),
.dns_resolver2(Arc::clone(&services.resolver.resolver))),
url_preview: create_client!(config, services; {
let url_preview_bind_addr = config
@@ -60,16 +60,16 @@ fn make($services: Arc<OnceServices>) -> Result<reqwest::Client> {
builder_interface(builder, url_preview_bind_iface.as_deref())
})?
.local_address(url_preview_bind_addr)
.dns_resolver(Arc::clone(&services.resolver.resolver))
.dns_resolver2(Arc::clone(&services.resolver.resolver))
.redirect(redirect::Policy::limited(3))
}),
extern_media: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver))
.dns_resolver2(Arc::clone(&services.resolver.resolver))
.redirect(redirect::Policy::limited(3))),
well_known: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver))
.dns_resolver2(Arc::clone(&services.resolver.resolver))
.connect_timeout(Duration::from_secs(config.well_known_conn_timeout))
.read_timeout(Duration::from_secs(config.well_known_timeout))
.timeout(Duration::from_secs(config.well_known_timeout))
@@ -77,20 +77,20 @@ fn make($services: Arc<OnceServices>) -> Result<reqwest::Client> {
.redirect(redirect::Policy::limited(4))),
federation: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
.dns_resolver2(Arc::clone(&services.resolver.resolver.hooked))
.read_timeout(Duration::from_secs(config.federation_timeout))
.pool_max_idle_per_host(config.federation_idle_per_host.into())
.pool_idle_timeout(Duration::from_secs(config.federation_idle_timeout))
.redirect(redirect::Policy::limited(3))),
synapse: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
.dns_resolver2(Arc::clone(&services.resolver.resolver.hooked))
.read_timeout(Duration::from_secs(305))
.pool_max_idle_per_host(0)
.redirect(redirect::Policy::limited(3))),
sender: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
.dns_resolver2(Arc::clone(&services.resolver.resolver.hooked))
.read_timeout(Duration::from_secs(config.sender_timeout))
.timeout(Duration::from_secs(config.sender_timeout))
.pool_max_idle_per_host(1)
@@ -98,7 +98,7 @@ fn make($services: Arc<OnceServices>) -> Result<reqwest::Client> {
.redirect(redirect::Policy::limited(2))),
appservice: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver))
.dns_resolver2(appservice_resolver(&services))
.connect_timeout(Duration::from_secs(5))
.read_timeout(Duration::from_secs(config.appservice_timeout))
.timeout(Duration::from_secs(config.appservice_timeout))
@@ -107,7 +107,7 @@ fn make($services: Arc<OnceServices>) -> Result<reqwest::Client> {
.redirect(redirect::Policy::limited(2))),
pusher: create_client!(config, services; base(config)?
.dns_resolver(Arc::clone(&services.resolver.resolver))
.dns_resolver2(Arc::clone(&services.resolver.resolver))
.pool_max_idle_per_host(1)
.pool_idle_timeout(Duration::from_secs(config.pusher_idle_timeout))
.redirect(redirect::Policy::limited(2))),
@@ -220,6 +220,14 @@ fn builder_interface(
}
}
fn appservice_resolver(services: &Arc<OnceServices>) -> Arc<dyn Resolve> {
if services.server.config.dns_passthru_appservices {
services.resolver.resolver.passthru.clone()
} else {
services.resolver.resolver.clone()
}
}
#[inline]
#[must_use]
#[implement(Service)]
+1 -1
View File
@@ -145,7 +145,7 @@ pub async fn full_deactivate(&self, user_id: &UserId) -> 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
{
+9 -1
View File
@@ -11,7 +11,9 @@
use async_trait::async_trait;
use data::Data;
use regex::RegexSet;
use ruma::{OwnedEventId, OwnedRoomAliasId, OwnedServerName, OwnedUserId, ServerName, UserId};
use ruma::{
OwnedEventId, OwnedRoomAliasId, OwnedServerName, OwnedUserId, RoomAliasId, ServerName, UserId,
};
use tuwunel_core::{Result, Server, error, utils::bytes::pretty};
use crate::service;
@@ -250,6 +252,12 @@ pub fn user_is_local(&self, user_id: &UserId) -> bool {
self.server_is_ours(user_id.server_name())
}
#[inline]
#[must_use]
pub fn alias_is_local(&self, alias: &RoomAliasId) -> bool {
self.server_is_ours(alias.server_name())
}
#[inline]
#[must_use]
pub fn server_is_ours(&self, server_name: &ServerName) -> bool {
+30 -70
View File
@@ -1,6 +1,6 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, OwnedUserId, RoomId, RoomVersionId,
UserId,
@@ -16,12 +16,9 @@
room::{AllowRule, JoinRule},
};
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, error, implement, info,
matrix::{
event::{gen_event_id, gen_event_id_canonical_json},
room_version,
},
pdu::{PduBuilder, PduEvent},
Err, Result, debug, debug_error, debug_info, debug_warn, err, error, implement, info,
matrix::{event::gen_event_id_canonical_json, room_version},
pdu::{PduBuilder, format::from_incoming_federation},
state_res, trace,
utils::{self, IterStream, ReadyExt},
warn,
@@ -152,6 +149,8 @@ pub async fn join_remote(
));
}
let room_version_rules = room_version::rules(&room_version_id)?;
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse(warn!(
@@ -221,27 +220,10 @@ pub async fn join_remote(
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
self.services
let event_id = self
.services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.gen_id_hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// It has enough fields to be called a proper event now
let mut join_event = join_event_stub;
@@ -342,8 +324,8 @@ pub async fn join_remote(
.await;
info!("Parsing join event");
let parsed_join_pdu = PduEvent::from_id_val(&event_id, join_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid join event PDU: {e:?}")))?;
let parsed_join_pdu =
from_incoming_federation(room_id, &event_id, &mut join_event, &room_version_rules)?;
info!("Acquiring server signing keys for response events");
let resp_events = &send_join_response.room_state;
@@ -366,27 +348,17 @@ pub async fn join_remote(
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.inspect_err(|e| debug_error!("Invalid send_join state event: {e:?}"))
.ready_filter_map(Result::ok)
.fold(HashMap::new(), async |mut state, (event_id, mut value)| {
let pdu = if value["type"] == "m.room.create" {
if !value.contains_key("room_id") {
let room_id = CanonicalJsonValue::String(room_id.as_str().into());
value.insert("room_id".into(), room_id);
}
PduEvent::from_rid_val(room_id, &event_id, value.clone())
} else {
PduEvent::from_id_val(&event_id, value.clone())
};
let pdu = match pdu {
| Ok(pdu) => pdu,
| Err(e) => {
.ready_filter_map(|(event_id, mut value)| {
from_incoming_federation(room_id, &event_id, &mut value, &room_version_rules)
.inspect_err(|e| {
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
return state;
},
};
})
.map(move |pdu| (event_id, pdu, value))
.ok()
})
.fold(HashMap::new(), async |mut state, (event_id, pdu, value)| {
self.services
.timeline
.add_pdu_outlier(&event_id, &value);
@@ -419,9 +391,14 @@ pub async fn join_remote(
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.inspect_err(|e| debug_error!("Invalid send_join auth_chain event: {e:?}"))
.ready_filter_map(Result::ok)
.ready_for_each(|(event_id, mut value)| {
if !value.contains_key("room_id") {
if !room_version_rules
.event_format
.require_room_create_room_id
&& value["type"] == "m.room.create"
{
let room_id = CanonicalJsonValue::String(room_id.as_str().into());
value.insert("room_id".into(), room_id);
}
@@ -436,7 +413,7 @@ pub async fn join_remote(
debug!("Running send_join auth check");
state_res::auth_check(
&room_version::rules(&room_version_id)?,
&room_version_rules,
&parsed_join_pdu,
&async |event_id| self.services.timeline.get_pdu(&event_id).await,
&async |event_type, state_key| {
@@ -717,27 +694,10 @@ pub async fn join_local(
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
self.services
let event_id = self
.services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.gen_id_hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// It has enough fields to be called a proper event now
let join_event = join_event_stub;
+13 -24
View File
@@ -2,7 +2,7 @@
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, UserId,
api::federation,
events::{
StateEventType,
@@ -11,7 +11,6 @@
};
use tuwunel_core::{
Err, Result, debug_info, debug_warn, err, implement,
matrix::event::gen_event_id,
pdu::PduBuilder,
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
@@ -31,6 +30,7 @@ pub async fn leave(
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
remote_leave_now: bool,
state_lock: &RoomMutexGuard,
) -> Result {
let default_member_content = RoomMemberEventContent {
@@ -80,7 +80,7 @@ pub async fn leave(
.eq(&false);
// Ask a remote server if we don't have this room and are not knocking on it
if dont_have_room.and(not_knocked).await {
if remote_leave_now || dont_have_room.and(not_knocked).await {
if let Err(e) = self.remote_leave(user_id, room_id).boxed().await {
warn!(%user_id, "Failed to leave room {room_id} remotely: {e}");
// Don't tell the client about this error
@@ -168,7 +168,7 @@ pub async fn leave(
#[implement(Service)]
#[tracing::instrument(name = "remote", level = "debug", skip_all)]
pub async fn remote_leave(&self, user_id: &UserId, room_id: &RoomId) -> Result {
async fn remote_leave(&self, user_id: &UserId, room_id: &RoomId) -> Result {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
@@ -176,6 +176,7 @@ pub async fn remote_leave(&self, user_id: &UserId, room_id: &RoomId) -> Result {
.services
.state_cache
.servers_invite_via(room_id)
.chain(self.services.state_cache.room_servers(room_id))
.map(ToOwned::to_owned)
.collect()
.await;
@@ -222,13 +223,17 @@ pub async fn remote_leave(&self, user_id: &UserId, room_id: &RoomId) -> Result {
},
}
servers.insert(user_id.server_name().to_owned());
if let Some(room_id_server_name) = room_id.server_name() {
servers.insert(room_id_server_name.to_owned());
}
debug_info!("servers in remote_leave_room: {servers:?}");
for remote_server in servers {
for remote_server in servers
.into_iter()
.filter(|server| !self.services.globals.server_is_ours(server))
{
let make_leave_response = self
.services
.sending
@@ -296,26 +301,10 @@ pub async fn remote_leave(&self, user_id: &UserId, room_id: &RoomId) -> Result {
),
);
// room v3 and above removed the "event_id" field from remote PDU format
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
leave_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
self.services
let event_id = self
.services
.server_keys
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
// Add event_id back
leave_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.gen_id_hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
// It has enough fields to be called a proper event now
let leave_event = leave_event_stub;
+24 -2
View File
@@ -1,13 +1,13 @@
mod data;
mod presence;
use std::{sync::Arc, time::Duration};
use std::{collections::HashMap, sync::Arc, time::Duration};
use async_trait::async_trait;
use futures::{Stream, StreamExt, TryFutureExt, stream::FuturesUnordered};
use loole::{Receiver, Sender};
use ruma::{OwnedUserId, UInt, UserId, events::presence::PresenceEvent, presence::PresenceState};
use tokio::time::sleep;
use tokio::{sync::RwLock, time::sleep};
use tuwunel_core::{Error, Result, checked, debug, debug_warn, error, result::LogErr, trace};
use self::{data::Data, presence::Presence};
@@ -19,6 +19,7 @@ pub struct Service {
offline_timeout: u64,
db: Data,
services: Arc<crate::services::OnceServices>,
last_sync_seen: RwLock<HashMap<OwnedUserId, u64>>,
}
type TimerType = (OwnedUserId, Duration);
@@ -36,6 +37,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
offline_timeout: checked!(offline_timeout_s * 1_000)?,
db: Data::new(&args),
services: args.services.clone(),
last_sync_seen: RwLock::new(HashMap::new()),
}))
}
@@ -88,6 +90,26 @@ fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// record that a user has just successfully completed a /sync (or
/// equivalent activity)
pub async fn note_sync(&self, user_id: &UserId) {
let now = tuwunel_core::utils::millis_since_unix_epoch();
self.last_sync_seen
.write()
.await
.insert(user_id.to_owned(), now);
}
/// Returns milliseconds since last observed sync for user (if any)
pub async fn last_sync_gap_ms(&self, user_id: &UserId) -> Option<u64> {
let now = tuwunel_core::utils::millis_since_unix_epoch();
self.last_sync_seen
.read()
.await
.get(user_id)
.map(|ts| now.saturating_sub(*ts))
}
/// Returns the latest presence event for the given user.
pub async fn get_presence(&self, user_id: &UserId) -> Result<PresenceEvent> {
self.db
+112 -33
View File
@@ -1,39 +1,87 @@
use std::{net::SocketAddr, sync::Arc, time::Duration};
use futures::FutureExt;
use hickory_resolver::{TokioResolver, lookup_ip::LookupIp};
use hickory_resolver::{
TokioResolver,
config::{LookupIpStrategy, ResolverConfig, ResolverOpts},
lookup_ip::LookupIp,
};
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use tuwunel_core::{Result, Server, err};
use tuwunel_core::{Result, Server, err, trace};
use super::cache::{Cache, CachedOverride};
pub struct Resolver {
pub(crate) resolver: Arc<TokioResolver>,
pub(crate) passthru: Arc<Passthru>,
pub(crate) hooked: Arc<Hooked>,
server: Arc<Server>,
}
pub(crate) struct Hooked {
resolver: Arc<TokioResolver>,
passthru: Arc<Passthru>,
cache: Arc<Cache>,
server: Arc<Server>,
}
pub(crate) struct Passthru {
resolver: Arc<TokioResolver>,
server: Arc<Server>,
}
type ResolvingResult = Result<Addrs, Box<dyn std::error::Error + Send + Sync>>;
impl Resolver {
#[allow(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
pub(super) fn build(server: &Arc<Server>, cache: Arc<Cache>) -> Result<Arc<Self>> {
// Create the primary resolver.
let (conf, opts) = Self::configure(server)?;
let resolver = Self::create(server, conf.clone(), opts.clone())?;
// Create the passthru resolver with modified options.
let (conf, mut opts) = (conf, opts);
opts.negative_min_ttl = None;
opts.negative_max_ttl = None;
opts.positive_min_ttl = None;
opts.positive_max_ttl = None;
opts.cache_size = ResolverOpts::default().cache_size;
let passthru = Arc::new(Passthru {
resolver: Self::create(server, conf, opts)?,
server: server.clone(),
});
Ok(Arc::new(Self {
hooked: Arc::new(Hooked {
resolver: resolver.clone(),
passthru: passthru.clone(),
server: server.clone(),
cache,
}),
server: server.clone(),
passthru,
resolver,
}))
}
fn create(
server: &Arc<Server>,
conf: ResolverConfig,
opts: ResolverOpts,
) -> Result<Arc<TokioResolver>> {
let rt_prov = hickory_resolver::proto::runtime::TokioRuntimeProvider::new();
let conn_prov = hickory_resolver::name_server::TokioConnectionProvider::new(rt_prov);
let mut builder = TokioResolver::builder_with_config(conf, conn_prov);
*builder.options_mut() = Self::configure_opts(server, opts);
Ok(Arc::new(builder.build()))
}
fn configure(server: &Arc<Server>) -> Result<(ResolverConfig, ResolverOpts)> {
let config = &server.config;
let (sys_conf, mut opts) = hickory_resolver::system_conf::read_system_conf()
let (sys_conf, opts) = hickory_resolver::system_conf::read_system_conf()
.map_err(|e| err!(error!("Failed to configure DNS resolver from system: {e}")))?;
let mut conf = hickory_resolver::config::ResolverConfig::new();
let mut conf = ResolverConfig::new();
if let Some(domain) = sys_conf.domain() {
conf.set_domain(domain.clone());
}
@@ -44,18 +92,26 @@ pub(super) fn build(server: &Arc<Server>, cache: Arc<Cache>) -> Result<Arc<Self>
for sys_conf in sys_conf.name_servers() {
let mut ns = sys_conf.clone();
ns.trust_negative_responses = !config.query_all_nameservers;
if config.query_over_tcp_only {
ns.protocol = hickory_resolver::proto::xfer::Protocol::Tcp;
}
ns.trust_negative_responses = !config.query_all_nameservers;
conf.add_name_server(ns);
}
Ok((conf, opts))
}
#[allow(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
fn configure_opts(server: &Arc<Server>, mut opts: ResolverOpts) -> ResolverOpts {
let config = &server.config;
opts.cache_size = config.dns_cache_entries as usize;
opts.preserve_intermediates = true;
opts.negative_min_ttl = Some(Duration::from_secs(config.dns_min_ttl_nxdomain));
opts.negative_max_ttl = Some(Duration::from_secs(60 * 60 * 24 * 30));
opts.positive_min_ttl = Some(Duration::from_secs(config.dns_min_ttl));
@@ -66,25 +122,16 @@ pub(super) fn build(server: &Arc<Server>, cache: Arc<Cache>) -> Result<Arc<Self>
opts.num_concurrent_reqs = 1;
opts.edns0 = true;
opts.case_randomization = true;
opts.preserve_intermediates = true;
opts.ip_strategy = match config.ip_lookup_strategy {
| 1 => hickory_resolver::config::LookupIpStrategy::Ipv4Only,
| 2 => hickory_resolver::config::LookupIpStrategy::Ipv6Only,
| 3 => hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6,
| 4 => hickory_resolver::config::LookupIpStrategy::Ipv6thenIpv4,
| _ => hickory_resolver::config::LookupIpStrategy::Ipv4thenIpv6,
| 1 => LookupIpStrategy::Ipv4Only,
| 2 => LookupIpStrategy::Ipv6Only,
| 3 => LookupIpStrategy::Ipv4AndIpv6,
| 4 => LookupIpStrategy::Ipv6thenIpv4,
| _ => LookupIpStrategy::Ipv4thenIpv6,
};
let rt_prov = hickory_resolver::proto::runtime::TokioRuntimeProvider::new();
let conn_prov = hickory_resolver::name_server::TokioConnectionProvider::new(rt_prov);
let mut builder = TokioResolver::builder_with_config(conf, conn_prov);
*builder.options_mut() = opts;
let resolver = Arc::new(builder.build());
Ok(Arc::new(Self {
resolver: resolver.clone(),
hooked: Arc::new(Hooked { resolver, cache, server: server.clone() }),
server: server.clone(),
}))
opts
}
/// Clear the in-memory hickory-dns caches
@@ -94,14 +141,46 @@ pub(super) fn build(server: &Arc<Server>, cache: Arc<Cache>) -> Result<Arc<Self>
impl Resolve for Resolver {
fn resolve(&self, name: Name) -> Resolving {
resolve_to_reqwest(self.server.clone(), self.resolver.clone(), name).boxed()
let resolver = if self
.server
.config
.dns_passthru_domains
.is_match(name.as_str())
{
trace!(?name, "matched to passthru resolver");
&self.passthru.resolver
} else {
trace!(?name, "using primary resolver");
&self.resolver
};
resolve_to_reqwest(self.server.clone(), resolver.clone(), name).boxed()
}
}
impl Resolve for Hooked {
fn resolve(&self, name: Name) -> Resolving {
hooked_resolve(self.cache.clone(), self.server.clone(), self.resolver.clone(), name)
.boxed()
let resolver = if self
.server
.config
.dns_passthru_domains
.is_match(name.as_str())
{
trace!(?name, "matched to passthru resolver");
&self.passthru.resolver
} else {
trace!(?name, "using hooked resolver");
&self.resolver
};
hooked_resolve(self.cache.clone(), self.server.clone(), resolver.clone(), name).boxed()
}
}
impl Resolve for Passthru {
fn resolve(&self, name: Name) -> Resolving {
trace!(?name, "using passthru resolver");
resolve_to_reqwest(self.server.clone(), self.resolver.clone(), name).boxed()
}
}
+37 -36
View File
@@ -125,33 +125,30 @@ pub async fn resolve_alias(
room_alias: &RoomAliasId,
servers: Option<Vec<OwnedServerName>>,
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
let server_name = room_alias.server_name();
let server_is_ours = self.services.globals.server_is_ours(server_name);
let servers_contains_ours = || {
servers
.as_ref()
.is_some_and(|servers| servers.contains(&self.services.server.name))
};
if self
.services
.globals
.server_is_ours(room_alias.server_name())
{
if let Ok(room_id) = self.resolve_local_alias(room_alias).await {
return Ok((room_id, Vec::new()));
}
if !server_is_ours && !servers_contains_ours() {
return self
.remote_resolve(room_alias, servers.unwrap_or_default())
.await;
if let Ok(room_id) = self.resolve_appservice_alias(room_alias).await {
return Ok((room_id, Vec::new()));
}
return Err!(Request(NotFound("Room with alias not found.")));
}
let room_id = match self.resolve_local_alias(room_alias).await {
| Ok(r) => Some(r),
| Err(_) => self.resolve_appservice_alias(room_alias).await?,
};
room_id.map_or_else(
|| Err!(Request(NotFound("Room with alias not found."))),
|room_id| Ok((room_id, Vec::new())),
)
return self
.remote_resolve(room_alias, servers.unwrap_or_default())
.await;
}
#[tracing::instrument(skip(self), level = "debug")]
#[tracing::instrument(skip(self), level = "trace")]
pub async fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result<OwnedRoomId> {
self.check_alias_local(alias)?;
self.db
.alias_roomid
.get(alias.alias())
@@ -182,6 +179,8 @@ pub fn all_local_aliases(&self) -> impl Stream<Item = (&RoomId, &str)> + Send +
}
async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result<bool> {
self.check_alias_local(alias)?;
let room_id = self
.resolve_local_alias(alias)
.await
@@ -228,6 +227,8 @@ async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) ->
}
async fn who_created_alias(&self, alias: &RoomAliasId) -> Result<OwnedUserId> {
self.check_alias_local(alias)?;
self.db
.alias_userid
.get(alias.alias())
@@ -235,12 +236,11 @@ async fn who_created_alias(&self, alias: &RoomAliasId) -> Result<OwnedUserId> {
.deserialized()
}
async fn resolve_appservice_alias(
&self,
room_alias: &RoomAliasId,
) -> Result<Option<OwnedRoomId>> {
async fn resolve_appservice_alias(&self, room_alias: &RoomAliasId) -> Result<OwnedRoomId> {
use ruma::api::appservice::query::query_room_alias;
self.check_alias_local(room_alias)?;
for appservice in self.services.appservice.read().await.values() {
if appservice.aliases.is_match(room_alias.as_str())
&& matches!(
@@ -256,27 +256,28 @@ async fn resolve_appservice_alias(
return self
.resolve_local_alias(room_alias)
.await
.map_err(|_| err!(Request(NotFound("Room does not exist."))))
.map(Some);
.map_err(|_| err!(Request(NotFound("Room does not exist."))));
}
}
Ok(None)
Err!(Request(NotFound("Room does not exist.")))
}
fn check_alias_local(&self, alias: &RoomAliasId) -> Result {
if !self.services.globals.alias_is_local(alias) {
return Err!(Request(InvalidParam("Alias is from another server.")));
}
Ok(())
}
#[tracing::instrument(skip(self, appservice_info), level = "trace")]
pub async fn appservice_checks(
&self,
room_alias: &RoomAliasId,
appservice_info: &Option<RegistrationInfo>,
) -> Result {
if !self
.services
.globals
.server_is_ours(room_alias.server_name())
{
return Err!(Request(InvalidParam("Alias is from another server.")));
}
self.check_alias_local(room_alias)?;
if let Some(info) = appservice_info {
if !info.aliases.is_match(room_alias.as_str()) {
return Err!(Request(Exclusive("Room alias is not in namespace.")));
+26 -34
View File
@@ -1,8 +1,7 @@
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::RoomId;
use tokio::time::sleep;
use tuwunel_core::{
Result, debug,
result::LogErr,
@@ -54,30 +53,20 @@ pub async fn delete_if_empty_local(&self, room_id: &RoomId, state_lock: RoomMute
debug!(?room_id, "Preparing to delete room...");
// Some arbitrary delay has to account for the leave event being synced to the
// client or they'll never be updated on their leave. This can be removed once
// a tombstone solution is implemented instead.
sleep(Duration::from_millis(2500)).await;
self.services
.delete
.delete_room(room_id, state_lock)
.delete_room(room_id, false, state_lock)
.boxed()
.await
.expect("unhandled error during room deletion");
}
pub async fn delete_room(&self, room_id: &RoomId, state_lock: RoomMutexGuard) -> Result {
// ban the room locally so new users cannot join while we're in the process of
// deleting it
debug!("Banning room {room_id} prior to deletion.");
self.services.metadata.ban_room(room_id);
// This might have to be dropped here to prevent deadlock, but the goal should
// be to hold it all the way through. For now the room is banned under lock at
// least.
drop(state_lock);
pub async fn delete_room(
&self,
room_id: &RoomId,
force: bool,
state_lock: RoomMutexGuard,
) -> Result {
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
@@ -95,18 +84,14 @@ pub async fn delete_room(&self, room_id: &RoomId, state_lock: RoomMutexGuard) ->
if let Err(e) = self
.services
.membership
.remote_leave(user_id, room_id)
.leave(user_id, room_id, Some("Room Deleted".into()), true, &state_lock)
.boxed()
.await
{
warn!("Failed to leave room: {e}");
}
self.services.state_cache.forget(room_id, user_id);
}
debug!("Disabling incoming federation on room {room_id}");
self.services.metadata.disable_room(room_id);
debug!("Deleting all our room aliases for the room");
self.services
.alias
@@ -159,7 +144,7 @@ pub async fn delete_room(&self, room_id: &RoomId, state_lock: RoomMutexGuard) ->
debug!("Deleting all the room's member counts");
self.services
.state_cache
.delete_room_join_counts(room_id)
.delete_room_join_counts(room_id, force)
.await
.log_err()
.ok();
@@ -172,10 +157,23 @@ pub async fn delete_room(&self, room_id: &RoomId, state_lock: RoomMutexGuard) ->
.log_err()
.ok();
debug!("Deleting the room's last notifications read.");
self.services
.user
.delete_room_notification_read(room_id)
.await
.log_err()
.ok();
debug!("Final stages of deleting the room");
debug!("Obtaining a mutex state lock for safety and future database operations");
let state_lock = self.services.state.mutex.lock(room_id).await;
debug!("Deleting room sync tokens from our database");
self.services
.user
.delete_room_synctokens(room_id)
.await
.log_err()
.ok();
debug!("Deleting room state hash from our database");
self.services
@@ -201,12 +199,6 @@ pub async fn delete_room(&self, room_id: &RoomId, state_lock: RoomMutexGuard) ->
.log_err()
.ok();
// TODO: add option to keep a room banned (`--block` or `--ban`)
self.services.metadata.enable_room(room_id);
self.services.metadata.unban_room(room_id);
drop(state_lock);
debug!("Successfully deleted room {room_id} from our database");
Ok(())
}
@@ -1,11 +1,11 @@
use futures::{StreamExt, TryFutureExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, RoomVersionId, ServerName,
events::TimelineEventType,
CanonicalJsonObject, EventId, RoomId, RoomVersionId, ServerName, events::TimelineEventType,
};
use tuwunel_core::{
Err, Result, debug, debug_info, err, implement,
matrix::{Event, PduEvent, event::TypeExt, room_version},
pdu::format::from_incoming_federation,
ref_at, state_res, trace,
utils::{future::TryExtExt, stream::IterStream},
warn,
@@ -66,12 +66,11 @@ pub(super) async fn handle_outlier_pdu(
},
};
// Now that we have checked the signature and hashes we can add the eventID and
// convert to our PduEvent type
pdu_json.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
let room_rules = room_version::rules(room_version)?;
let event = serde_json::from_value::<PduEvent>(serde_json::to_value(&pdu_json)?)
.map_err(|e| err!(Request(BadJson(debug_warn!("Event is not a valid PDU: {e}")))))?;
// Now that we have checked the signature and hashes we can make mutations and
// convert to our PduEvent type.
let event = from_incoming_federation(room_id, event_id, &mut pdu_json, &room_rules)?;
check_room_id(room_id, &event)?;
@@ -89,7 +88,6 @@ pub(super) async fn handle_outlier_pdu(
// auth events
debug!("Checking based on auth events");
let room_rules = room_version::rules(room_version)?;
let is_hydra = !room_rules
.event_format
.allow_room_create_in_auth_events;
@@ -9,7 +9,7 @@
#[implement(super::Service)]
pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
let value = serde_json::from_str::<CanonicalJsonObject>(pdu.get()).map_err(|e| {
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
err!(BadServerResponse(debug_error!("Error parsing incoming event: {e} {pdu:#?}")))
})?;
let room_id: OwnedRoomId = value
+3 -2
View File
@@ -360,7 +360,8 @@ async fn get_room_summary(
.services
.state_accessor
.get_avatar(room_id)
.map(|res| res.into_option().unwrap_or_default().url);
.map_ok(|content| content.url)
.ok();
let room_version = self.services.state.get_room_version(room_id).ok();
@@ -397,12 +398,12 @@ async fn get_room_summary(
let summary = SpaceHierarchyParentSummary {
children_state,
summary: RoomSummary {
avatar_url: avatar_url.flatten(),
canonical_alias,
name,
topic,
world_readable,
guest_can_join,
avatar_url,
room_type,
encryption,
room_version,
+3 -7
View File
@@ -8,7 +8,7 @@
use async_trait::async_trait;
use futures::{FutureExt, TryFutureExt, future::try_join};
use ruma::{
EventEncryptionAlgorithm, JsOption, OwnedRoomAliasId, RoomId, UserId,
EventEncryptionAlgorithm, OwnedRoomAliasId, RoomId, UserId,
events::{
StateEventType,
room::{
@@ -88,13 +88,9 @@ pub async fn get_name(&self, room_id: &RoomId) -> Result<String> {
.map(|c: RoomNameEventContent| c.name)
}
pub async fn get_avatar(&self, room_id: &RoomId) -> JsOption<RoomAvatarEventContent> {
let content = self
.room_state_get_content(room_id, &StateEventType::RoomAvatar, "")
pub async fn get_avatar(&self, room_id: &RoomId) -> Result<RoomAvatarEventContent> {
self.room_state_get_content(room_id, &StateEventType::RoomAvatar, "")
.await
.ok();
JsOption::from_option(content)
}
pub async fn get_member(
+6 -1
View File
@@ -138,12 +138,17 @@ pub async fn user_can_see_state_events(&self, user_id: &UserId, room_id: &RoomId
});
match history_visibility {
| HistoryVisibility::WorldReadable => true,
| HistoryVisibility::Shared =>
self.services
.state_cache
.once_joined(user_id, room_id)
.await,
| HistoryVisibility::Invited =>
self.services
.state_cache
.is_invited(user_id, room_id)
.await,
| HistoryVisibility::WorldReadable => true,
| _ => false,
}
}
+4 -1
View File
@@ -577,7 +577,7 @@ pub async fn is_left(&self, user_id: &UserId, room_id: &RoomId) -> bool {
#[implement(Service)]
#[tracing::instrument(skip(self), level = "trace")]
pub async fn delete_room_join_counts(&self, room_id: &RoomId) -> Result {
pub async fn delete_room_join_counts(&self, room_id: &RoomId, force: bool) -> Result {
let prefix = (room_id, Interfix);
self.db.roomid_knockedcount.remove(room_id);
@@ -648,6 +648,9 @@ pub async fn delete_room_join_counts(&self, room_id: &RoomId) -> Result {
.roomuserid_leftcount
.keys_prefix(&prefix)
.ignore_err()
.ready_filter(|(_, user_id): &(&RoomId, &UserId)| {
force || !self.services.globals.user_is_local(user_id)
})
.ready_for_each(|key: (&RoomId, &UserId)| {
trace!("Removing key: {key:?}");
self.db.roomuserid_leftcount.del(key);
+18 -25
View File
@@ -3,16 +3,16 @@
use futures::{StreamExt, TryStreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedEventId,
OwnedRoomId, RoomId, RoomVersionId, UserId,
OwnedRoomId, RoomId, UserId,
events::{StateEventType, TimelineEventType, room::create::RoomCreateEventContent},
room_version_rules::RoomIdFormatVersion,
uint,
};
use serde_json::value::to_raw_value;
use tuwunel_core::{
Err, Error, Result, err, implement,
Error, Result, err, implement,
matrix::{
event::{Event, StateKey, TypeExt, gen_event_id},
event::{Event, StateKey, TypeExt},
pdu::{EventHash, PduBuilder, PduEvent},
room_version,
state_res::{self},
@@ -175,36 +175,29 @@ pub async fn create_hash_and_sign_event(
err!(Request(BadJson(warn!("Failed to convert PDU to canonical JSON: {e}"))))
})?;
// room v3 and above removed the "event_id" field from remote PDU format
if !matches!(room_version, RoomVersionId::V1 | RoomVersionId::V2) {
pdu_json.remove("event_id");
}
// room v12 and above removed the placeholder "room_id" field from m.room.create
if matches!(version_rules.room_id_format, RoomIdFormatVersion::V2)
if !version_rules
.event_format
.require_room_create_room_id
&& pdu.kind == TimelineEventType::RoomCreate
{
pdu_json.remove("room_id");
}
if let Err(e) = self
pdu.event_id = self
.services
.server_keys
.hash_and_sign_event(&mut pdu_json, &room_version)
{
use ruma::signatures::Error::PduSize;
return match e {
| Error::Signatures(PduSize) => {
Err!(Request(TooLarge("Message/PDU is too long (exceeds 65535 bytes)")))
},
| _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))),
};
}
// Generate event id
pdu.event_id = gen_event_id(&pdu_json, &room_version)?;
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
.gen_id_hash_and_sign_event(&mut pdu_json, &room_version)
.map_err(|e| {
use Error::Signatures;
use ruma::signatures::Error::PduSize;
match e {
| Signatures(PduSize) => {
err!(Request(TooLarge("PDU exceeds 65535 bytes")))
},
| _ => err!(Request(Unknown(warn!("Signing event failed: {e}")))),
}
})?;
// Room id is event id for V12+
if matches!(version_rules.room_id_format, RoomIdFormatVersion::V2)
+44 -2
View File
@@ -1,8 +1,11 @@
use std::sync::Arc;
use ruma::{RoomId, UserId};
use tuwunel_core::{Result, implement};
use tuwunel_database::{Database, Deserialized, Map};
use tuwunel_core::{
Result, implement, trace,
utils::stream::{ReadyExt, TryIgnore},
};
use tuwunel_database::{Database, Deserialized, Interfix, Map};
use crate::rooms::short::ShortStateHash;
@@ -87,6 +90,24 @@ pub async fn last_notification_read(&self, user_id: &UserId, room_id: &RoomId) -
.unwrap_or(0)
}
#[implement(Service)]
pub async fn delete_room_notification_read(&self, room_id: &RoomId) -> Result {
let key = (room_id, Interfix);
self.db
.roomuserid_lastnotificationread
.keys_prefix_raw(&key)
.ignore_err()
.ready_for_each(|key| {
trace!("Removing key: {key:?}");
self.db
.roomuserid_lastnotificationread
.remove(key);
})
.await;
Ok(())
}
#[implement(Service)]
#[tracing::instrument(level = "trace", skip(self))]
pub async fn associate_token_shortstatehash(
@@ -128,3 +149,24 @@ pub async fn get_token_shortstatehash(
.await
.deserialized()
}
#[implement(Service)]
pub async fn delete_room_synctokens(&self, room_id: &RoomId) -> Result {
let shortroomid = self
.services
.short
.get_shortroomid(room_id)
.await?;
self.db
.roomsynctoken_shortstatehash
.keys_prefix_raw(&shortroomid)
.ignore_err()
.ready_for_each(|key| {
trace!("Removing key: {key:?}");
self.db.roomsynctoken_shortstatehash.remove(key);
})
.await;
Ok(())
}
+37
View File
@@ -33,6 +33,7 @@
AnySyncEphemeralRoomEvent, GlobalAccountDataEventType, push_rules::PushRulesEvent,
receipt::ReceiptType,
},
presence::PresenceState,
push,
serde::Raw,
uint,
@@ -834,6 +835,42 @@ async fn send_events_dest_push(
continue;
}
// optional suppression: heuristic combining presence age and recent sync
// activity.
if self.services.config.suppress_push_when_active
&& let Ok(presence) = self
.services
.presence
.get_presence(&user_id)
.await
{
let is_online = presence.content.presence == PresenceState::Online;
let presence_age_ms = presence
.content
.last_active_ago
.map(u64::from)
.unwrap_or(u64::MAX);
let sync_gap_ms = self
.services
.presence
.last_sync_gap_ms(&user_id)
.await;
let considered_active = is_online
&& presence_age_ms < 65_000
&& sync_gap_ms.is_some_and(|gap| gap < 32_000);
if considered_active {
trace!(
?user_id,
presence_age_ms, sync_gap_ms, "suppressing push: active heuristic"
);
continue;
}
}
let rules_for_user = self
.services
.account_data
+64 -12
View File
@@ -1,28 +1,71 @@
use ruma::{CanonicalJsonObject, RoomVersionId};
use tuwunel_core::{Result, err, implement};
use ruma::{CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomVersionId};
use tuwunel_core::{
Result, implement,
matrix::{event::gen_event_id, room_version},
};
#[implement(super::Service)]
pub fn sign_json(&self, object: &mut CanonicalJsonObject) -> Result {
use ruma::signatures::sign_json;
pub fn gen_id_hash_and_sign_event(
&self,
object: &mut CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
object.remove("event_id");
let server_name = self.services.globals.server_name().as_str();
sign_json(server_name, self.keypair(), object).map_err(Into::into)
if room_version::rules(room_version_id)?
.event_format
.require_event_id
{
self.gen_id_hash_and_sign_event_v1(object, room_version_id)
} else {
self.gen_id_hash_and_sign_event_v3(object, room_version_id)
}
}
#[implement(super::Service)]
fn gen_id_hash_and_sign_event_v1(
&self,
object: &mut CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
let event_id = gen_event_id(object, room_version_id)?;
object.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
self.services
.server_keys
.hash_and_sign_event(object, room_version_id)?;
Ok(event_id)
}
#[implement(super::Service)]
fn gen_id_hash_and_sign_event_v3(
&self,
object: &mut CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
self.services
.server_keys
.hash_and_sign_event(object, room_version_id)?;
let event_id = gen_event_id(object, room_version_id)?;
object.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
Ok(event_id)
}
#[implement(super::Service)]
pub fn hash_and_sign_event(
&self,
object: &mut CanonicalJsonObject,
room_version: &RoomVersionId,
room_version_id: &RoomVersionId,
) -> Result {
use ruma::signatures::hash_and_sign_event;
let server_name = &self.services.server.name;
let room_version_rules = room_version.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Cannot hash and sign event for unknown room version {room_version:?}."
)))
})?;
let room_version_rules = room_version::rules(room_version_id)?;
hash_and_sign_event(
server_name.as_str(),
@@ -32,3 +75,12 @@ pub fn hash_and_sign_event(
)
.map_err(Into::into)
}
#[implement(super::Service)]
pub fn sign_json(&self, object: &mut CanonicalJsonObject) -> Result {
use ruma::signatures::sign_json;
let server_name = self.services.globals.server_name().as_str();
sign_json(server_name, self.keypair(), object).map_err(Into::into)
}
+29 -28
View File
@@ -2,17 +2,21 @@
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomVersionId, signatures::Verified,
};
use serde_json::value::RawValue as RawJsonValue;
use tuwunel_core::{Err, Result, err, implement, matrix::event::gen_event_id_canonical_json};
use tuwunel_core::{
Err, Result, implement,
matrix::{event::gen_event_id_canonical_json, room_version},
};
#[implement(super::Service)]
pub async fn validate_and_add_event_id(
&self,
pdu: &RawJsonValue,
room_version: &RoomVersionId,
room_version_id: &RoomVersionId,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let (event_id, mut value) = gen_event_id_canonical_json(pdu, room_version)?;
let (event_id, mut value) = gen_event_id_canonical_json(pdu, room_version_id)?;
if let Err(e) = self
.verify_event(&value, Some(room_version))
.verify_event(&value, Some(room_version_id))
.await
{
return Err!(BadServerResponse(debug_error!(
@@ -20,7 +24,13 @@ pub async fn validate_and_add_event_id(
)));
}
value.insert("event_id".into(), CanonicalJsonValue::String(event_id.as_str().into()));
// For v3+ rooms we add the event_id, but for v1/v2 rooms it's already present.
if !room_version::rules(room_version_id)?
.event_format
.require_event_id
{
value.insert("event_id".into(), CanonicalJsonValue::String(event_id.as_str().into()));
}
Ok((event_id, value))
}
@@ -29,14 +39,10 @@ pub async fn validate_and_add_event_id(
pub async fn validate_and_add_event_id_no_fetch(
&self,
pdu: &RawJsonValue,
room_version: &RoomVersionId,
room_version_id: &RoomVersionId,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let (event_id, mut value) = gen_event_id_canonical_json(pdu, room_version)?;
let room_version_rules = room_version.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Cannot verify event for unknown room version {room_version:?}."
)))
})?;
let (event_id, mut value) = gen_event_id_canonical_json(pdu, room_version_id)?;
let room_version_rules = room_version::rules(room_version_id)?;
if !self
.required_keys_exist(&value, &room_version_rules)
@@ -48,7 +54,7 @@ pub async fn validate_and_add_event_id_no_fetch(
}
if let Err(e) = self
.verify_event(&value, Some(room_version))
.verify_event(&value, Some(room_version_id))
.await
{
return Err!(BadServerResponse(debug_error!(
@@ -56,7 +62,10 @@ pub async fn validate_and_add_event_id_no_fetch(
)));
}
value.insert("event_id".into(), CanonicalJsonValue::String(event_id.as_str().into()));
// For v3+ rooms we add the event_id, but for v1/v2 rooms it's already present.
if !room_version_rules.event_format.require_event_id {
value.insert("event_id".into(), CanonicalJsonValue::String(event_id.as_str().into()));
}
Ok((event_id, value))
}
@@ -65,14 +74,10 @@ pub async fn validate_and_add_event_id_no_fetch(
pub async fn verify_event(
&self,
event: &CanonicalJsonObject,
room_version: Option<&RoomVersionId>,
room_version_id: Option<&RoomVersionId>,
) -> Result<Verified> {
let room_version = room_version.unwrap_or(&RoomVersionId::V11);
let room_version_rules = room_version.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Cannot verify event for unknown room version {room_version:?}."
)))
})?;
let room_version_id = room_version_id.unwrap_or(&RoomVersionId::V11);
let room_version_rules = room_version::rules(room_version_id)?;
let event_keys = self
.get_event_keys(event, &room_version_rules)
@@ -85,14 +90,10 @@ pub async fn verify_event(
pub async fn verify_json(
&self,
event: &CanonicalJsonObject,
room_version: Option<&RoomVersionId>,
room_version_id: Option<&RoomVersionId>,
) -> Result {
let room_version = room_version.unwrap_or(&RoomVersionId::V11);
let room_version_rules = room_version.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Cannot verify json for unknown room version {room_version:?}."
)))
})?;
let room_version_id = room_version_id.unwrap_or(&RoomVersionId::V11);
let room_version_rules = room_version::rules(room_version_id)?;
let event_keys = self
.get_event_keys(event, &room_version_rules)
+176 -165
View File
@@ -5,8 +5,11 @@
sync::{Arc, Mutex, Mutex as StdMutex},
};
use ruma::{OwnedDeviceId, OwnedRoomId, OwnedUserId, api::client::sync::sync_events::v5};
use tuwunel_core::Result;
use ruma::{
OwnedDeviceId, OwnedRoomId, OwnedUserId,
api::client::sync::sync_events::v5::{Request, request},
};
use tuwunel_core::{Result, implement, smallstr::SmallString};
use tuwunel_database::Map;
pub struct Service {
@@ -31,17 +34,21 @@ pub struct Data {
userid_lastonetimekeyupdate: Arc<Map>,
}
#[derive(Default)]
#[derive(Debug, Default)]
struct SnakeSyncCache {
lists: BTreeMap<String, v5::request::List>,
subscriptions: BTreeMap<OwnedRoomId, v5::request::RoomSubscription>,
known_rooms: BTreeMap<String, BTreeMap<OwnedRoomId, u64>>,
extensions: v5::request::Extensions,
lists: BTreeMap<ListId, request::List>,
subscriptions: RoomSubscriptions,
known_rooms: KnownRooms,
extensions: request::Extensions,
}
type DbConnections<K, V> = Mutex<BTreeMap<K, V>>;
type SnakeConnectionsKey = (OwnedUserId, OwnedDeviceId, Option<String>);
pub type KnownRooms = BTreeMap<ListId, BTreeMap<OwnedRoomId, u64>>;
pub type RoomSubscriptions = BTreeMap<OwnedRoomId, request::RoomSubscription>;
pub type SnakeConnectionsKey = (OwnedUserId, OwnedDeviceId, Option<ConnId>);
type SnakeConnectionsVal = Arc<Mutex<SnakeSyncCache>>;
type DbConnections<K, V> = Mutex<BTreeMap<K, V>>;
pub type ListId = SmallString<[u8; 16]>;
pub type ConnId = SmallString<[u8; 16]>;
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
@@ -69,185 +76,189 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
pub fn snake_connection_cached(&self, key: &SnakeConnectionsKey) -> bool {
self.snake_connections
.lock()
.expect("locked")
.contains_key(key)
}
#[implement(Service)]
pub fn update_snake_sync_request_with_cache(
&self,
snake_key: &SnakeConnectionsKey,
request: &mut Request,
) -> KnownRooms {
let mut cache = self.snake_connections.lock().expect("locked");
let cached = Arc::clone(
cache
.entry(snake_key.clone())
.or_insert_with(|| Arc::new(Mutex::new(SnakeSyncCache::default()))),
);
pub fn forget_snake_sync_connection(&self, key: &SnakeConnectionsKey) {
self.snake_connections
.lock()
.expect("locked")
.remove(key);
}
let cached = &mut cached.lock().expect("locked");
drop(cache);
pub fn update_snake_sync_request_with_cache(
&self,
snake_key: &SnakeConnectionsKey,
request: &mut v5::Request,
) -> BTreeMap<String, BTreeMap<OwnedRoomId, u64>> {
let mut cache = self.snake_connections.lock().expect("locked");
let cached = Arc::clone(
cache
.entry(snake_key.clone())
.or_insert_with(|| Arc::new(Mutex::new(SnakeSyncCache::default()))),
);
let cached = &mut cached.lock().expect("locked");
drop(cache);
//Request::try_from_http_request(req, path_args);
for (list_id, list) in &mut request.lists {
if let Some(cached_list) = cached.lists.get(list_id.as_str()) {
list_or_sticky(
&mut list.room_details.required_state,
&cached_list.room_details.required_state,
);
//v5::Request::try_from_http_request(req, path_args);
for (list_id, list) in &mut request.lists {
if let Some(cached_list) = cached.lists.get(list_id) {
list_or_sticky(
&mut list.room_details.required_state,
&cached_list.room_details.required_state,
);
//some_or_sticky(&mut list.include_heroes, cached_list.include_heroes);
//some_or_sticky(&mut list.include_heroes, cached_list.include_heroes);
match (&mut list.filters, cached_list.filters.clone()) {
| (Some(filters), Some(cached_filters)) => {
some_or_sticky(&mut filters.is_invite, cached_filters.is_invite);
// TODO (morguldir): Find out how a client can unset this, probably need
// to change into an option inside ruma
list_or_sticky(
&mut filters.not_room_types,
&cached_filters.not_room_types,
);
},
| (_, Some(cached_filters)) => list.filters = Some(cached_filters),
| (Some(list_filters), _) => list.filters = Some(list_filters.clone()),
| (..) => {},
}
match (&mut list.filters, cached_list.filters.clone()) {
| (Some(filters), Some(cached_filters)) => {
some_or_sticky(&mut filters.is_invite, cached_filters.is_invite);
// TODO (morguldir): Find out how a client can unset this, probably need
// to change into an option inside ruma
list_or_sticky(&mut filters.not_room_types, &cached_filters.not_room_types);
},
| (_, Some(cached_filters)) => list.filters = Some(cached_filters),
| (Some(list_filters), _) => list.filters = Some(list_filters.clone()),
| (..) => {},
}
cached.lists.insert(list_id.clone(), list.clone());
}
cached
.subscriptions
.extend(request.room_subscriptions.clone());
request
.room_subscriptions
.extend(cached.subscriptions.clone());
request.extensions.e2ee.enabled = request
.extensions
.e2ee
.enabled
.or(cached.extensions.e2ee.enabled);
request.extensions.to_device.enabled = request
.extensions
.to_device
.enabled
.or(cached.extensions.to_device.enabled);
request.extensions.account_data.enabled = request
.extensions
.account_data
.enabled
.or(cached.extensions.account_data.enabled);
request.extensions.account_data.lists = request
.extensions
.account_data
.lists
.clone()
.or_else(|| cached.extensions.account_data.lists.clone());
request.extensions.account_data.rooms = request
.extensions
.account_data
.rooms
.clone()
.or_else(|| cached.extensions.account_data.rooms.clone());
some_or_sticky(&mut request.extensions.typing.enabled, cached.extensions.typing.enabled);
some_or_sticky(
&mut request.extensions.typing.rooms,
cached.extensions.typing.rooms.clone(),
);
some_or_sticky(
&mut request.extensions.typing.lists,
cached.extensions.typing.lists.clone(),
);
some_or_sticky(
&mut request.extensions.receipts.enabled,
cached.extensions.receipts.enabled,
);
some_or_sticky(
&mut request.extensions.receipts.rooms,
cached.extensions.receipts.rooms.clone(),
);
some_or_sticky(
&mut request.extensions.receipts.lists,
cached.extensions.receipts.lists.clone(),
);
cached.extensions = request.extensions.clone();
cached.known_rooms.clone()
.insert(list_id.as_str().into(), list.clone());
}
pub fn update_snake_sync_known_rooms(
&self,
key: &SnakeConnectionsKey,
list_id: String,
new_cached_rooms: BTreeSet<OwnedRoomId>,
globalsince: u64,
) {
assert!(key.2.is_some(), "Some(conn_id) required for this call");
let mut cache = self.snake_connections.lock().expect("locked");
let cached = Arc::clone(
cache
.entry(key.clone())
.or_insert_with(|| Arc::new(Mutex::new(SnakeSyncCache::default()))),
);
let cached = &mut cached.lock().expect("locked");
drop(cache);
cached
.subscriptions
.extend(request.room_subscriptions.clone());
for (room_id, lastsince) in cached
.known_rooms
.entry(list_id.clone())
.or_default()
.iter_mut()
{
if !new_cached_rooms.contains(room_id) {
*lastsince = 0;
}
}
let list = cached.known_rooms.entry(list_id).or_default();
for room_id in new_cached_rooms {
list.insert(room_id, globalsince);
request
.room_subscriptions
.extend(cached.subscriptions.clone());
request.extensions.e2ee.enabled = request
.extensions
.e2ee
.enabled
.or(cached.extensions.e2ee.enabled);
request.extensions.to_device.enabled = request
.extensions
.to_device
.enabled
.or(cached.extensions.to_device.enabled);
request.extensions.account_data.enabled = request
.extensions
.account_data
.enabled
.or(cached.extensions.account_data.enabled);
request.extensions.account_data.lists = request
.extensions
.account_data
.lists
.clone()
.or_else(|| cached.extensions.account_data.lists.clone());
request.extensions.account_data.rooms = request
.extensions
.account_data
.rooms
.clone()
.or_else(|| cached.extensions.account_data.rooms.clone());
{
let (request, cached) = (&mut request.extensions.typing, &cached.extensions.typing);
some_or_sticky(&mut request.enabled, cached.enabled);
some_or_sticky(&mut request.rooms, cached.rooms.clone());
some_or_sticky(&mut request.lists, cached.lists.clone());
};
{
let (request, cached) = (&mut request.extensions.receipts, &cached.extensions.receipts);
some_or_sticky(&mut request.enabled, cached.enabled);
some_or_sticky(&mut request.rooms, cached.rooms.clone());
some_or_sticky(&mut request.lists, cached.lists.clone());
};
cached.extensions = request.extensions.clone();
cached.known_rooms.clone()
}
#[implement(Service)]
pub fn update_snake_sync_known_rooms(
&self,
key: &SnakeConnectionsKey,
list_id: ListId,
new_cached_rooms: BTreeSet<OwnedRoomId>,
globalsince: u64,
) {
assert!(key.2.is_some(), "Some(conn_id) required for this call");
let mut cache = self.snake_connections.lock().expect("locked");
let cached = Arc::clone(
cache
.entry(key.clone())
.or_insert_with(|| Arc::new(Mutex::new(SnakeSyncCache::default()))),
);
let cached = &mut cached.lock().expect("locked");
drop(cache);
for (room_id, lastsince) in cached
.known_rooms
.entry(list_id.clone())
.or_default()
.iter_mut()
{
if !new_cached_rooms.contains(room_id) {
*lastsince = 0;
}
}
pub fn update_snake_sync_subscriptions(
&self,
key: &SnakeConnectionsKey,
subscriptions: BTreeMap<OwnedRoomId, v5::request::RoomSubscription>,
) {
let mut cache = self.snake_connections.lock().expect("locked");
let cached = Arc::clone(
cache
.entry(key.clone())
.or_insert_with(|| Arc::new(Mutex::new(SnakeSyncCache::default()))),
);
let cached = &mut cached.lock().expect("locked");
drop(cache);
cached.subscriptions = subscriptions;
let list = cached.known_rooms.entry(list_id).or_default();
for room_id in new_cached_rooms {
list.insert(room_id, globalsince);
}
}
#[implement(Service)]
pub fn update_snake_sync_subscriptions(
&self,
key: &SnakeConnectionsKey,
subscriptions: RoomSubscriptions,
) {
let mut cache = self.snake_connections.lock().expect("locked");
let cached = Arc::clone(
cache
.entry(key.clone())
.or_insert_with(|| Arc::new(Mutex::new(SnakeSyncCache::default()))),
);
let cached = &mut cached.lock().expect("locked");
drop(cache);
cached.subscriptions = subscriptions;
}
#[implement(Service)]
pub fn forget_snake_sync_connection(&self, key: &SnakeConnectionsKey) {
self.snake_connections
.lock()
.expect("locked")
.remove(key);
}
#[implement(Service)]
pub fn snake_connection_cached(&self, key: &SnakeConnectionsKey) -> bool {
self.snake_connections
.lock()
.expect("locked")
.contains_key(key)
}
#[inline]
pub fn into_snake_key<U, D, C>(user_id: U, device_id: D, conn_id: C) -> SnakeConnectionsKey
pub fn into_snake_key<U, D, C>(
user_id: U,
device_id: D,
conn_id: Option<C>,
) -> SnakeConnectionsKey
where
U: Into<OwnedUserId>,
D: Into<OwnedDeviceId>,
C: Into<Option<String>>,
C: Into<ConnId>,
{
(user_id.into(), device_id.into(), conn_id.into())
(user_id.into(), device_id.into(), conn_id.map(Into::into))
}
/// load params from cache if body doesn't contain it, as long as it's allowed
+27
View File
@@ -152,9 +152,36 @@ pub async fn count_one_time_keys(
})
.await;
let total = algorithm_counts
.values()
.copied()
.map(TryInto::try_into)
.filter_map(Result::ok)
.fold(0_usize, usize::saturating_add);
if total > self.services.config.one_time_key_limit {
self.prune_one_time_keys(user_id, device_id).await;
}
algorithm_counts
}
#[implement(super::Service)]
pub async fn prune_one_time_keys(&self, user_id: &UserId, device_id: &DeviceId) {
use tuwunel_database::keyval::Key;
let query = (user_id, device_id);
self.db
.onetimekeyid_onetimekeys
.keys_prefix(&query)
.ignore_err()
.skip(self.services.config.one_time_key_limit)
.ready_for_each(|key: Key<'_>| {
self.db.onetimekeyid_onetimekeys.remove(key);
})
.await;
}
#[implement(super::Service)]
pub async fn add_device_keys(
&self,
@@ -328,8 +328,6 @@
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent"}
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent"}
{"Action":"fail","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms"}
{"Action":"fail","Test":"TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph"}
{"Action":"fail","Test":"TestMSC4297StateResolutionV2_1_starts_from_empty_set"}
{"Action":"fail","Test":"TestMSC4311FullCreateEventOnStrippedState"}
{"Action":"pass","Test":"TestMediaConfig"}
{"Action":"pass","Test":"TestMediaFilenames"}
+53 -5
View File
@@ -214,9 +214,10 @@
# Minimum time-to-live in seconds for entries in the DNS cache. The
# default may appear high to most administrators; this is by design as the
# majority of NXDOMAINs are correct for a long time (e.g. the server is no
# longer running Matrix). Only decrease this if you are using an external
# DNS cache.
# exotic loads of federating to many other servers require a higher TTL
# than many domains have set. Even when using an external DNS cache the
# problem is shifted to that cache which is ignorant of its role for
# this application and can adhere to many low TTL's increasing its load.
#
#dns_min_ttl = 10800
@@ -284,6 +285,23 @@
#
#ip_lookup_strategy = 5
# List of domain patterns resolved via the alternative path without any
# persistent cache, very small memory cache, and no enforced TTL. This
# is intended for internal network and application services which require
# these specific properties. This path does not support federation or
# general purposes.
#
# example: ["*\.dns\.podman$"]
#
#dns_passthru_domains = []
# Whether to resolve appservices via the alternative path; setting this is
# superior to providing domains in `dns_passthru_domains` if all
# appservices intend to be matched anyway. The overhead of matching regex
# and maintaining the list of domains can be avoided.
#
#dns_passthru_appservices = false
# Max request size for file uploads in bytes. Defaults to 20MB.
#
#max_request_size = 20971520
@@ -434,10 +452,21 @@
#allow_encryption = true
# Controls whether federation is allowed or not. It is not recommended to
# disable this after the fact due to potential federation breakage.
# disable this after installation due to potential federation breakage but
# this is technically not a permanent setting.
#
#allow_federation = true
# Sets the default `m.federate` property for newly created rooms when the
# client does not request one. If `allow_federation` is set to false at
# the same this value is set to false it then always overrides the client
# requested `m.federate` value to false.
#
# Rooms are fixed to the setting at the time of their creation and can
# never be changed; changing this value only affects new rooms.
#
#federate_created_rooms = true
# Allows federation requests to be made to itself
#
# This isn't intended and is very likely a bug if federation requests are
@@ -1093,6 +1122,17 @@
#
#presence_timeout_remote_users = true
# Suppresses push notifications for users marked as active. (Experimental)
#
# When enabled, users with `Online` presence and recent activity
# (based on presence state and sync activity) wont receive push
# notifications, reducing duplicate alerts while they're active
# on another client.
#
# Disabled by default to preserve legacy behavior.
#
#suppress_push_when_active = false
# Allow receiving incoming read receipts from remote servers.
#
#allow_incoming_read_receipts = true
@@ -1665,7 +1705,7 @@
# jeopardize existing rooms, but otherwise will default to true at the
# next point release or patch.
#
#hydra_backports = false
#hydra_backports = true
# Delete rooms when the last user from this server leaves. This feature is
# experimental and for the purpose of least-surprise is not enabled by
@@ -1678,6 +1718,14 @@
#
#delete_rooms_after_leave = false
# Limits the number of One Time Keys per device (not per-algorithm). The
# reference implementation maintains 50 OTK's at any given time, therefor
# our default is at least five times that. There is no known reason for an
# administrator to adjust this value; it is provided here rather than
# hardcoding it.
#
#one_time_key_limit = 256
#[global.tls]
# Path to a valid TLS certificate file.