mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-08-07 05:09:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4dfafe2d | ||
|
|
8dbea51968 | ||
|
|
7b5905bbca | ||
|
|
42d143c013 | ||
|
|
10fb1cd192 | ||
|
|
1322ba1b00 | ||
|
|
d73b4332bf | ||
|
|
11eeca7e01 | ||
|
|
19dc5fafd4 | ||
|
|
54b347b855 | ||
|
|
8ed61aecb0 | ||
|
|
bf8aa57d03 | ||
|
|
a9f1926654 | ||
|
|
248d778290 | ||
|
|
2e13e87e43 | ||
|
|
ba19d407d0 | ||
|
|
de7c5dcbc8 | ||
|
|
adadafa88f | ||
|
|
deda746222 | ||
|
|
29c2c8a333 | ||
|
|
296018f0cc | ||
|
|
6295ca135a | ||
|
|
b5a9884194 | ||
|
|
1be7fd9247 | ||
|
|
be1264965a | ||
|
|
f6d2ce2f22 | ||
|
|
3cbe1e25cd | ||
|
|
897322964c | ||
|
|
996a5488be | ||
|
|
0ed4bd1e07 | ||
|
|
da79de5381 | ||
|
|
1bcf3ae19a | ||
|
|
91e500efe3 | ||
|
|
35b1ebaf3b | ||
|
|
ea5deb7e85 | ||
|
|
542cc51047 | ||
|
|
3225db0ddd | ||
|
|
a164da8b58 | ||
|
|
7221d466ce | ||
|
|
f6c88e3a16 | ||
|
|
3deebeab78 | ||
|
|
1d2818de58 | ||
|
|
86781522b6 | ||
|
|
4b49aaad53 | ||
|
|
3a78ba2b16 | ||
|
|
685946faed | ||
|
|
13c9385ef7 | ||
|
|
6a583a359e | ||
|
|
eec6e45358 | ||
|
|
804257eb52 | ||
|
|
a1677f05d3 | ||
|
|
1f77200299 | ||
|
|
888b93df91 |
@@ -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"},
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+10
-18
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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": "",
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../CODE_OF_CONDUCT.md
|
||||
+1
-1
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -60,5 +60,8 @@ pub(super) enum RoomCommand {
|
||||
/// - Delete room
|
||||
DeleteRoom {
|
||||
room_id: OwnedRoomId,
|
||||
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<_>) =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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([(
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
mod appservice;
|
||||
mod jwt;
|
||||
pub(crate) mod jwt;
|
||||
mod ldap;
|
||||
mod logout;
|
||||
mod password;
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"))),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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) won’t 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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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),)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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) won’t 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.
|
||||
|
||||
Reference in New Issue
Block a user