Compare commits

...

43 Commits

Author SHA1 Message Date
Ginger
e5b11af3e8 fix: Allow cargo_common_metadata clippy lint 2026-01-08 14:00:02 -05:00
Ginger
71a26e433f fix: Update package and crate metadata 2026-01-08 00:21:28 +00:00
Jade Ellis
d353446488 fix: Incomplete rename 2026-01-07 23:48:04 +00:00
timedout
77e8fd1744 style: Use contains to check for row presence 2026-01-07 17:31:54 +00:00
timedout
7fa7b129c0 perf: Store empty value (row only needs to exist) 2026-01-07 17:31:54 +00:00
timedout
247bc15659 fix: Await future 2026-01-07 17:31:53 +00:00
timedout
88a35e139d fix: Correctly return M_USER_LOCKED during login 2026-01-07 17:31:53 +00:00
timedout
37574ef5cc chore: Add news fragment 2026-01-07 17:31:53 +00:00
timedout
1c816850ed feat: Allow admins to disable the login capability of an account
# Conflicts:
#	src/admin/user/commands.rs
2026-01-07 17:31:51 +00:00
timedout
3483059e1c fix: Unawaited future causing build error 2026-01-07 14:33:37 +00:00
Ginger
d865dd4454 feat(!783): Add --once shortcut flag to issue command 2026-01-07 14:22:37 +00:00
Ginger
adc7c5ac49 fix(!783): Don't allow registrations by default with no token configured 2026-01-07 14:22:37 +00:00
Ginger
112403e470 chore(!783): Remove config file check for no static token or captcha 2026-01-07 14:22:37 +00:00
Ginger
ea0a124981 chore(!783): Update config file documentation, depluralize token subcommand 2026-01-07 14:22:37 +00:00
Ginger
bf205fb13c chore(!783): Note that registration_token_file is gone 2026-01-07 14:22:37 +00:00
Ginger
9a6408f98f chore(!783): News fragment 2026-01-07 14:22:37 +00:00
Ginger
ca77970ff3 feat(!783): Add admin commands for managing tokens 2026-01-07 14:22:37 +00:00
Ginger
42f4ec34cd feat(!783): Initial implementation
Adds support for extra limited-use registration tokens
stored in the database, and a new service to manage them.
2026-01-07 14:22:37 +00:00
Renovate Bot
ecf74bb31f chore(deps): update dependency lddtree to 0.4.0 2026-01-06 20:38:12 +00:00
timedout
8c716befdc chore: Add news fragment 2026-01-06 20:32:52 +00:00
timedout
a8209d1dd9 feat: Add command to forcefully log out all of a user's devices 2026-01-06 20:28:23 +00:00
Jade Ellis
9552dd7485 style: Log error 2026-01-06 01:55:52 +00:00
Ginger
88c84f221f chore: Add comment and warning to unhappy path 2026-01-06 00:59:32 +00:00
Laurențiu Nicola
a10bd71945 fix(admin): fix force-leaving rooms with no left_state PDU 2026-01-06 00:59:31 +00:00
Jade Ellis
2f11bf4d74 docs: Document image mirrors 2026-01-06 00:45:12 +00:00
Jade Ellis
1e8748d1a0 docs: Fix some issues 2026-01-06 00:45:12 +00:00
Tom Foster
70ef6e4211 docs: Document maxperf Docker image variants from #1017
Add documentation for the new performance-optimised Docker images with
"-maxperf" suffix. These use the release-max-perf build profile with LTO
and target haswell CPU architecture on amd64 for optimal performance.

Also restructure the static prebuilt binary section in generic deployment
docs for better clarity and fix various UK English spelling issues.
2026-01-06 00:45:12 +00:00
Renovate Bot
212c1bc14d chore(deps): update github-actions-non-major 2026-01-06 00:24:55 +00:00
timedout
ce46b6869f chore: Bump dependencies to fix request errors 2026-01-05 20:10:30 +00:00
timedout
a18b8254d0 chore: Add news fragment 2026-01-05 20:10:30 +00:00
timedout
279f7cbfe4 style: Fix failing lints 2026-01-05 20:10:29 +00:00
timedout
006c57face perf: Don't check accept_make_join twice for restricted make_join 2026-01-05 20:10:29 +00:00
timedout
d52e0dc014 fix: Apply check_all_joins to make_join 2026-01-05 20:10:29 +00:00
timedout
4b873a1b95 fix: Apply spam checker to local restricted joins 2026-01-05 20:10:29 +00:00
timedout
76865e6f91 fix: Accept_may_join callback works again 2026-01-05 20:10:29 +00:00
timedout
99f16c2dfc fix: Call user_may_join_room later in the join process 2026-01-05 20:10:28 +00:00
timedout
5ac82f36f3 feat: Consolidate antispam checks into a service
Also adds support for the spam checker join rule, and Draupnir callbacks
2026-01-05 20:10:28 +00:00
timedout
c249dd992e feat: Add support for automatically rejecting pending invites 2026-01-05 20:10:28 +00:00
timedout
0956779802 feat: Add Meowlnir invite interception support
Co-authored-by: Jade Ellis <jade@ellis.link>
2026-01-05 20:10:27 +00:00
timedout
a83c1f1513 fix: Restrict suspend+lock commands to admin room
Also prevent locking the service user or admin users
2026-01-05 19:49:12 +00:00
timedout
8b5e4d8fe1 chore: Add news fragment 2026-01-05 19:34:21 +00:00
timedout
7502a944d7 feat: Add user locking and unlocking commands and functionality
Also corrects the response code returned by UserSuspended
2026-01-05 19:30:16 +00:00
Jade Ellis
aed15f246a refactor: Clean up logging issues
Primary issues: Double escapes (debug fmt), spans without levels
2026-01-05 18:28:57 +00:00
112 changed files with 1692 additions and 553 deletions

View File

@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:42.11.0@sha256:656c1e5b808279eac16c37b89562fb4c699e02fc7e219244f4a1fc2f0a7ce367
image: ghcr.io/renovatebot/renovate:42.70.2@sha256:3c2ac1b94fa92ef2fa4d1a0493f2c3ba564454720a32fdbcac2db2846ff1ee47
options: --tmpfs /tmp:exec
steps:
- name: Checkout

View File

@@ -23,7 +23,7 @@ jobs:
persist-credentials: true
token: ${{ secrets.FORGEJO_TOKEN }}
- uses: https://github.com/cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0
- uses: https://github.com/cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with:
nix_path: nixpkgs=channel:nixos-unstable

58
Cargo.lock generated
View File

@@ -1632,6 +1632,16 @@ dependencies = [
"litrs",
]
[[package]]
name = "draupnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"ruma-common",
"serde",
"serde_json",
]
[[package]]
name = "dtor"
version = "0.1.0"
@@ -1750,7 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2405,7 +2415,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.1",
"tokio",
"tower-service",
"tracing",
@@ -2982,6 +2992,16 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "meowlnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"ruma-common",
"serde",
"serde_json",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -3121,7 +3141,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -3749,7 +3769,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -3786,9 +3806,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.1",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -4065,11 +4085,13 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
[[package]]
name = "ruma"
version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"assign",
"draupnir-antispam",
"js_int",
"js_option",
"meowlnir-antispam",
"ruma-appservice-api",
"ruma-client-api",
"ruma-common",
@@ -4085,7 +4107,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"js_int",
"ruma-common",
@@ -4097,7 +4119,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"as_variant",
"assign",
@@ -4120,7 +4142,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -4152,7 +4174,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"as_variant",
"indexmap",
@@ -4177,7 +4199,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"bytes",
"headers",
@@ -4199,7 +4221,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"js_int",
"thiserror 2.0.17",
@@ -4208,7 +4230,7 @@ dependencies = [
[[package]]
name = "ruma-identity-service-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"js_int",
"ruma-common",
@@ -4218,7 +4240,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -4233,7 +4255,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"js_int",
"ruma-common",
@@ -4245,7 +4267,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
@@ -4325,7 +4347,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]

View File

@@ -1,26 +1,17 @@
#cargo-features = ["profile-rustflags"]
[workspace]
resolver = "2"
members = ["src/*", "xtask/*"]
default-members = ["src/*"]
[workspace.package]
authors = [
"June Clementine Strawberry <june@girlboss.ceo>",
"strawberry <strawberry@puppygock.gay>", # woof
"Jason Volk <jason@zemos.net>",
]
categories = ["network-programming"]
description = "a very cool Matrix chat homeserver written in Rust"
authors = ["Continuwuity Team and contributors <team@continuwuity.org>"]
description = "A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."
edition = "2024"
homepage = "https://continuwuity.org/"
keywords = ["chat", "matrix", "networking", "server", "uwu"]
license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
rust-version = "1.86.0"
version = "0.5.1"
[workspace.metadata.crane]
@@ -33,11 +24,11 @@ features = ["serde"]
[workspace.dependencies.smallvec]
version = "1.14.0"
features = [
"const_generics",
"const_new",
"serde",
"union",
"write",
"const_generics",
"const_new",
"serde",
"union",
"write",
]
[workspace.dependencies.smallstr]
@@ -96,13 +87,13 @@ version = "1.11.1"
version = "0.7.9"
default-features = false
features = [
"form",
"http1",
"http2",
"json",
"matched-path",
"tokio",
"tracing",
"form",
"http1",
"http2",
"json",
"matched-path",
"tokio",
"tracing",
]
[workspace.dependencies.axum-extra]
@@ -149,10 +140,10 @@ features = ["aws_lc_rs"]
version = "0.12.15"
default-features = false
features = [
"rustls-tls-native-roots",
"socks",
"hickory-dns",
"http2",
"rustls-tls-native-roots",
"socks",
"hickory-dns",
"http2",
]
[workspace.dependencies.serde]
@@ -188,18 +179,18 @@ default-features = false
version = "0.25.5"
default-features = false
features = [
"jpeg",
"png",
"gif",
"webp",
"jpeg",
"png",
"gif",
"webp",
]
[workspace.dependencies.blurhash]
version = "0.2.3"
default-features = false
features = [
"fast-linear-to-srgb",
"image",
"fast-linear-to-srgb",
"image",
]
# logging
@@ -229,13 +220,13 @@ default-features = false
version = "4.5.35"
default-features = false
features = [
"derive",
"env",
"error-context",
"help",
"std",
"string",
"usage",
"derive",
"env",
"error-context",
"help",
"std",
"string",
"usage",
]
[workspace.dependencies.futures]
@@ -247,15 +238,15 @@ features = ["std", "async-await"]
version = "1.44.2"
default-features = false
features = [
"fs",
"net",
"macros",
"sync",
"signal",
"time",
"rt-multi-thread",
"io-util",
"tracing",
"fs",
"net",
"macros",
"sync",
"signal",
"time",
"rt-multi-thread",
"io-util",
"tracing",
]
[workspace.dependencies.tokio-metrics]
@@ -280,18 +271,18 @@ default-features = false
version = "1.6.0"
default-features = false
features = [
"server",
"http1",
"http2",
"server",
"http1",
"http2",
]
[workspace.dependencies.hyper-util]
version = "=0.1.17"
default-features = false
features = [
"server-auto",
"server-graceful",
"tokio",
"server-auto",
"server-graceful",
"tokio",
]
# to support multiple variations of setting a config option
@@ -310,9 +301,9 @@ features = ["env", "toml"]
version = "0.25.1"
default-features = false
features = [
"serde",
"system-config",
"tokio",
"serde",
"system-config",
"tokio",
]
# Used for conduwuit::Error type
@@ -351,7 +342,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
rev = "27abe0dcd33fd4056efc94bab3582646b31b6ce9"
rev = "79abd5d331bca596b7f37e367a9f2cebccd9f64d"
features = [
"compat",
"rand",
@@ -381,13 +372,13 @@ features = [
"unstable-msc4095",
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4155",
"unstable-msc4155",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-extensible-events",
"unstable-pdu",
"unstable-msc4155"
"unstable-msc4155"
]
[workspace.dependencies.rust-rocksdb]
@@ -395,11 +386,11 @@ git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
default-features = false
features = [
"multi-threaded-cf",
"mt_static",
"lz4",
"zstd",
"bzip2",
"multi-threaded-cf",
"mt_static",
"lz4",
"zstd",
"bzip2",
]
[workspace.dependencies.sha2]
@@ -458,16 +449,16 @@ git = "https://forgejo.ellis.link/continuwuation/jemallocator"
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
default-features = false
features = [
"background_threads_runtime_support",
"unprefixed_malloc_on_supported_platforms",
"background_threads_runtime_support",
"unprefixed_malloc_on_supported_platforms",
]
[workspace.dependencies.tikv-jemallocator]
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
default-features = false
features = [
"background_threads_runtime_support",
"unprefixed_malloc_on_supported_platforms",
"background_threads_runtime_support",
"unprefixed_malloc_on_supported_platforms",
]
[workspace.dependencies.tikv-jemalloc-ctl]
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
@@ -491,9 +482,9 @@ default-features = false
version = "0.1.2"
default-features = false
features = [
"static",
"gcc",
"light",
"static",
"gcc",
"light",
]
[workspace.dependencies.rustyline-async]
@@ -848,6 +839,8 @@ unknown_lints = "allow"
###################
cargo = { level = "warn", priority = -1 }
# Nobody except for us should be consuming these crates, they don't need metadata
cargo_common_metadata = { level = "allow"}
## some sadness
multiple_crate_versions = { level = "allow", priority = 1 }

2
changelog.d/1263.feature Normal file
View File

@@ -0,0 +1,2 @@
Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
Contributed by @nex.

1
changelog.d/1266.feature Normal file
View File

@@ -0,0 +1 @@
Implemented account locking functionality, to complement user suspension. Contributed by @nex.

1
changelog.d/1271.feature Normal file
View File

@@ -0,0 +1 @@
Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex.

1
changelog.d/1272.feature Normal file
View File

@@ -0,0 +1 @@
Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex.

View File

@@ -0,0 +1 @@
Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**.

View File

@@ -421,7 +421,7 @@
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
#
# If you would like registration only via token reg, please configure
# `registration_token` or `registration_token_file`.
# `registration_token`.
#
#allow_registration = false
@@ -452,22 +452,13 @@
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
# to true to allow open registration without any conditions.
#
# YOU NEED TO EDIT THIS OR USE registration_token_file.
# If you do not want to set a static token, the `!admin token` commands
# may also be used to manage registration tokens.
#
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
#
#registration_token =
# Path to a file on the system that gets read for additional registration
# tokens. Multiple tokens can be added if you separate them with
# whitespace
#
# continuwuity must be able to access the file, and it must not be empty
#
# example: "/etc/continuwuity/.reg_token"
#
#registration_token_file =
# The public site key for reCaptcha. If this is provided, reCaptcha
# becomes required during registration. If both captcha *and*
# registration token are enabled, both will be required during
@@ -1647,7 +1638,7 @@
# Enable the tokio-console. This option is only relevant to developers.
#
# For more information, see:
# For more information, see:
# https://continuwuity.org/development.html#debugging-with-tokio-console
#
#tokio_console = false
@@ -1923,3 +1914,41 @@
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
#
#admin_filter = ""
[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =
# The authentication secret defined in antispam->secret. Required for
# continuwuity to talk to Meowlnir.
#
#secret =
# The management room for which to send requests
#
#management_room =
# If enabled run all federated join attempts (both federated and local)
# through the Meowlnir anti-spam checks.
#
# By default, only join attempts for rooms with the `fi.mau.spam_checker`
# restricted join rule are checked.
#
#check_all_joins = false
[global.antispam.draupnir]
# The base URL on which to contact Draupnir (before /api/).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =
# The authentication secret defined in
# web->synapseHTTPAntispam->authorization
#
#secret =

View File

@@ -52,7 +52,7 @@ ENV BINSTALL_VERSION=1.16.6
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.3.7
ENV LDDTREE_VERSION=0.4.0
# renovate: datasource=crate depName=timelord-cli
ENV TIMELORD_VERSION=3.0.1

View File

@@ -22,7 +22,7 @@ ENV BINSTALL_VERSION=1.16.6
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.3.7
ENV LDDTREE_VERSION=0.4.0
# Install unpackaged tools
RUN <<EOF

View File

@@ -11,10 +11,10 @@ ### Use a registry
| Registry | Image | Notes |
| --------------- | --------------------------------------------------------------- | -----------------------|
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest][fj] | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main][fj] | Main branch image. |
[fj]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | Performance optimised version. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | Performance optimised version. |
Use
@@ -24,6 +24,15 @@ ### Use a registry
to pull it to your machine.
#### Mirrors
Images are mirrored to multiple locations automatically, on a schedule:
- `ghcr.io/continuwuity/continuwuity`
- `docker.io/jadedblueeyes/continuwuity`
- `registry.gitlab.com/continuwuity/continuwuity`
- `git.nexy7574.co.uk/mirrored/continuwuity` (releases only, no `main`)
### Run
When you have the image, you can simply run it with
@@ -49,7 +58,7 @@ ### Run
flag, which cleans up everything related to your container after you stop
it.
### Docker-compose
### Docker Compose
If the `docker run` command is not suitable for you or your setup, you can also use one
of the provided `docker-compose` files.
@@ -158,8 +167,19 @@ # Build for the current platform and load into the local Docker daemon
# Example: Build for specific platforms and push to a registry.
# docker buildx build --platform linux/amd64,linux/arm64 --tag registry.io/org/continuwuity:latest -f docker/Dockerfile . --push
# Example: Build binary optimized for the current CPU
# docker buildx build --load --tag continuwuity:latest --build-arg TARGET_CPU=native -f docker/Dockerfile .
# Example: Build binary optimised for the current CPU (standard release profile)
# docker buildx build --load \
# --tag continuwuity:latest \
# --build-arg TARGET_CPU=native \
# -f docker/Dockerfile .
# Example: Build maxperf variant (release-max-perf profile with LTO)
# Optimised for runtime performance and smaller binary size, but requires longer build time
# docker buildx build --load \
# --tag continuwuity:latest-maxperf \
# --build-arg TARGET_CPU=native \
# --build-arg RUST_PROFILE=release-max-perf \
# -f docker/Dockerfile .
```
Refer to the Docker Buildx documentation for more advanced build options.
@@ -198,5 +218,3 @@ ### Use Traefik as Proxy
## Voice communication
See the [TURN](../turn.md) page.
[nix-buildlayeredimage]: https://ryantm.github.io/nixpkgs/builders/images/dockertools/#ssec-pkgs-dockerTools-buildLayeredImage

View File

@@ -8,29 +8,39 @@ # Generic deployment documentation
## Installing Continuwuity
### Static prebuilt binary
### Prebuilt binary
You may simply download the binary that fits your machine architecture (x86_64
or aarch64). Run `uname -m` to see what you need.
Download the binary for your architecture (x86_64 or aarch64) -
run the `uname -m` to check which you need.
You can download prebuilt fully static musl binaries from the latest tagged
release [here](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest) or
from the `main` CI branch workflow artifact output. These also include Debian/Ubuntu
packages.
Prebuilt binaries are available from:
- **Tagged releases**: [Latest release page](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest)
- **Development builds**: CI artifacts from the `main` branch
(includes Debian/Ubuntu packages)
You can download these directly using curl. The `ci-bins` are CI workflow binaries organized by commit
hash/revision, and `releases` are tagged releases. Sort by descending last
modified date to find the latest.
When browsing CI artifacts, `ci-bins` contains binaries organised
by commit hash, while `releases` contains tagged versions. Sort
by last modified date to find the most recent builds.
These binaries have jemalloc and io_uring statically linked and included with
them, so no additional dynamic dependencies need to be installed.
The binaries require jemalloc and io_uring on the host system. Currently
we can't cross-build static binaries - contributions are welcome here.
For the **best** performance: if you are using an `x86_64` CPU made in the last ~15 years,
we recommend using the `-haswell-` optimized binaries. These set
`-march=haswell`, which provides the most compatible and highest performance with
optimized binaries. The database backend, RocksDB, benefits most from this as it
uses hardware-accelerated CRC32 hashing/checksumming, which is critical
for performance.
#### Performance-optimised builds
For x86_64 systems with CPUs from the last ~15 years, use the
`-haswell-` optimised binaries for best performance. These
binaries enable hardware-accelerated CRC32 checksumming in
RocksDB, which significantly improves database performance.
The haswell instruction set provides an excellent balance of
compatibility and speed.
If you're using Docker instead, equivalent performance-optimised
images are available with the `-maxperf` suffix (e.g.
`forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf`).
These images use the `release-max-perf`
build profile with
[link-time optimisation (LTO)](https://doc.rust-lang.org/cargo/reference/profiles.html#lto)
and, for amd64, target the haswell CPU architecture.
### Compiling

View File

@@ -128,7 +128,7 @@ ### Log Levels
```rs
// Good
error!(
error = %err,
error = ?err,
room_id = %room_id,
"Failed to send event to room"
);
@@ -264,7 +264,7 @@ ### Code Comments
warn!(
destination = %destination,
attempt = attempt,
error = %err,
error = ?err,
retry_delay_ms = retry_delay.as_millis(),
"Federation request failed, retrying"
);

View File

@@ -8,7 +8,7 @@ # Command-Line Help for `continuwuity`
## `continuwuity`
a very cool Matrix chat homeserver written in Rust
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.
**Usage:** `continuwuity [OPTIONS]`

View File

@@ -4,7 +4,7 @@
Name: continuwuity
Version: {{{ git_repo_version }}}
Release: 1%{?dist}
Summary: Very cool Matrix chat homeserver written in Rust
Summary: A Matrix homeserver written in Rust.
License: Apache-2.0 AND MIT
@@ -23,7 +23,7 @@ Requires: glibc
Requires: libstdc++
%global _description %{expand:
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.}
%description %{_description}

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_admin"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -2,10 +2,17 @@
use conduwuit::Result;
use crate::{
appservice, appservice::AppserviceCommand, check, check::CheckCommand, context::Context,
debug, debug::DebugCommand, federation, federation::FederationCommand, media,
media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand, server,
server::ServerCommand, user, user::UserCommand,
appservice::{self, AppserviceCommand},
check::{self, CheckCommand},
context::Context,
debug::{self, DebugCommand},
federation::{self, FederationCommand},
media::{self, MediaCommand},
query::{self, QueryCommand},
room::{self, RoomCommand},
server::{self, ServerCommand},
token::{self, TokenCommand},
user::{self, UserCommand},
};
#[derive(Debug, Parser)]
@@ -19,6 +26,10 @@ pub enum AdminCommand {
/// - Commands for managing local users
Users(UserCommand),
#[command(subcommand)]
/// - Commands for managing registration tokens
Token(TokenCommand),
#[command(subcommand)]
/// - Commands for managing rooms
Rooms(RoomCommand),
@@ -48,7 +59,7 @@ pub enum AdminCommand {
Query(QueryCommand),
}
#[tracing::instrument(skip_all, name = "command")]
#[tracing::instrument(skip_all, name = "command", level = "info")]
pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Result {
use AdminCommand::*;
@@ -64,6 +75,11 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
context.bail_restricted()?;
user::process(command, context).await
},
| Token(command) => {
// token commands are all restricted
context.bail_restricted()?;
token::process(command, context).await
},
| Rooms(command) => room::process(command, context).await,
| Federation(command) => federation::process(command, context).await,
| Server(command) => server::process(command, context).await,

View File

@@ -456,7 +456,7 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
}
#[admin_command]
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "info")]
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
@@ -483,7 +483,7 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
}
#[admin_command]
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "info")]
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
@@ -510,7 +510,7 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
}
#[admin_command]
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "info")]
pub(super) async fn force_set_room_state_from_server(
&self,
room_id: OwnedRoomId,

View File

@@ -17,6 +17,7 @@
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
pub(crate) mod token;
pub(crate) mod user;
extern crate conduwuit_api as api;

View File

@@ -37,7 +37,7 @@ pub(super) fn dispatch(services: Arc<Services>, command: CommandInput) -> Proces
Box::pin(handle_command(services, command))
}
#[tracing::instrument(skip_all, name = "admin")]
#[tracing::instrument(skip_all, name = "admin", level = "info")]
async fn handle_command(services: Arc<Services>, command: CommandInput) -> ProcessorResult {
AssertUnwindSafe(Box::pin(process_command(services, &command)))
.catch_unwind()

View File

@@ -98,7 +98,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
%room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
@@ -240,7 +240,7 @@ async fn ban_list_of_rooms(&self) -> Result {
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
%room_id,
?servers,
"Got federation response fetching room ID for \
{room}",
@@ -397,7 +397,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
%room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);

View File

@@ -0,0 +1,76 @@
use conduwuit::{Err, Result, utils};
use conduwuit_macros::admin_command;
use futures::StreamExt;
use service::registration_tokens::TokenExpires;
#[admin_command]
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
let expires = {
if expires.immortal {
None
} else if let Some(max_uses) = expires.max_uses {
Some(TokenExpires::AfterUses(max_uses))
} else if expires.once {
Some(TokenExpires::AfterUses(1))
} else if let Some(max_age) = expires
.max_age
.as_deref()
.map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?))
.transpose()?
{
Some(TokenExpires::AfterTime(max_age))
} else {
unreachable!();
}
};
let (token, info) = self
.services
.registration_tokens
.issue_token(self.sender_or_service_user().into(), expires);
self.write_str(&format!(
"New registration token issued: `{token}`. {}.",
if let Some(expires) = info.expires {
format!("{expires}")
} else {
"Never expires".to_owned()
}
))
.await
}
#[admin_command]
pub(super) async fn revoke_token(&self, token: String) -> Result {
let Some(token) = self
.services
.registration_tokens
.validate_token(token)
.await
else {
return Err!("This token does not exist or has already expired.");
};
self.services.registration_tokens.revoke_token(token)?;
self.write_str("Token revoked successfully.").await
}
#[admin_command]
pub(super) async fn list_tokens(&self) -> Result {
let tokens: Vec<_> = self
.services
.registration_tokens
.iterate_tokens()
.collect()
.await;
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
.await?;
for token in tokens {
self.write_str(&format!("- {token}\n")).await?;
}
Ok(())
}

51
src/admin/token/mod.rs Normal file
View File

@@ -0,0 +1,51 @@
mod commands;
use clap::{Args, Subcommand};
use conduwuit::Result;
use crate::admin_command_dispatch;
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum TokenCommand {
/// - Issue a new registration token
#[clap(name = "issue")]
IssueToken {
/// When this token will expire.
#[command(flatten)]
expires: TokenExpires,
},
/// - Revoke a registration token
#[clap(name = "revoke")]
RevokeToken {
/// The token to revoke.
token: String,
},
/// - List all registration tokens
#[clap(name = "list")]
ListTokens,
}
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
pub struct TokenExpires {
/// The maximum number of times this token is allowed to be used before it
/// expires.
#[arg(long)]
max_uses: Option<u64>,
/// The maximum age of this token (e.g. 30s, 5m, 7d). It will expire after
/// this much time has passed.
#[arg(long)]
max_age: Option<String>,
/// This token will never expire.
#[arg(long)]
immortal: bool,
/// A shortcut for `--max-uses 1`.
#[arg(long)]
once: bool,
}

View File

@@ -238,6 +238,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
#[admin_command]
pub(super) async fn suspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user {
@@ -262,6 +263,7 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
#[admin_command]
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user {
@@ -278,7 +280,12 @@ pub(super) async fn unsuspend(&self, user_id: String) -> Result {
}
#[admin_command]
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
pub(super) async fn reset_password(
&self,
logout: bool,
username: String,
password: Option<String>,
) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
if user_id == self.services.globals.server_user {
@@ -301,7 +308,18 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.await
.await?;
if logout {
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
write!(self, "\nAll existing sessions have been logged out.").await?;
}
Ok(())
}
#[admin_command]
@@ -974,3 +992,113 @@ pub(super) async fn force_leave_remote_room(
self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
.await
}
#[admin_command]
pub(super) async fn lock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to lock the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot be locked.");
}
self.services
.users
.lock_account(&user_id, self.sender_or_service_user())
.await;
self.write_str(&format!("User {user_id} has been locked."))
.await
}
#[admin_command]
pub(super) async fn unlock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
self.services.users.unlock_account(&user_id).await;
self.write_str(&format!("User {user_id} has been unlocked."))
.await
}
#[admin_command]
pub(super) async fn logout(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to log out the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("You cannot forcefully log out admin users.");
}
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
self.write_str(&format!("User {user_id} has been logged out from all devices."))
.await
}
#[admin_command]
pub(super) async fn disable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to disable login for the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot have their login disallowed.");
}
self.services.users.disable_login(&user_id);
self.write_str(&format!(
"{user_id} can no longer log in. Their existing sessions remain unaffected."
))
.await
}
#[admin_command]
pub(super) async fn enable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
self.services.users.enable_login(&user_id);
self.write_str(&format!("{user_id} can now log in.")).await
}

View File

@@ -20,6 +20,9 @@ pub enum UserCommand {
/// - Reset user password
ResetPassword {
/// Log out existing sessions
#[arg(short, long)]
logout: bool,
/// Username of the user for whom the password should be reset
username: String,
/// New password for the user, if unspecified one is generated
@@ -59,6 +62,18 @@ pub enum UserCommand {
force: bool,
},
/// - Forcefully log a user out of all of their devices.
///
/// This will invalidate all access tokens for the specified user,
/// effectively logging them out from all sessions.
/// Note that this is destructive and may result in data loss for the user,
/// such as encryption keys. Use with caution. Can only be used in the admin
/// room.
Logout {
/// Username of the user to log out
user_id: String,
},
/// - Suspend a user
///
/// Suspended users are able to log in, sync, and read messages, but are not
@@ -81,6 +96,42 @@ pub enum UserCommand {
user_id: String,
},
/// - Lock a user
///
/// Locked users are unable to use their accounts beyond logging out. This
/// is akin to a temporary deactivation that does not change the user's
/// password. This can be used to quickly prevent a user from accessing
/// their account.
Lock {
/// Username of the user to lock
user_id: String,
},
/// - Unlock a user
///
/// Reverses the effects of the `lock` command, allowing the user to use
/// their account again.
Unlock {
/// Username of the user to unlock
user_id: String,
},
/// - Enable login for a user
EnableLogin {
/// Username of the user to enable login for
user_id: String,
},
/// - Disable login for a user
///
/// Disables login for the specified user without deactivating or locking
/// their account. This prevents the user from obtaining new access tokens,
/// but does not invalidate existing sessions.
DisableLogin {
/// Username of the user to disable login for
user_id: String,
},
/// - List local users in the database
#[clap(alias = "list")]
ListUsers,

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_api"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -49,7 +49,7 @@
///
/// Note: This will not reserve the username, so the username might become
/// invalid when trying to register
#[tracing::instrument(skip_all, fields(%client), name = "register_available")]
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -138,7 +138,7 @@ pub(crate) async fn get_register_available_route(
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register")]
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -179,13 +179,18 @@ pub(crate) async fn register_route(
},
}
return Err!(Request(Forbidden("Registration has been disabled.")));
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services.globals.registration_token.is_some()))
&& services
.registration_tokens
.get_config_file_token()
.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, \
@@ -203,7 +208,9 @@ pub(crate) async fn register_route(
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
let user_id = match (body.username.as_ref(), is_guest) {
@@ -301,7 +308,13 @@ pub(crate) async fn register_route(
let skip_auth = body.appservice_info.is_some() || is_guest;
// Populate required UIAA flows
if services.globals.registration_token.is_some() {
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
// Registration token required
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
@@ -323,7 +336,19 @@ pub(crate) async fn register_route(
}
if uiaainfo.flows.is_empty() && !skip_auth {
// No registration token necessary, but clients must still go through the flow
// Registration isn't _disabled_, but there's no captcha configured and no
// registration tokens currently set. Bail out by default unless open
// registration was explicitly enabled.
if !services
.config
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
{
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
// We have open registration enabled (😧), provide a dummy stage
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
@@ -603,7 +628,7 @@ pub(crate) async fn register_route(
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "change_password")]
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
pub(crate) async fn change_password_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -727,7 +752,7 @@ pub(crate) async fn whoami_route(
/// - Forgets all to-device events
/// - Triggers device list updates
/// - Removes ability to log in again
#[tracing::instrument(skip_all, fields(%client), name = "deactivate")]
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
pub(crate) async fn deactivate_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -846,19 +871,20 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
/// Checks if the provided registration token is valid at the time of checking.
pub(crate) async fn check_registration_token_validity(
State(services): State<crate::State>,
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err!(Request(Forbidden("Server does not allow token registration")));
};
// TODO: ratelimit this pretty heavily
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
let valid = services
.registration_tokens
.validate_token(body.token.clone())
.await
.is_some();
Ok(check_registration_token_validity::v1::Response { valid })
}
/// Runs through all the deactivation steps:

View File

@@ -102,7 +102,7 @@ pub(crate) async fn get_alias_route(
};
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
debug!(?room_alias, ?room_id, "available servers: {servers:?}");
debug!(%room_alias, %room_id, "available servers: {servers:?}");
Ok(get_alias::v3::Response::new(room_id, servers))
}

View File

@@ -74,7 +74,7 @@ pub(crate) async fn get_context_route(
}
if !visible {
debug_warn!(req_evt = ?event_id, ?base_id, ?room_id, "Event requested by {sender_user} but is not allowed to see it, returning 404");
debug_warn!(req_evt = %event_id, ?base_id, %room_id, "Event requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}

View File

@@ -49,7 +49,7 @@ pub(crate) async fn get_device_route(
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
///
/// Updates the metadata on a given device of the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "update_device")]
#[tracing::instrument(skip_all, fields(%client), name = "update_device", level = "debug")]
pub(crate) async fn update_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -44,7 +44,7 @@
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
pub(crate) async fn get_public_rooms_filtered_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -80,7 +80,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
pub(crate) async fn get_public_rooms_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -116,7 +116,7 @@ pub(crate) async fn get_public_rooms_route(
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
///
/// Sets the visibility of a given room in the room directory.
#[tracing::instrument(skip_all, fields(%client), name = "room_directory")]
#[tracing::instrument(skip_all, fields(%client), name = "room_directory", level = "info")]
pub(crate) async fn set_room_visibility_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -52,7 +52,7 @@ pub(crate) async fn upload_keys_route(
.deserialize()
.inspect_err(|e| {
debug_warn!(
?key_id,
%key_id,
?one_time_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
@@ -94,8 +94,8 @@ pub(crate) async fn upload_keys_route(
{
if existing_keys.json().get() == device_keys.json().get() {
debug!(
?sender_user,
?sender_device,
%sender_user,
%sender_device,
?device_keys,
"Ignoring user uploaded keys as they are an exact copy already in the \
database"
@@ -338,7 +338,7 @@ pub(crate) async fn upload_signatures_route(
for (user_id, keys) in &body.signed_keys {
for (key_id, key) in keys {
let Ok(key) = serde_json::to_value(key)
.inspect_err(|e| debug_warn!(?key_id, "Invalid \"key\" JSON: {e}"))
.inspect_err(|e| debug_warn!(%key_id, "Invalid \"key\" JSON: {e}"))
else {
continue;
};

View File

@@ -3,6 +3,7 @@
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
warn,
};
use futures::FutureExt;
use ruma::{
@@ -21,7 +22,7 @@
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
/// Tries to send an invite event into the room.
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
pub(crate) async fn invite_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -124,6 +125,18 @@ pub(crate) async fn invite_helper(
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.to_owned(), room_id.to_owned())
.await
{
warn!(
"Invite from {} to {} in room {} blocked by antispam: {e:?}",
sender_user, recipient_user, room_id
);
return Err!(Request(Forbidden("Invite blocked by antispam service.")));
}
if !services.globals.user_is_local(recipient_user) {
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;

View File

@@ -33,7 +33,7 @@
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
join_rules::{AllowRule, JoinRule},
member::{MembershipState, RoomMemberEventContent},
},
},
@@ -59,7 +59,7 @@
/// rules locally
/// - If the server does not know about the room: asks other servers over
/// federation
#[tracing::instrument(skip_all, fields(%client), name = "join")]
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
pub(crate) async fn join_room_by_id_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -131,7 +131,7 @@ pub(crate) async fn join_room_by_id_route(
/// - If the server does not know about the room: use the server name query
/// param if specified. if not specified, asks other servers over federation
/// via room alias server name and room ID server name
#[tracing::instrument(skip_all, fields(%client), name = "join")]
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
pub(crate) async fn join_room_by_id_or_alias_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -288,6 +288,23 @@ pub async fn join_room_by_id_helper(
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
}
if let Err(e) = services
.antispam
.user_may_join_room(
sender_user.to_owned(),
room_id.to_owned(),
services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await,
)
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("You are not allowed to join this room.")));
}
let server_in_room = services
.rooms
.state_cache
@@ -321,6 +338,17 @@ pub async fn join_room_by_id_helper(
)));
}
if services.antispam.check_all_joins() {
if let Err(e) = services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
if server_in_room {
join_room_by_id_helper_local(
services,
@@ -347,11 +375,10 @@ pub async fn join_room_by_id_helper(
.boxed()
.await?;
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote")]
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote", level = "info")]
async fn join_room_by_id_helper_remote(
services: &Services,
sender_user: &UserId,
@@ -709,7 +736,7 @@ async fn join_room_by_id_helper_remote(
Ok(())
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local")]
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local", level = "info")]
async fn join_room_by_id_helper_local(
services: &Services,
sender_user: &UserId,
@@ -720,45 +747,51 @@ async fn join_room_by_id_helper_local(
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can join locally");
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
let join_rules_event_content = services
.rooms
.state_accessor
.room_state_get_content::<RoomJoinRulesEventContent>(
room_id,
&StateEventType::RoomJoinRules,
"",
)
.await;
let restriction_rooms = match join_rules_event_content {
| Ok(RoomJoinRulesEventContent {
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
}) => restricted
.allow
.into_iter()
.filter_map(|a| match a {
| AllowRule::RoomMembership(r) => Some(r.room_id),
| _ => None,
})
.collect(),
| _ => Vec::new(),
};
let join_authorized_via_users_server: Option<OwnedUserId> = {
if restriction_rooms
.iter()
.stream()
.any(|restriction_room_id| {
trace!("Checking if {sender_user} is joined to {restriction_room_id}");
services
.rooms
.state_cache
.is_joined(sender_user, restriction_room_id)
})
.await
{
services
let mut restricted_join_authorized = None;
match join_rules {
| JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
for restriction in restricted.allow {
match restriction {
| AllowRule::RoomMembership(membership) => {
if services
.rooms
.state_cache
.is_joined(sender_user, &membership.room_id)
.await
{
restricted_join_authorized = Some(true);
break;
}
},
| AllowRule::UnstableSpamChecker => {
match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
| Ok(()) => {
restricted_join_authorized = Some(true);
break;
},
| Err(_) =>
return Err!(Request(Forbidden(
"Antispam rejected join request."
))),
}
},
| _ => {},
}
}
},
| _ => {},
}
let join_authorized_via_users_server = if restricted_join_authorized.is_none() {
None
} else {
match restricted_join_authorized.unwrap() {
| true => services
.rooms
.state_cache
.local_users_in_room(room_id)
@@ -774,10 +807,14 @@ async fn join_room_by_id_helper_local(
.boxed()
.next()
.await
.map(ToOwned::to_owned)
} else {
trace!("No restriction rooms are joined by {sender_user}");
None
.map(ToOwned::to_owned),
| false => {
warn!(
"Join authorization failed for restricted join in room {room_id} for user \
{sender_user}"
);
return Err!(Request(Forbidden("You are not authorized to join this room.")));
},
}
};
@@ -805,16 +842,14 @@ async fn join_room_by_id_helper_local(
return Ok(());
};
if restriction_rooms.is_empty()
&& (servers.is_empty()
|| servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
return Err(error);
}
warn!(
"We couldn't do the join locally, maybe federation can help to satisfy the restricted \
join requirements"
?error,
servers = %servers.len(),
"Could not join restricted room locally, attempting remote join",
);
let Ok((make_join_response, remote_server)) =
make_join_request(services, sender_user, room_id, servers).await

View File

@@ -44,7 +44,7 @@
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
///
/// Tries to knock the room to ask permission to join for the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "knock")]
#[tracing::instrument(skip_all, fields(%client), name = "knock", level = "info")]
pub(crate) async fn knock_room_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -178,7 +178,20 @@ pub async fn leave_room(
.rooms
.state_cache
.left_state(user_id, room_id)
.await?
.await
.inspect_err(|err| {
// `left_state` may return an Err if the user _is_ in the room they're
// trying to leave, but the membership cache is incorrect and
// they're cached as being joined. In this situation
// we save a `None` to the `roomuserid_leftcount` table, which generates
// and sends a dummy leave to the client.
warn!(
?err,
"Trying to leave room not cached as leave, sending dummy leave \
event to client"
);
})
.unwrap_or_default()
},
}
};

View File

@@ -63,7 +63,7 @@ pub(crate) async fn joined_rooms_route(
///
/// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is
/// enabled
#[tracing::instrument(skip(services))]
#[tracing::instrument(skip(services), level = "info")]
pub(crate) async fn banned_room_check(
services: &Services,
user_id: &UserId,

View File

@@ -115,7 +115,7 @@ async fn paginate_relations_with_filter(
.user_can_see_event(sender_user, room_id, target)
.await
{
debug_warn!(req_evt = ?target, ?room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
debug_warn!(req_evt = %target, %room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}

View File

@@ -29,7 +29,7 @@ struct Report {
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_room")]
#[tracing::instrument(skip_all, fields(%client), name = "report_room", level = "info")]
pub(crate) async fn report_room_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -85,7 +85,7 @@ pub(crate) async fn report_room_route(
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
///
/// Reports an inappropriate event to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_event")]
#[tracing::instrument(skip_all, fields(%client), name = "report_event", level = "info")]
pub(crate) async fn report_event_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -133,7 +133,7 @@ pub(crate) async fn report_event_route(
Ok(report_content::v3::Response {})
}
#[tracing::instrument(skip_all, fields(%client), name = "report_user")]
#[tracing::instrument(skip_all, fields(%client), name = "report_user", level = "info")]
pub(crate) async fn report_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -492,7 +492,7 @@ pub(crate) async fn create_room_route(
.boxed()
.await
{
warn!(%e, "Failed to send invite");
warn!(?e, "Failed to send invite");
}
}
@@ -627,7 +627,7 @@ async fn room_alias_check(
.map_err(|e| {
err!(Request(InvalidParam(debug_error!(
?e,
?room_alias_name,
%room_alias_name,
"Failed to parse room alias.",
))))
})?;
@@ -711,7 +711,7 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
}
})
.inspect(|full_room_id| {
debug_info!(?full_room_id, "Full custom room ID");
debug_info!(%full_room_id, "Full custom room ID");
})
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
.inspect_err(|e| warn!(?e, %custom_room_id, "Failed to create room with custom room ID",))
}

View File

@@ -46,7 +46,7 @@ pub(crate) async fn get_room_summary_legacy(
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
///
/// Returns a short description of the state of a room.
#[tracing::instrument(skip_all, fields(%client), name = "room_summary")]
#[tracing::instrument(skip_all, fields(%client), name = "room_summary", level = "info")]
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -116,7 +116,10 @@ async fn local_room_summary_response(
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
trace!(
sender_user = sender_user.map(tracing::field::display),
"Sending local room summary response for {room_id:?}"
);
let (join_rule, world_readable, guest_can_join) = join3(
services.rooms.state_accessor.get_join_rules(room_id),
services.rooms.state_accessor.is_world_readable(room_id),
@@ -228,7 +231,7 @@ async fn remote_room_summary_hierarchy_response(
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<SpaceHierarchyParentSummary> {
trace!(?sender_user, ?servers, "Sending remote room summary response for {room_id:?}");
trace!(sender_user = ?sender_user.map(tracing::field::display), ?servers, "Sending remote room summary response for {room_id:?}");
if !services.config.allow_federation {
return Err!(Request(Forbidden("Federation is disabled.")));
}

View File

@@ -5,6 +5,7 @@
use conduwuit::{
Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash},
warn,
};
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
@@ -12,6 +13,7 @@
use ruma::{
OwnedUserId, UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@@ -35,7 +37,7 @@
///
/// Get the supported login types of this server. One of these should be used as
/// the `type` field when logging in.
#[tracing::instrument(skip_all, fields(%client), name = "login")]
#[tracing::instrument(skip_all, fields(%client), name = "login", level = "info")]
pub(crate) async fn get_login_types_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -53,7 +55,7 @@ pub(crate) async fn get_login_types_route(
/// Authenticates the given user by its ID and its password.
///
/// Returns the user ID if successful, and an error otherwise.
#[tracing::instrument(skip_all, fields(%user_id), name = "password")]
#[tracing::instrument(skip_all, fields(%user_id), name = "password", level = "debug")]
pub(crate) async fn password_login(
services: &Services,
user_id: &UserId,
@@ -96,7 +98,7 @@ pub(crate) async fn password_login(
///
/// Creates the user if the user is found in the LDAP and do not already have an
/// account.
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap")]
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap", level = "debug")]
pub(super) async fn ldap_login(
services: &Services,
user_id: &UserId,
@@ -184,6 +186,15 @@ pub(crate) async fn handle_login(
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
if services.users.is_locked(&user_id).await? {
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
}
if services.users.is_login_disabled(&user_id).await {
warn!(%user_id, "user attempted to log in with a login-disabled account");
return Err!(Request(Forbidden("This account is not permitted to log in.")));
}
if cfg!(feature = "ldap") && services.config.ldap.enable {
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
| Ok(user_id) => Ok(user_id),
@@ -212,7 +223,7 @@ pub(crate) async fn handle_login(
/// Note: You can use [`GET
/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
/// supported login types.
#[tracing::instrument(skip_all, fields(%client), name = "login")]
#[tracing::instrument(skip_all, fields(%client), name = "login", level = "info")]
pub(crate) async fn login_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -345,7 +356,7 @@ pub(crate) async fn login_route(
/// to log in with the m.login.token flow.
///
/// <https://spec.matrix.org/v1.13/client-server-api/#post_matrixclientv1loginget_token>
#[tracing::instrument(skip_all, fields(%client), name = "login_token")]
#[tracing::instrument(skip_all, fields(%client), name = "login_token", level = "info")]
pub(crate) async fn login_token_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -413,7 +424,7 @@ pub(crate) async fn login_token_route(
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "logout")]
#[tracing::instrument(skip_all, fields(%client), name = "logout", level = "info")]
pub(crate) async fn logout_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -440,7 +451,7 @@ pub(crate) async fn logout_route(
/// Note: This is equivalent to calling [`GET
/// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this
/// user.
#[tracing::instrument(skip_all, fields(%client), name = "logout")]
#[tracing::instrument(skip_all, fields(%client), name = "logout", level = "info")]
pub(crate) async fn logout_all_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -140,8 +140,8 @@ pub(crate) async fn get_state_events_for_key_route(
.await
.map_err(|_| {
err!(Request(NotFound(debug_warn!(
room_id = ?body.room_id,
event_type = ?body.event_type,
room_id = %body.room_id,
event_type = %body.event_type,
"State event not found in room.",
))))
})?;
@@ -226,7 +226,7 @@ async fn allowed_to_send_state_event(
match event_type {
| StateEventType::RoomCreate => {
return Err!(Request(BadJson(debug_warn!(
?room_id,
%room_id,
"You cannot update m.room.create after a room has been created."
))));
},
@@ -237,7 +237,7 @@ async fn allowed_to_send_state_event(
| Ok(acl_content) => {
if acl_content.allow_is_empty() {
return Err!(Request(BadJson(debug_warn!(
?room_id,
%room_id,
"Sending an ACL event with an empty allow key will permanently \
brick the room for non-conduwuit's as this equates to no servers \
being allowed to participate in this room."
@@ -246,7 +246,7 @@ async fn allowed_to_send_state_event(
if acl_content.deny_contains("*") && acl_content.allow_contains("*") {
return Err!(Request(BadJson(debug_warn!(
?room_id,
%room_id,
"Sending an ACL event with a deny and allow key value of \"*\" will \
permanently brick the room for non-conduwuit's as this equates to \
no servers being allowed to participate in this room."
@@ -258,7 +258,7 @@ async fn allowed_to_send_state_event(
&& !acl_content.allow_contains(services.globals.server_name().as_str())
{
return Err!(Request(BadJson(debug_warn!(
?room_id,
%room_id,
"Sending an ACL event with a deny key value of \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
@@ -270,7 +270,7 @@ async fn allowed_to_send_state_event(
&& !acl_content.allow_contains(services.globals.server_name().as_str())
{
return Err!(Request(BadJson(debug_warn!(
?room_id,
%room_id,
"Sending an ACL event for an allow key without \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."

View File

@@ -50,8 +50,8 @@
level = "debug",
skip_all,
fields(
room_id = ?room_id,
syncing_user = ?sync_context.syncing_user,
room_id = %room_id,
syncing_user = %sync_context.syncing_user,
),
)]
pub(super) async fn load_joined_room(
@@ -578,7 +578,7 @@ async fn build_notification_counts(
)
.await;
trace!(?notification_count, ?highlight_count, "syncing new notification counts");
trace!(%notification_count, %highlight_count, "syncing new notification counts");
Ok(Some(UnreadNotificationsCount {
notification_count: Some(notification_count),
@@ -692,8 +692,8 @@ async fn build_room_summary(
};
trace!(
?joined_member_count,
?invited_member_count,
%joined_member_count,
%invited_member_count,
heroes_length = heroes.as_ref().map(HashSet::len),
"syncing updated summary"
);

View File

@@ -307,8 +307,8 @@ async fn build_left_state_and_timeline(
}
trace!(
?timeline_start_count,
?timeline_end_count,
%timeline_start_count,
%timeline_end_count,
"syncing {} timeline events (limited = {}) and {} state events",
timeline.pdus.len(),
timeline.limited,

View File

@@ -275,7 +275,7 @@ pub(crate) async fn build_sync_events(
match joined_room {
| Ok((room, updates)) => Some((room_id, room, updates)),
| Err(err) => {
warn!(?err, ?room_id, "error loading joined room {}", room_id);
warn!(?err, %room_id, "error loading joined room");
None
},
}

View File

@@ -217,7 +217,7 @@ pub(super) async fn build_state_incremental<'a>(
the performance penalty is acceptable.
*/
trace!(?timeline_is_linear, ?timeline.limited, "computing state for incremental sync");
trace!(%timeline_is_linear, %timeline.limited, "computing state for incremental sync");
// fetch the shorteventids of state events in the timeline
let state_events_in_timeline: BTreeSet<ShortEventId> = services

View File

@@ -26,7 +26,7 @@
/// TODO: Implement pagination, currently this just returns everything
///
/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms")]
#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms", level = "info")]
pub(crate) async fn get_mutual_rooms_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -137,12 +137,30 @@ pub(super) async fn auth(
| (
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)),
) => Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: Some(device_id),
appservice_info: None,
}),
) => {
let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
})?;
if is_locked {
// Only /logout and /logout/all are allowed for locked users
if !matches!(
metadata,
&ruma::api::client::session::logout::v3::Request::METADATA
| &ruma::api::client::session::logout_all::v3::Request::METADATA
) {
return Err(Error::BadRequest(
ErrorKind::UserLocked,
"This account has been locked.",
));
}
}
Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: Some(device_id),
appservice_info: None,
})
},
| (AuthScheme::ServerSignatures, Token::None) =>
Ok(auth_server(services, request, json_body).await?),
| (

View File

@@ -40,7 +40,7 @@ pub(crate) async fn get_missing_events_route(
while i < queued_events.len() && events.len() < limit {
let Ok(pdu) = services.rooms.timeline.get_pdu(&queued_events[i]).await else {
debug!(
?body.origin,
body.origin = body.origin.as_ref().map(tracing::field::display),
"Event {} does not exist locally, skipping", &queued_events[i]
);
i = i.saturating_add(1);
@@ -59,7 +59,7 @@ pub(crate) async fn get_missing_events_route(
.await
{
debug!(
?body.origin,
body.origin = body.origin.as_ref().map(tracing::field::display),
"Server cannot see {:?} in {:?}, skipping", pdu.event_id, pdu.room_id
);
i = i.saturating_add(1);
@@ -68,7 +68,7 @@ pub(crate) async fn get_missing_events_route(
let Ok(event) = to_canonical_object(&pdu) else {
debug_error!(
?body.origin,
body.origin = body.origin.as_ref().map(tracing::field::display),
"Failed to convert PDU in database to canonical JSON: {pdu:?}"
);
i = i.saturating_add(1);

View File

@@ -19,7 +19,7 @@
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
///
/// Invites a remote user to a room.
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
pub(crate) async fn create_invite_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -148,6 +148,15 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("This server does not allow room invites.")));
}
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.clone(), body.room_id.clone())
.await
{
warn!("Antispam rejected invite: {e:?}");
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
}
let mut invite_state = body.invite_room_state.clone();
let mut event: JsonObject = serde_json::from_str(body.event.get())

View File

@@ -1,7 +1,7 @@
use std::borrow::ToOwned;
use axum::extract::State;
use conduwuit::{
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
};
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
use conduwuit_service::Services;
use futures::StreamExt;
use ruma::{
@@ -22,7 +22,7 @@
/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
///
/// Creates a join template.
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()))]
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()), level = "info")]
pub(crate) async fn create_join_event_template_route(
State(services): State<crate::State>,
body: Ruma<prepare_join_event::v1::Request>,
@@ -122,6 +122,16 @@ pub(crate) async fn create_join_event_template_route(
None
}
};
if services.antispam.check_all_joins() && join_authorized_via_users_server.is_none() {
if services
.antispam
.meowlnir_accept_make_join(body.room_id.clone(), body.user_id.clone())
.await
.is_err()
{
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
let (_pdu, mut pdu_json) = services
.rooms
@@ -136,7 +146,6 @@ pub(crate) async fn create_join_event_template_route(
&state_lock,
)
.await?;
drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format
@@ -192,25 +201,44 @@ pub(crate) async fn user_can_perform_restricted_join(
return Ok(false);
}
if r.allow
.iter()
.filter_map(|rule| {
if let AllowRule::RoomMembership(membership) = rule {
Some(membership)
} else {
None
}
})
.stream()
.any(|m| services.rooms.state_cache.is_joined(user_id, &m.room_id))
.await
{
Ok(true)
} else {
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
for allow_rule in &r.allow {
match allow_rule {
| AllowRule::RoomMembership(membership) => {
if services
.rooms
.state_cache
.is_joined(user_id, &membership.room_id)
.await
{
debug!(
"User {} is allowed to join room {} via membership in room {}",
user_id, room_id, membership.room_id
);
return Ok(true);
}
},
| AllowRule::UnstableSpamChecker =>
return match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned())
.await
{
| Ok(()) => Ok(true),
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
},
| _ => {
debug_info!(
"Unsupported allow rule in restricted join for room {}: {:?}",
room_id,
allow_rule
);
},
}
}
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
}
pub(crate) fn maybe_strip_event_id(

View File

@@ -3,9 +3,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Result, debug,
debug::INFO_SPAN_LEVEL,
debug_warn, err, error,
Err, Error, Result, debug, debug_warn, err, error,
result::LogErr,
trace,
utils::{
@@ -48,7 +46,7 @@
/// Push EDUs and PDUs to this server.
#[tracing::instrument(
name = "txn",
level = INFO_SPAN_LEVEL,
level = "debug",
skip_all,
fields(
%client,
@@ -83,8 +81,8 @@ pub(crate) async fn send_transaction_message_route(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = ?body.transaction_id,
origin =?body.origin(),
id = %body.transaction_id,
origin = %body.origin(),
"Starting txn",
);
@@ -110,8 +108,8 @@ pub(crate) async fn send_transaction_message_route(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = ?body.transaction_id,
origin =?body.origin(),
id = %body.transaction_id,
origin = %body.origin(),
"Finished txn",
);
for (id, result) in &results {

View File

@@ -26,7 +26,7 @@
use crate::Ruma;
/// helper method for /send_join v1 and v2
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()))]
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()), level = "info")]
async fn create_join_event(
services: &Services,
origin: &ServerName,

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_build_metadata"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_core"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -340,7 +340,7 @@ fn set<T>(key: &Key, val: T) -> Result<T>
#[tracing::instrument(
name = "get",
level = "trace"
level = "trace",
skip_all,
fields(?key)
)]
@@ -357,7 +357,7 @@ fn get<T>(key: &Key) -> Result<T>
#[tracing::instrument(
name = "xchg",
level = "trace"
level = "trace",
skip_all,
fields(?key, ?val)
)]

View File

@@ -146,22 +146,6 @@ pub fn check(config: &Config) -> Result {
));
}
// check if we can read the token file path, and check if the file is empty
if config.registration_token_file.as_ref().is_some_and(|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
}) else {
return true;
};
token == String::new()
}) {
return Err!(Config(
"registration_token_file",
"Registration token file was specified but is empty or failed to be read"
));
}
if config.max_request_size < 10_000_000 {
return Err!(Config(
"max_request_size",
@@ -187,29 +171,9 @@ pub fn check(config: &Config) -> Result {
));
}
if config.allow_registration
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
&& config.recaptcha_site_key.is_none()
{
return Err!(Config(
"registration_token",
"!! You have `allow_registration` enabled without a token or captcha configured \
which means you are allowing ANYONE to register on your continuwuity instance \
without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \
abused by malicious actors. If this is not the intended behaviour, please set a \
registration token. For security and safety reasons, continuwuity will shut down. \
If you are extra sure this is the desired behaviour you want, please set the \
following config option to true:
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
));
}
if config.allow_registration
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
{
warn!(
"Open registration is enabled via setting \

View File

@@ -59,7 +59,7 @@ fn deref(&self) -> &Self::Target { HANDLE.with_borrow_mut(|handle| self.load(han
/// Update the active configuration, returning prior configuration.
#[implement(Manager)]
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub fn update(&self, config: Config) -> Result<Arc<Config>> {
let config = Arc::new(config);
let new = Arc::into_raw(config);

View File

@@ -18,7 +18,7 @@
pub use figment::{Figment, value::Value as FigmentValue};
use regex::RegexSet;
use ruma::{
OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::discovery::discover_support::ContactRole,
};
use serde::{Deserialize, de::IgnoredAny};
@@ -53,7 +53,8 @@
### For more information, see:
### https://continuwuity.org/configuration.html
"#,
ignore = "config_paths catchall well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure"
ignore = "config_paths catchall well_known tls blurhashing \
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
)]
pub struct Config {
// Paths to config file(s). Not supposed to be set manually in the config file,
@@ -544,7 +545,7 @@ pub struct Config {
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
///
/// If you would like registration only via token reg, please configure
/// `registration_token` or `registration_token_file`.
/// `registration_token`.
#[serde(default)]
pub allow_registration: bool,
@@ -575,22 +576,14 @@ pub struct Config {
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
/// to true to allow open registration without any conditions.
///
/// YOU NEED TO EDIT THIS OR USE registration_token_file.
/// If you do not want to set a static token, the `!admin token` commands
/// may also be used to manage registration tokens.
///
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
///
/// display: sensitive
pub registration_token: Option<String>,
/// Path to a file on the system that gets read for additional registration
/// tokens. Multiple tokens can be added if you separate them with
/// whitespace
///
/// continuwuity must be able to access the file, and it must not be empty
///
/// example: "/etc/continuwuity/.reg_token"
pub registration_token_file: Option<PathBuf>,
/// The public site key for reCaptcha. If this is provided, reCaptcha
/// becomes required during registration. If both captcha *and*
/// registration token are enabled, both will be required during
@@ -1887,7 +1880,7 @@ pub struct Config {
/// Enable the tokio-console. This option is only relevant to developers.
///
/// For more information, see:
/// For more information, see:
/// https://continuwuity.org/development.html#debugging-with-tokio-console
#[serde(default)]
pub tokio_console: bool,
@@ -2024,6 +2017,10 @@ pub struct Config {
#[serde(default)]
pub ldap: LdapConfig,
/// Configuration for antispam support
#[serde(default)]
pub antispam: Option<Antispam>,
// external structure; separate section
#[serde(default)]
pub blurhashing: BlurhashConfig,
@@ -2240,7 +2237,58 @@ struct ListeningAddr {
addrs: Either<IpAddr, Vec<IpAddr>>,
}
const DEPRECATED_KEYS: &[&str; 9] = &[
#[derive(Clone, Debug, Deserialize)]
pub struct Antispam {
pub meowlnir: Option<MeowlnirConfig>,
pub draupnir: Option<DraupnirConfig>,
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.meowlnir"
)]
pub struct MeowlnirConfig {
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
///
/// Example: "http://127.0.0.1:29339"
pub base_url: Url,
/// The authentication secret defined in antispam->secret. Required for
/// continuwuity to talk to Meowlnir.
pub secret: String,
/// The management room for which to send requests
pub management_room: OwnedRoomId,
/// If enabled run all federated join attempts (both federated and local)
/// through the Meowlnir anti-spam checks.
///
/// By default, only join attempts for rooms with the `fi.mau.spam_checker`
/// restricted join rule are checked.
#[serde(default)]
pub check_all_joins: bool,
}
// TODO: the DraupnirConfig and MeowlnirConfig are basically identical.
// Maybe management_room could just become an Option<> and these structs merged?
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.draupnir"
)]
pub struct DraupnirConfig {
/// The base URL on which to contact Draupnir (before /api/).
///
/// Example: "http://127.0.0.1:29339"
pub base_url: Url,
/// The authentication secret defined in
/// web->synapseHTTPAntispam->authorization
pub secret: String,
}
const DEPRECATED_KEYS: &[&str] = &[
"cache_capacity",
"conduit_cache_capacity_modifier",
"max_concurrent_requests",
@@ -2250,6 +2298,7 @@ struct ListeningAddr {
"well_known_support_role",
"well_known_support_email",
"well_known_support_mxid",
"registration_token_file",
];
impl Config {

View File

@@ -75,10 +75,12 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| ThreepidDenied
| InviteBlocked
| WrongRoomKeysVersion { .. }
| UserSuspended
| Forbidden { .. } => StatusCode::FORBIDDEN,
// 401
| UnknownToken { .. } | MissingToken | Unauthorized => StatusCode::UNAUTHORIZED,
| UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
StatusCode::UNAUTHORIZED,
// 400
| _ => StatusCode::BAD_REQUEST,

View File

@@ -532,8 +532,8 @@ pub async fn auth_check<E, F, Fut>(
if sender_power_level < invite_level {
warn!(
%sender,
has=?sender_power_level,
required=?invite_level,
has=%sender_power_level,
required=%invite_level,
"sender cannot send invites in this room"
);
return Ok(false);
@@ -605,8 +605,8 @@ pub async fn auth_check<E, F, Fut>(
if !check_redaction(room_version, incoming_event, sender_power_level, redact_level)? {
warn!(
%sender,
?sender_power_level,
?redact_level,
%sender_power_level,
%redact_level,
"redaction event was not allowed"
);
return Ok(false);
@@ -772,11 +772,12 @@ struct GetThirdPartyInvite {
power_levels_event.as_ref().is_some(),
) || auth_user_pl >= invite_level;
trace!(
auth_user_pl=?auth_user_pl,
invite_level=?invite_level,
user_joined=?user_joined,
okay_power=?okay_power,
passing=?(user_joined && okay_power),
%auth_user_pl,
%auth_user_pl,
%invite_level,
%user_joined,
%okay_power,
passing=%(user_joined && okay_power),
"user for join auth is valid check details"
);
user_joined && okay_power
@@ -1211,7 +1212,7 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
{
warn!(
%user_level,
required=?event_type_power_level,
required=%event_type_power_level,
state_key=?event.state_key(),
sender=%event.sender(),
"state_key starts with @ but does not match sender",

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_database"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -7,7 +7,7 @@
use crate::util::map_err;
#[implement(Engine)]
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "info")]
pub fn backup(&self) -> Result {
let mut engine = self.backup_engine()?;
let config = &self.ctx.server.config;

View File

@@ -4,7 +4,7 @@
#[tracing::instrument(
parent = None,
name = "rocksdb",
level = "trace"
level = "trace",
skip(msg),
)]
pub(crate) fn handle(level: LogLevel, msg: &str) {

View File

@@ -17,7 +17,7 @@
use crate::{Context, or_else};
#[implement(Engine)]
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<Self>> {
let server = &ctx.server;
let config = &server.config;
@@ -63,7 +63,7 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
}
#[implement(Engine)]
#[tracing::instrument(name = "configure", skip_all)]
#[tracing::instrument(name = "configure", skip_all, level = "debug")]
fn configure_cfds(
ctx: &Arc<Context>,
db_opts: &Options,
@@ -119,7 +119,7 @@ fn configure_cfds(
}
#[implement(Engine)]
#[tracing::instrument(name = "discover", skip_all)]
#[tracing::instrument(name = "discover", skip_all, level = "debug")]
fn discover_cfs(path: &Path, opts: &Options) -> BTreeSet<String> {
Db::list_cf(opts, path)
.unwrap_or_default()

View File

@@ -26,7 +26,7 @@ pub struct Options {
#[implement(super::Map)]
#[tracing::instrument(
name = "compact",
level = "info"
level = "info",
skip(self),
fields(%self),
)]

View File

@@ -141,6 +141,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "referencedevents",
..descriptor::RANDOM
},
Descriptor {
name: "registrationtoken_info",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomid_invitedcount",
..descriptor::RANDOM_SMALL
@@ -386,6 +390,14 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "userid_suspension",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_lock",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_logindisabled",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_presenceid",
..descriptor::RANDOM_SMALL

View File

@@ -113,7 +113,7 @@ fn drop(&mut self) {
}
#[implement(Pool)]
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "debug")]
pub(crate) fn close(&self) {
let workers = take(&mut *self.workers.lock());
@@ -147,8 +147,8 @@ pub(crate) fn close(&self) {
.map(|result| result.map_err(Error::from_panic))
.enumerate()
.for_each(|(id, result)| match result {
| Ok(()) => trace!(?id, "worker joined"),
| Err(error) => error!(?id, "worker joined with error: {error}"),
| Ok(()) => trace!(%id, "worker joined"),
| Err(error) => error!(%id, "worker joined with error: {error}"),
});
}
@@ -297,7 +297,7 @@ fn worker_init(&self, id: usize) {
}
debug!(
?group,
%group,
affinity = ?affinity.collect::<Vec<_>>(),
"worker ready"
);

View File

@@ -105,8 +105,8 @@ pub(super) fn configure(server: &Arc<Server>) -> (usize, Vec<usize>, Vec<usize>)
.unwrap_or("None"),
?worker_counts,
?queue_sizes,
?total_workers,
stream_width = ?stream::automatic_width(),
%total_workers,
stream_width = %stream::automatic_width(),
"Frontend topology",
);
@@ -139,13 +139,13 @@ fn update_stream_width(server: &Arc<Server>, num_queues: usize, total_workers: u
let (old_width, new_width) = stream::set_width(req_width);
let (old_amp, new_amp) = stream::set_amplification(req_amp);
debug!(
scale = ?config.stream_width_scale,
?num_queues,
?req_width,
?old_width,
?new_width,
?old_amp,
?new_amp,
scale = %config.stream_width_scale,
%num_queues,
%req_width,
%old_width,
%new_width,
%old_amp,
%new_amp,
"Updated global stream width"
);
}

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_macros"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -2,15 +2,12 @@
name = "conduwuit"
default-run = "conduwuit"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
metadata.crane.workspace = true
@@ -23,14 +20,13 @@ crate-type = [
[package.metadata.deb]
name = "continuwuity"
maintainer = "continuwuity developers <contact@continuwuity.org>"
copyright = "2024, continuwuity developers"
maintainer = "Continuwuity Team and contributors <team@continuwuity.org>"
license-file = ["../../LICENSE", "3"]
depends = "$auto, ca-certificates"
breaks = ["conduwuit (<<0.5.0)"]
replaces = ["conduwuit (<<0.5.0)"]
extended-description = """\
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."""
section = "net"
priority = "optional"
conf-files = ["/etc/conduwuit/conduwuit.toml"]

View File

@@ -6,6 +6,7 @@
debug_warn, err,
log::{ConsoleFormat, ConsoleWriter, LogLevelReloadHandles, capture, fmt_span},
result::UnwrapOrErr,
warn,
};
#[cfg(feature = "otlp_telemetry")]
use opentelemetry::trace::TracerProvider;
@@ -85,7 +86,7 @@ pub(crate) fn init(
let exporter = match config.otlp_protocol.as_str() {
| "grpc" => opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
.with_protocol(opentelemetry_otlp::Protocol::Grpc) // TODO: build from env when 0.32 is released
.build()
.expect("Failed to create OTLP gRPC exporter"),
| "http" => opentelemetry_otlp::SpanExporter::builder()
@@ -93,7 +94,7 @@ pub(crate) fn init(
.build()
.expect("Failed to create OTLP HTTP exporter"),
| protocol => {
debug_warn!(
warn!(
"Invalid OTLP protocol '{}', falling back to HTTP. Valid options are \
'http' or 'grpc'.",
protocol

View File

@@ -50,7 +50,8 @@ pub fn run_with_args(args: &Args) -> Result<()> {
#[tracing::instrument(
name = "main",
parent = None,
skip_all
skip_all,
level = "info"
)]
async fn async_main(server: &Arc<Server>) -> Result<(), Error> {
extern crate conduwuit_router as router;

View File

@@ -6,7 +6,7 @@
use super::server::Server;
#[cfg(unix)]
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub(super) async fn signal(server: Arc<Server>) {
use signal::unix;
use unix::SignalKind;
@@ -39,13 +39,13 @@ pub(super) async fn signal(server: Arc<Server>) {
};
if let Err(e) = result {
debug_error!(?sig, "signal: {e}");
debug_error!(%sig, "signal: {e}");
}
}
}
#[cfg(not(unix))]
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub(super) async fn signal(server: Arc<Server>) {
loop {
tokio::select! {

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_router"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -102,13 +102,13 @@ fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Respons
let reason = status.canonical_reason().unwrap_or("Unknown Reason");
if status.is_server_error() {
error!(method = ?method, uri = ?uri, "{code} {reason}");
error!(%method, %uri, "{code} {reason}");
} else if status.is_client_error() {
debug_error!(method = ?method, uri = ?uri, "{code} {reason}");
debug_error!(%method, %uri, "{code} {reason}");
} else if status.is_redirection() {
debug!(method = ?method, uri = ?uri, "{code} {reason}");
debug!(%method, %uri, "{code} {reason}");
} else {
trace!(method = ?method, uri = ?uri, "{code} {reason}");
trace!(%method, %uri, "{code} {reason}");
}
if status == StatusCode::METHOD_NOT_ALLOWED {

View File

@@ -19,7 +19,7 @@
use crate::serve;
/// Main loop base
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub(crate) async fn run(services: Arc<Services>) -> Result<()> {
let server = &services.server;
debug!("Start");
@@ -58,7 +58,7 @@ pub(crate) async fn run(services: Arc<Services>) -> Result<()> {
}
/// Async initializations
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub(crate) async fn start(server: Arc<Server>) -> Result<Arc<Services>> {
debug!("Starting...");
@@ -73,7 +73,7 @@ pub(crate) async fn start(server: Arc<Server>) -> Result<Arc<Services>> {
}
/// Async destructions
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
pub(crate) async fn stop(services: Arc<Services>) -> Result<()> {
debug!("Shutting down...");
@@ -108,7 +108,7 @@ pub(crate) async fn stop(services: Arc<Services>) -> Result<()> {
Ok(())
}
#[tracing::instrument(skip_all)]
#[tracing::instrument(skip_all, level = "info")]
async fn signal(server: Arc<Server>, tx: Sender<()>, handle: axum_server::Handle) {
server
.clone()
@@ -126,7 +126,7 @@ async fn handle_shutdown(server: Arc<Server>, tx: Sender<()>, handle: axum_serve
let timeout = Duration::from_secs(timeout);
debug!(
?timeout,
handle_active = ?server.metrics.requests_handle_active.load(Ordering::Relaxed),
handle_active = %server.metrics.requests_handle_active.load(Ordering::Relaxed),
"Notifying for graceful shutdown"
);

View File

@@ -1,9 +1,7 @@
[package]
name = "conduwuit_service"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -122,7 +122,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
let room_tag = self.services.server.config.admin_room_tag.as_str();
if !room_tag.is_empty() {
if let Err(e) = self.set_room_tag(&room_id, user_id, room_tag).await {
error!(?room_id, ?user_id, ?room_tag, "Failed to set tag for admin grant: {e}");
error!(%room_id, %user_id, %room_tag, "Failed to set tag for admin grant: {e}");
}
}

View File

@@ -96,7 +96,7 @@ async fn worker(self: Arc<Self>) -> Result<()> {
}
if let Err(e) = self.check().await {
warn!(%e, "Failed to check for announcements");
warn!(?e, "Failed to check for announcements");
}
}

182
src/service/antispam/mod.rs Normal file
View File

@@ -0,0 +1,182 @@
use std::{fmt::Debug, sync::Arc};
use async_trait::async_trait;
use conduwuit::{Result, config::Antispam, debug};
use ruma::{OwnedRoomId, OwnedUserId, draupnir_antispam, meowlnir_antispam};
use crate::{client, config, sending, service::Dep};
struct Services {
config: Dep<config::Service>,
client: Dep<client::Service>,
}
pub struct Service {
services: Services,
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
client: args.depend::<client::Service>("client"),
config: args.depend::<config::Service>("config"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
async fn send_antispam_request<T>(
&self,
base_url: &str,
secret: &str,
request: T,
) -> Result<T::IncomingResponse>
where
T: ruma::api::OutgoingRequest + Debug + Send,
{
sending::antispam::send_antispam_request(
&self.services.client.appservice,
base_url,
secret,
request,
)
.await
}
/// Checks with the antispam service whether `inviter` may invite `invitee`
/// to `room_id`.
///
/// If no antispam service is configured, this always returns `Ok(())`.
/// If an error is returned, the invite should be blocked - the antispam
/// service was unreachable, or refused the invite.
pub async fn user_may_invite(
&self,
inviter: OwnedUserId,
invitee: OwnedUserId,
room_id: OwnedRoomId,
) -> Result<()> {
if let Some(config) = &self.services.config.antispam {
let result = if let Some(meowlnir) = &config.meowlnir {
debug!(?room_id, ?inviter, ?invitee, "Asking meowlnir for user_may_invite");
self.send_antispam_request(
meowlnir.base_url.as_str(),
&meowlnir.secret,
meowlnir_antispam::user_may_invite::v1::Request::new(
meowlnir.management_room.clone(),
inviter,
invitee,
room_id,
),
)
.await
.inspect(|_| debug!("meowlnir allowed the invite"))
.inspect_err(|e| debug!("meowlnir denied the invite: {e:?}"))
.map(|_| ())
} else if let Some(draupnir) = &config.draupnir {
debug!(?room_id, ?inviter, ?invitee, "Asking draupnir for user_may_invite");
self.send_antispam_request(
draupnir.base_url.as_str(),
&draupnir.secret,
draupnir_antispam::user_may_invite::v1::Request::new(
room_id, inviter, invitee,
),
)
.await
.inspect(|_| debug!("draupnir allowed the invite"))
.inspect_err(|e| debug!("draupnir denied the invite: {e:?}"))
.map(|_| ())
} else {
Ok(())
};
return result;
}
Ok(())
}
/// Checks with the antispam service whether `user_id` may join `room_id`.
pub async fn user_may_join_room(
&self,
user_id: OwnedUserId,
room_id: OwnedRoomId,
is_invited: bool,
) -> Result<()> {
if let Some(config) = &self.services.config.antispam {
let result = if let Some(meowlnir) = &config.meowlnir {
debug!(?room_id, ?user_id, ?is_invited, "Asking meowlnir for user_may_join_room");
self.send_antispam_request(
meowlnir.base_url.as_str(),
&meowlnir.secret,
meowlnir_antispam::user_may_join_room::v1::Request::new(
meowlnir.management_room.clone(),
user_id,
room_id,
is_invited,
),
)
.await
.inspect(|_| debug!("meowlnir allowed the join"))
.inspect_err(|e| debug!("meowlnir denied the join: {e:?}"))
.map(|_| ())
} else if let Some(draupnir) = &config.draupnir {
debug!(?room_id, ?user_id, ?is_invited, "Asking draupnir for user_may_join_room");
self.send_antispam_request(
draupnir.base_url.as_str(),
&draupnir.secret,
draupnir_antispam::user_may_join_room::v1::Request::new(
user_id, room_id, is_invited,
),
)
.await
.inspect(|_| debug!("draupnir allowed the join"))
.inspect_err(|e| debug!("draupnir denied the join: {e:?}"))
.map(|_| ())
} else {
Ok(())
};
return result;
}
Ok(())
}
/// Checks with Meowlnir whether the incoming federated `make_join` request
/// should be allowed. Applies the `fi.mau.spam_checker` join rule.
pub async fn meowlnir_accept_make_join(
&self,
room_id: OwnedRoomId,
user_id: OwnedUserId,
) -> Result<()> {
if let Some(Antispam { meowlnir: Some(meowlnir), .. }) = &self.services.config.antispam {
debug!(?room_id, ?user_id, "Asking meowlnir for accept_make_join");
self.send_antispam_request(
meowlnir.base_url.as_str(),
&meowlnir.secret,
meowlnir_antispam::accept_make_join::v1::Request::new(
meowlnir.management_room.clone(),
user_id,
room_id,
),
)
.await
.inspect(|_| debug!("meowlnir allowed the make_join"))
.inspect_err(|e| debug!("meowlnir denied the make_join: {e:?}"))
.map(|_| ())
} else {
Ok(())
}
}
/// Returns whether all joins should be checked with Meowlnir.
/// Is always false if Meowlnir is not configured.
pub fn check_all_joins(&self) -> bool {
if let Some(Antispam { meowlnir: Some(cfg), .. }) = &self.services.config.antispam {
cfg.check_all_joins
} else {
false
}
}
}

View File

@@ -88,7 +88,7 @@ async fn perform<T>(
let url = request.url().clone();
let method = request.method().clone();
debug!(?method, ?url, "Sending request");
debug!(%method, %url, "Sending request");
match client.execute(request).await {
| Ok(response) => handle_response::<T>(dest, actual, &method, &url, response).await,
| Err(error) =>
@@ -144,9 +144,9 @@ async fn into_http_response(
) -> Result<http::Response<Bytes>> {
let status = response.status();
trace!(
?status, ?method,
request_url = ?url,
response_url = ?response.url(),
%status, %method,
request_url = %url,
response_url = %response.url(),
"Received response from {}",
actual.string(),
);
@@ -196,9 +196,9 @@ fn handle_error(
debug_warn!("{e:?}");
} else if e.is_redirect() {
debug_error!(
method = ?method,
url = ?url,
final_url = ?e.url(),
%method,
%url,
final_url = e.url().map(tracing::field::display),
"Redirect loop {}: {}",
actual.host,
e,

View File

@@ -18,7 +18,6 @@ pub struct Service {
pub server_user: OwnedUserId,
pub admin_alias: OwnedRoomAliasId,
pub turn_secret: String,
pub registration_token: Option<String>,
}
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
@@ -41,19 +40,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
},
);
let registration_token = config.registration_token_file.as_ref().map_or_else(
|| config.registration_token.clone(),
|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
}) else {
return config.registration_token.clone();
};
Some(token.trim().to_owned())
},
);
Ok(Arc::new(Self {
db,
server: args.server.clone(),
@@ -66,7 +52,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
)
.expect("@conduit:server_name is valid"),
turn_secret,
registration_token,
}))
}

View File

@@ -124,16 +124,16 @@ pub(super) async fn search_file_metadata(
.next()
.map(string_from_bytes)
.transpose()
.map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?;
.map_err(|e| err!(Database(error!(%mxc, "Content-type is invalid: {e}"))))?;
let content_disposition = parts
.next()
.map(Some)
.ok_or_else(|| err!(Database(error!(?mxc, "Media ID in db is invalid."))))?
.ok_or_else(|| err!(Database(error!(%mxc, "Media ID in db is invalid."))))?
.filter(|bytes| !bytes.is_empty())
.map(string_from_bytes)
.transpose()
.map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?
.map_err(|e| err!(Database(error!(%mxc, "Content-type is invalid: {e}"))))?
.as_deref()
.map(str::parse)
.transpose()?;

View File

@@ -118,14 +118,14 @@ pub async fn delete(&self, mxc: &Mxc<'_>) -> Result<()> {
match self.db.search_mxc_metadata_prefix(mxc).await {
| Ok(keys) => {
for key in keys {
trace!(?mxc, "MXC Key: {key:?}");
debug_info!(?mxc, "Deleting from filesystem");
trace!(%mxc, "MXC Key: {key:?}");
debug_info!(%mxc, "Deleting from filesystem");
if let Err(e) = self.remove_media_file(&key).await {
debug_error!(?mxc, "Failed to remove media file: {e}");
debug_error!(%mxc, "Failed to remove media file: {e}");
}
debug_info!(?mxc, "Deleting from database");
debug_info!(%mxc, "Deleting from database");
self.db.delete_file_mxc(mxc).await;
}
@@ -148,7 +148,7 @@ pub async fn delete_from_user(&self, user: &UserId) -> Result<usize> {
for mxc in mxcs {
let Ok(mxc) = mxc.as_str().try_into().inspect_err(|e| {
debug_error!(?mxc, "Failed to parse MXC URI from database: {e}");
debug_error!(%mxc, "Failed to parse MXC URI from database: {e}");
}) else {
continue;
};
@@ -210,7 +210,7 @@ pub async fn get_all_mxcs(&self) -> Result<Vec<OwnedMxcUri>> {
let Some(mxc_s) = mxc else {
debug_warn!(
?mxc,
mxc,
"Parsed MXC URL unicode bytes from database but is still invalid"
);
continue;
@@ -256,7 +256,7 @@ pub async fn delete_all_media_within_timeframe(
let Some(mxc_s) = mxc else {
debug_warn!(
?mxc,
mxc,
"Parsed MXC URL unicode bytes from database but is still invalid"
);
continue;

View File

@@ -71,10 +71,10 @@ async fn request_url_preview(&self, url: &Url) -> Result<UrlPreviewData> {
let client = &self.services.client.url_preview;
let response = client.head(url.as_str()).send().await?;
debug!(?url, "URL preview response headers: {:?}", response.headers());
debug!(%url, "URL preview response headers: {:?}", response.headers());
if let Some(remote_addr) = response.remote_addr() {
debug!(?url, "URL preview response remote address: {:?}", remote_addr);
debug!(%url, "URL preview response remote address: {:?}", remote_addr);
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
if !self.services.client.valid_cidr_range(&ip) {

View File

@@ -247,7 +247,7 @@ async fn handle_location(
) -> Result<FileMeta> {
self.location_request(location).await.map_err(|error| {
err!(Request(NotFound(
debug_warn!(%mxc, ?user, ?location, ?error, "Fetching media from location failed")
debug_warn!(%mxc, user = user.map(tracing::field::display), ?location, ?error, "Fetching media from location failed")
)))
})
}
@@ -320,7 +320,7 @@ fn handle_federation_error(
) -> Error {
let fallback = || {
err!(Request(NotFound(
debug_error!(%mxc, ?user, ?server, ?error, "Remote media not found")
debug_error!(%mxc, user = user.map(tracing::field::display), server = server.map(tracing::field::display), ?error, "Remote media not found")
)))
};

View File

@@ -120,7 +120,7 @@ async fn get_thumbnail_generate(
let mut cursor = std::io::Cursor::new(&mut thumbnail_bytes);
thumbnail
.write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|error| err!(error!(?error, "Error writing PNG thumbnail.")))?;
.map_err(|error| err!(error!(%error, "Error writing PNG thumbnail.")))?;
// Save thumbnail in database so we don't have to generate it again next time
let thumbnail_key = self.db.create_file_metadata(

View File

@@ -677,7 +677,7 @@ async fn populate_userroomid_leftstate_table(services: &Services) -> Result {
shortstatehash_cache.insert(room_id.to_owned(), shortstatehash);
shortstatehash
} else {
warn!(?room_id, ?user_id, "room has no shortstatehash");
warn!(%room_id, %user_id, "room has no shortstatehash");
return Ok((total, fixed, shortstatehash_cache));
};
@@ -698,8 +698,8 @@ async fn populate_userroomid_leftstate_table(services: &Services) -> Result {
},
| Err(_) => {
warn!(
?room_id,
?user_id,
%room_id,
%user_id,
"room cached as left has no leave event for user, removing \
cache entry"
);

View File

@@ -1,6 +1,8 @@
#![type_length_limit = "8192"]
#![allow(refining_impl_trait)]
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_database as database;
mod manager;
mod migrations;
mod service;
@@ -10,6 +12,7 @@
pub mod account_data;
pub mod admin;
pub mod announcements;
pub mod antispam;
pub mod appservice;
pub mod client;
pub mod config;
@@ -21,6 +24,7 @@
pub mod moderation;
pub mod presence;
pub mod pusher;
pub mod registration_tokens;
pub mod resolver;
pub mod rooms;
pub mod sending;
@@ -30,9 +34,6 @@
pub mod uiaa;
pub mod users;
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_database as database;
use ctor::{ctor, dtor};
pub(crate) use service::{Args, Dep, Service};

View File

@@ -198,11 +198,11 @@ pub async fn unset_all_presence(&self) {
presence.presence,
PresenceState::Unavailable | PresenceState::Online | PresenceState::Busy
) {
trace!(?user_id, ?presence, "Skipping user");
trace!(%user_id, ?presence, "Skipping user");
continue;
}
trace!(?user_id, ?presence, "Resetting presence to offline");
trace!(%user_id, ?presence, "Resetting presence to offline");
_ = self
.set_presence(

View File

@@ -0,0 +1,129 @@
use std::{sync::Arc, time::SystemTime};
use conduwuit::utils::{
self,
stream::{ReadyExt, TryIgnore},
};
use database::{Database, Deserialized, Json, Map};
use futures::Stream;
use ruma::OwnedUserId;
use serde::{Deserialize, Serialize};
pub(super) struct Data {
registrationtoken_info: Arc<Map>,
}
/// Metadata of a registration token.
#[derive(Debug, Serialize, Deserialize)]
pub struct DatabaseTokenInfo {
/// The admin user who created this token.
pub creator: OwnedUserId,
/// The number of times this token has been used to create an account.
pub uses: u64,
/// When this token will expire, if it expires.
pub expires: Option<TokenExpires>,
}
impl DatabaseTokenInfo {
pub(super) fn new(creator: OwnedUserId, expires: Option<TokenExpires>) -> Self {
Self { creator, uses: 0, expires }
}
/// Determine whether this token info represents a valid token, i.e. one
/// that has not expired according to its [`Self::expires`] property. If
/// [`Self::expires`] is [`None`], this function will always return `true`.
#[must_use]
pub fn is_valid(&self) -> bool {
match self.expires {
| Some(TokenExpires::AfterUses(max_uses)) => self.uses < max_uses,
| Some(TokenExpires::AfterTime(expiry_time)) => {
let now = SystemTime::now();
expiry_time >= now
},
| None => true,
}
}
}
impl std::fmt::Display for DatabaseTokenInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Token created by {} and used {} times. ", &self.creator, self.uses)?;
if let Some(expires) = &self.expires {
write!(f, "{expires}.")?;
} else {
write!(f, "Never expires.")?;
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum TokenExpires {
AfterUses(u64),
AfterTime(SystemTime),
}
impl std::fmt::Display for TokenExpires {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
| Self::AfterUses(max_uses) => write!(f, "Expires after {max_uses} uses"),
| Self::AfterTime(max_age) => {
let now = SystemTime::now();
let formatted_expiry = utils::time::format(*max_age, "%+");
match max_age.duration_since(now) {
| Ok(duration) => write!(
f,
"Expires in {} ({formatted_expiry})",
utils::time::pretty(duration)
),
| Err(_) => write!(f, "Expired at {formatted_expiry}"),
}
},
}
}
}
impl Data {
pub(super) fn new(db: &Arc<Database>) -> Self {
Self {
registrationtoken_info: db["registrationtoken_info"].clone(),
}
}
/// Associate a registration token with its metadata in the database.
pub(super) fn save_token(&self, token: &str, info: &DatabaseTokenInfo) {
self.registrationtoken_info.raw_put(token, Json(info));
}
/// Delete a registration token.
pub(super) fn revoke_token(&self, token: &str) { self.registrationtoken_info.remove(token); }
/// Look up a registration token's metadata.
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<DatabaseTokenInfo> {
self.registrationtoken_info
.get(token)
.await
.deserialized()
.ok()
}
/// Iterate over all valid tokens and delete expired ones.
pub(super) fn iterate_and_clean_tokens(
&self,
) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
self.registrationtoken_info
.stream()
.ignore_err()
.ready_filter_map(|item: (&str, DatabaseTokenInfo)| {
if item.1.is_valid() {
Some(item)
} else {
self.registrationtoken_info.remove(item.0);
None
}
})
}
}

View File

@@ -0,0 +1,172 @@
mod data;
use std::sync::Arc;
use conduwuit::{Err, Result, utils};
use data::Data;
pub use data::{DatabaseTokenInfo, TokenExpires};
use futures::{Stream, StreamExt, stream};
use ruma::OwnedUserId;
use crate::{Dep, config};
const RANDOM_TOKEN_LENGTH: usize = 16;
pub struct Service {
db: Data,
services: Services,
}
struct Services {
config: Dep<config::Service>,
}
/// A validated registration token which may be used to create an account.
#[derive(Debug)]
pub struct ValidToken {
pub token: String,
pub source: ValidTokenSource,
}
impl std::fmt::Display for ValidToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "`{}` --- {}", self.token, &self.source)
}
}
impl PartialEq<str> for ValidToken {
fn eq(&self, other: &str) -> bool { self.token == other }
}
/// The source of a valid database token.
#[derive(Debug)]
pub enum ValidTokenSource {
/// The static token set in the homeserver's config file, which is
/// always valid.
ConfigFile,
/// A database token which has been checked to be valid.
Database(DatabaseTokenInfo),
}
impl std::fmt::Display for ValidTokenSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
| Self::ConfigFile => write!(f, "Token defined in config."),
| Self::Database(info) => info.fmt(f),
}
}
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
db: Data::new(args.db),
services: Services {
config: args.depend::<config::Service>("config"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Issue a new registration token and save it in the database.
pub fn issue_token(
&self,
creator: OwnedUserId,
expires: Option<TokenExpires>,
) -> (String, DatabaseTokenInfo) {
let token = utils::random_string(RANDOM_TOKEN_LENGTH);
let info = DatabaseTokenInfo::new(creator, expires);
self.db.save_token(&token, &info);
(token, info)
}
/// Get the registration token set in the config file, if it exists.
pub fn get_config_file_token(&self) -> Option<ValidToken> {
self.services
.config
.registration_token
.clone()
.map(|token| ValidToken {
token,
source: ValidTokenSource::ConfigFile,
})
}
/// Validate a registration token.
pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
// Check the registration token in the config first
if self
.get_config_file_token()
.is_some_and(|valid_token| valid_token == *token)
{
return Some(ValidToken {
token,
source: ValidTokenSource::ConfigFile,
});
}
// Now check the database
if let Some(token_info) = self.db.lookup_token_info(&token).await
&& token_info.is_valid()
{
return Some(ValidToken {
token,
source: ValidTokenSource::Database(token_info),
});
}
// Otherwise it's not valid
None
}
/// Mark a valid token as having been used to create a new account.
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
match source {
| ValidTokenSource::ConfigFile => {
// we don't track uses of the config file token, do nothing
},
| ValidTokenSource::Database(mut info) => {
info.uses = info.uses.saturating_add(1);
self.db.save_token(&token, &info);
},
}
}
/// Try to revoke a valid token.
///
/// Note that some tokens (like the one set in the config file) cannot be
/// revoked.
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
match source {
| ValidTokenSource::ConfigFile => {
// the config file token cannot be revoked
Err!(
"The token set in the config file cannot be revoked. Edit the config file \
to change it."
)
},
| ValidTokenSource::Database(_) => {
self.db.revoke_token(&token);
Ok(())
},
}
}
/// Iterate over all valid registration tokens.
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
let db_tokens = self
.db
.iterate_and_clean_tokens()
.map(|(token, info)| ValidToken {
token: token.to_owned(),
source: ValidTokenSource::Database(info),
});
stream::iter(self.get_config_file_token()).chain(db_tokens)
}
}

View File

@@ -151,7 +151,7 @@ async fn get_auth_chain_outer(
let auth_chain = self.get_auth_chain_inner(room_id, event_id).await?;
self.cache_auth_chain_vec(vec![shortid], auth_chain.as_slice());
debug!(
?event_id,
%event_id,
elapsed = ?started.elapsed(),
"Cache missed event"
);
@@ -188,18 +188,18 @@ async fn get_auth_chain_inner(
let mut found = HashSet::new();
while let Some(event_id) = todo.pop_front() {
trace!(?event_id, "processing auth event");
trace!(%event_id, "processing auth event");
match self.services.timeline.get_pdu(&event_id).await {
| Err(e) => {
debug_error!(?event_id, ?e, "Could not find pdu mentioned in auth events");
debug_error!(%event_id, ?e, "Could not find pdu mentioned in auth events");
},
| Ok(pdu) => {
if let Some(claimed_room_id) = pdu.room_id.clone() {
if claimed_room_id != *room_id {
return Err!(Request(Forbidden(error!(
?event_id,
?room_id,
%event_id,
%room_id,
wrong_room_id = ?pdu.room_id.unwrap(),
"auth event for incorrect room"
))));
@@ -214,7 +214,7 @@ async fn get_auth_chain_inner(
.await;
if found.insert(sauthevent) {
trace!(?event_id, ?auth_event, "adding auth event to processing queue");
trace!(%event_id, ?auth_event, "adding auth event to processing queue");
todo.push_back(auth_event.clone());
}

View File

@@ -104,9 +104,9 @@ fn check_room_id<Pdu: Event>(room_id: &RoomId, pdu: &Pdu) -> Result {
.is_some_and(|claimed_room_id| claimed_room_id != room_id)
{
return Err!(Request(InvalidParam(error!(
pdu_event_id = ?pdu.event_id(),
pdu_room_id = ?pdu.room_id(),
?room_id,
pdu_event_id = %pdu.event_id(),
pdu_room_id = pdu.room_id().map(tracing::field::display),
%room_id,
"Found event from room in room",
))));
}

View File

@@ -31,7 +31,7 @@
/// contacted for whatever reason, Err(e) is returned, which generally is a
/// fail-open operation.
#[implement(super::Service)]
#[tracing::instrument(skip(self, pdu, pdu_json, room_id))]
#[tracing::instrument(skip(self, pdu, pdu_json, room_id), level = "info")]
pub async fn ask_policy_server(
&self,
pdu: &PduEvent,
@@ -184,7 +184,7 @@ pub async fn ask_policy_server(
/// Asks a remote policy server for a signature on this event.
/// If the policy server signs this event, the original data is mutated.
#[implement(super::Service)]
#[tracing::instrument(skip_all, fields(event_id=%pdu.event_id(), via=%via))]
#[tracing::instrument(skip_all, fields(event_id=%pdu.event_id(), via=%via), level = "info")]
pub async fn fetch_policy_server_signature(
&self,
pdu: &PduEvent,

View File

@@ -28,7 +28,7 @@ pub async fn redact_pdu<Pdu: Event + Send + Sync>(
.await
.map(Event::into_pdu)
.map_err(|e| {
err!(Database(error!(?pdu_id, ?event_id, ?e, "PDU ID points to invalid PDU.")))
err!(Database(error!(?pdu_id, %event_id, ?e, "PDU ID points to invalid PDU.")))
})?;
if let Ok(content) = pdu.get_content::<ExtractBody>() {
@@ -48,7 +48,7 @@ pub async fn redact_pdu<Pdu: Event + Send + Sync>(
pdu.redact(&room_version_id, reason.to_value())?;
let obj = utils::to_canonical_object(&pdu).map_err(|e| {
err!(Database(error!(?event_id, ?e, "Failed to convert PDU to canonical JSON")))
err!(Database(error!(%event_id, ?e, "Failed to convert PDU to canonical JSON")))
})?;
self.replace_pdu(&pdu_id, &obj).await

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