Compare commits

...

34 Commits

Author SHA1 Message Date
rooot
c91e9951a6 docs(livekit): document nginx websockets too
Signed-off-by: rooot <hey@rooot.gay>
2026-02-16 04:39:18 +01:00
rooot
de6a44d272 docs(livekit): fix port in caddy config example
Signed-off-by: rooot <hey@rooot.gay>
2026-02-16 04:32:35 +01:00
rooot
c2ea303363 docs(livekit): add nginx proxy example
Signed-off-by: rooot <hey@rooot.gay>
2026-02-16 04:32:03 +01:00
Jade Ellis
cacd8681d1 docs: Update & apply feedback 2026-02-16 02:55:26 +00:00
burgundia
b095518e6f Update documentation to feature LiveKit-related configuration options present in continuwuity.toml 2026-02-16 02:35:41 +00:00
Jade Ellis
a91add4aca docs: Apply feedback 2026-02-16 02:35:41 +00:00
Jade Ellis
7fec48423a chore: Style 2026-02-16 02:35:40 +00:00
Jade Ellis
2f6b7c7a40 docs: Update TURN guide 2026-02-16 02:35:40 +00:00
Jade Ellis
48ab6adec1 chore: Apply review comments 2026-02-16 02:35:40 +00:00
Jade Ellis
592244d5aa docs: Last dead link 2026-02-16 02:35:40 +00:00
Jade Ellis
091893f8bc fix: oops 2026-02-16 02:35:40 +00:00
Jade Ellis
6eba6a838e docs: Fix broken links 2026-02-16 02:35:39 +00:00
Jade Ellis
1a11c784f5 docs: Write up how to set up LiveKit calling 2026-02-16 02:35:38 +00:00
Renovate Bot
55ccfdb973 chore(deps): update rust-patch-updates 2026-02-15 23:04:26 +00:00
Henry-Hiles
a9a39e6d5e fix: Update regex for web template in uwulib build 2026-02-15 23:04:05 +00:00
Jade Ellis
38bf1ccbcc refactor: Drop duplicate clone 2026-02-15 23:03:23 +00:00
timedout
b7a8cbdb42 feat: Exclude empty rooms from !admin rooms list by default
Reviewed-By: Ginger <ginger@gingershaped.computer>
2026-02-15 23:03:23 +00:00
Ginger
4e1dac32a5 fix: Don't panic when running startup admin commands 2026-02-15 17:32:26 -05:00
timedout
7b21c3fd9f chore: Update changelog 2026-02-15 20:39:14 +00:00
timedout
f566ca1b93 chore: Release 0.5.5 2026-02-15 20:31:58 +00:00
timedout
debe411e23 fix(ci): Work around LLVM issue & dynamically select clang pkg version 2026-02-15 20:27:55 +00:00
timedout
dc0d6a9220 fix: Install clang-23 specifically
clang (clang-22) is busted
2026-02-15 19:09:33 +00:00
timedout
2efdb6fb0d fix: Work around https://github.com/llvm/llvm-project/issues/153385 2026-02-15 18:55:17 +00:00
Ginger
576348a445 fix: Set default value of allow_registration to true 2026-02-15 18:05:42 +00:00
Ginger
f322b6dca0 chore: News fragment 2026-02-15 18:05:42 +00:00
Ginger
a1ed77a99c feat: Add a link to the clients list on matrix.org 2026-02-15 18:05:42 +00:00
Ginger
01b5dffeee feat: Default index page improvements
- Add project logo to footer and favicon
- Display different messages depending on if first-run mode is active
2026-02-15 18:05:42 +00:00
Ginger
ea3c00da43 chore: Clippy fixes 2026-02-15 18:05:42 +00:00
Ginger
047eba0442 feat: Improve the initial setup experience
- Issue a single-use token for initial account creation
- Disable registration through other methods until the first account is made
- Print helpful instructions to the console on the first run
- Improve the welcome message sent in the admin room on first run
2026-02-15 18:05:42 +00:00
Ginger
11a088be5d feat: Stop logging announcements to the console 2026-02-15 18:05:42 +00:00
Ginger
dc6bd4e541 fix: Silence unnecessary policy server errors in debug builds 2026-02-15 18:05:42 +00:00
Ginger
2bf9207cc4 feat: Add skeleton first-run service 2026-02-15 18:05:42 +00:00
Ginger
b2a87e2fb9 refactor: Add support for multiple static tokens to registration token service 2026-02-15 18:05:42 +00:00
timedout
7d0686f33c fix: Error response can leak appservice token
Reviewed-By: Ginger <ginger@gingershaped.computer>
Reviewed-By: Jade Ellis <jade@ellis.link>
2026-02-15 17:58:48 +00:00
51 changed files with 1251 additions and 620 deletions

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
container: ["ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable"]
container: [ "ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable" ]
container:
image: "ghcr.io/tcpipuk/act-runner:${{ matrix.container }}"
@@ -30,6 +30,28 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
- name: Work around llvm-project#153385
id: llvm-workaround
run: |
if [ -f /usr/share/apt/default-sequoia.config ]; then
echo "Applying workaround for llvm-project#153385"
mkdir -p /etc/crypto-policies/back-ends/
cp /usr/share/apt/default-sequoia.config /etc/crypto-policies/back-ends/apt-sequoia.config
sed -i 's/\(sha1\.second_preimage_resistance = \)2026-02-01/\12026-06-01/' /etc/crypto-policies/back-ends/apt-sequoia.config
else
echo "No workaround needed for llvm-project#153385"
fi
- name: Pick compatible clang version
id: clang-version
run: |
# both latest need to use clang-23, but oldstable and previous can just use clang
if [[ "${{ matrix.container }}" == "ubuntu-latest" || "${{ matrix.container }}" == "debian-latest" ]]; then
echo "Using clang-23 package for ${{ matrix.container }}"
echo "version=clang-23" >> $GITHUB_OUTPUT
else
echo "Using default clang package for ${{ matrix.container }}"
echo "version=clang" >> $GITHUB_OUTPUT
fi
- name: Checkout repository with full history
uses: actions/checkout@v6
@@ -105,7 +127,7 @@ jobs:
run: |
apt-get update -y
# Build dependencies for rocksdb
apt-get install -y clang liburing-dev
apt-get install -y liburing-dev ${{ steps.clang-version.outputs.version }}
- name: Run cargo-deb
id: cargo-deb

View File

@@ -24,3 +24,5 @@ extend-ignore-re = [
"continuwity" = "continuwuity"
"execuse" = "execuse"
"oltp" = "OTLP"
rememvering = "remembering"

View File

@@ -1,25 +1,65 @@
# Continuwuity v0.5.5 (2026-02-15)
## Features
- Added unstable support for [MSC4406:
`M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
Contributed by @nex ([#1308](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1308))
- Introduce a resolver command to allow flushing a server from the cache or to flush the complete cache. Contributed by
@Omar007 ([#1349](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1349))
- Improved the handling of restricted join rules and improved the performance of local-first joins. Contributed by
@nex. ([#1368](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1368))
- You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be
rejected. Contributed by @trashpanda ([#1372](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1372))
- Improved the first-time setup experience for new homeserver administrators:
- Account registration is disabled on the first run, except for with a new special registration token that is logged
to the console.
- Other helpful information is logged to the console as well, including a giant warning if open registration is
enabled.
- The default index page now says to check the console for setup instructions if no accounts have been created.
- Once the first admin account is created, an improved welcome message is sent to the admin room.
Contributed by @ginger.
## Bugfixes
- Fixed invites sent to other users in the same homeserver not being properly sent down sync. Users with missing or
broken invites should clear their client caches after updating to make them appear. ([#1249](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1249))
- LDAP-enabled servers will no longer have all admins demoted when LDAP-controlled admins are not configured.
Contributed by @Jade ([#1307](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1307))
- Fixed sliding sync not resolving wildcard state key requests, enabling Video/Audio calls in Element X. ([#1370](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1370))
## Misc
- #1344
# Continuwuity v0.5.4 (2026-02-08)
## Features
- The announcement checker will now announce errors it encounters in the first run to the admin room, plus a few other
misc improvements. Contributed by @Jade ([#1288](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1288))
- Drastically improved the performance and reliability of account deactivations. Contributed by @nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
- Drastically improved the performance and reliability of account deactivations. Contributed by
@nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
- Refuse to process requests for and events in rooms that we no longer have any local users in (reduces state resets
and improves performance). Contributed by @nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
and improves performance). Contributed by
@nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
- Added server-specific admin API routes to ban and unban rooms, for use with moderation bots. Contributed by @nex
([#1301](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1301))
## Bugfixes
- Fix the generated configuration containing uncommented optional sections. Contributed by @Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
- Fixed specification non-compliance when handling remote media errors. Contributed by @nex ([#1298](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1298))
- Fix the generated configuration containing uncommented optional sections. Contributed by
@Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
- Fixed specification non-compliance when handling remote media errors. Contributed by
@nex ([#1298](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1298))
- UIAA requests which check for out-of-band success (sent by matrix-js-sdk) will no longer create unhelpful errors in
the logs. Contributed by @ginger ([#1305](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1305))
- Use exists instead of contains to save writing to a buffer in `src/service/users/mod.rs`: `is_login_disabled`.
Contributed
by @aprilgrimoire. ([#1340](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1340))
- Fixed backtraces being swallowed during panics. Contributed by @jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
- Fixed backtraces being swallowed during panics. Contributed by
@jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
- Fixed a potential vulnerability that could allow an evil remote server to return malicious events during the room join
and knock process. Contributed by @nex, reported by violet & [mat](https://matdoes.dev).
- Fixed a race condition that could result in outlier PDUs being incorrectly marked as visible to a remote server.
@@ -28,25 +68,30 @@ ## Bugfixes
## Docs
- Fixed Fedora install instructions. Contributed by @julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
- Fixed Fedora install instructions. Contributed by
@julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
# Continuwuity 0.5.3 (2026-01-12)
## Features
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by @Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by
@Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
## Bugfixes
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by @nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by
@nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
## Docs
- Improve admin command documentation generation. Contributed by @ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
- Improve admin command documentation generation. Contributed by
@ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
## Misc
- Improve timeout-related code for federation and URL previews. Contributed by @Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
- Improve timeout-related code for federation and URL previews. Contributed by
@Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
# Continuwuity 0.5.2 (2026-01-09)
@@ -57,11 +102,14 @@ ## Features
after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is
superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration
tokens. Contributed by @ginger (#783).
- Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
- Implemented a configuration defined admin list independent of the admin room. Contributed by
@Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
Contributed by @nex. ([#1263](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1263))
- Implemented account locking functionality, to complement user suspension. Contributed by @nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex. ([#1271](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1271))
- Implemented account locking functionality, to complement user suspension. Contributed by
@nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by
@nex. ([#1271](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1271))
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (
[#1272](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1272))
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs.
@@ -71,7 +119,8 @@ ## Features
## Bugfixes
- Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
- Fixed unreliable room summary fetching and improved error messages. Contributed by
@nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now
also concurrent. Contributed by @nex. ([#1261](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1261))
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by
@@ -90,9 +139,12 @@ # Continuwuity 0.5.0 (2025-12-30)
## Features
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (
@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
## Bug Fixes
- Don't allow admin room upgrades, as this can break the admin room (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Fix invalid creators in power levels during upgrade to v12 (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Don't allow admin room upgrades, as this can break the admin room (
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Fix invalid creators in power levels during upgrade to v12 (
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))

321
Cargo.lock generated
View File

@@ -617,15 +617,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "backtrace-ext"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
dependencies = [
"backtrace",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -844,15 +835,6 @@ dependencies = [
"toml 0.9.12+spec-1.1.0",
]
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.52"
@@ -968,20 +950,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "compression-codecs"
version = "0.4.36"
@@ -1013,7 +981,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"clap",
"conduwuit_admin",
@@ -1045,7 +1013,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"clap",
"conduwuit_api",
@@ -1066,7 +1034,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"async-trait",
"axum 0.7.9",
@@ -1098,14 +1066,14 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"built",
]
[[package]]
name = "conduwuit_core"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"argon2",
"arrayvec",
@@ -1166,7 +1134,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1184,7 +1152,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -1194,7 +1162,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"axum 0.7.9",
"axum-client-ip",
@@ -1228,8 +1196,9 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"askama 0.14.0",
"async-trait",
"base64 0.22.1",
"blurhash",
@@ -1264,11 +1233,12 @@ dependencies = [
"tracing",
"url",
"webpage",
"yansi",
]
[[package]]
name = "conduwuit_web"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"askama 0.14.0",
"axum 0.7.9",
@@ -1603,41 +1573,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "817fa642fb0ee7fe42e95783e00e0969927b96091bdd4b9b1af082acd943913b"
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
@@ -2058,9 +1993,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
@@ -2073,9 +2008,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
@@ -2083,15 +2018,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
@@ -2100,15 +2035,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
@@ -2117,21 +2052,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
@@ -2141,32 +2076,9 @@ dependencies = [
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "garde"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a74b56a4039a46e8c91cc9d84e8a7df4e1f8b24239ca57d1304b3263cb599b9"
dependencies = [
"compact_str",
"garde_derive",
"smallvec",
]
[[package]]
name = "garde_derive"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7224c08ec489e2840af29ed882b47f7f6ac8f4ce15c275d9fc0d6d1b94578ae6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -2623,12 +2535,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
@@ -2761,12 +2667,6 @@ dependencies = [
"serde",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "itertools"
version = "0.13.0"
@@ -3124,36 +3024,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "miette"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
dependencies = [
"backtrace",
"backtrace-ext",
"cfg-if",
"miette-derive",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -3689,12 +3559,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "owo-colors"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]]
name = "parking"
version = "2.2.1"
@@ -3932,28 +3796,6 @@ dependencies = [
"toml_edit 0.23.10+spec-1.0.0",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -4964,18 +4806,15 @@ dependencies = [
[[package]]
name = "serde-saphyr"
version = "0.0.18"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "191a4f997fef5e095212c5790898516e9567d2d8502c4159317603ff0321e394"
checksum = "29fb2d57f074f415e3ea6905994be8aca2bd7a17f8a0344cc43ed15bf240a547"
dependencies = [
"ahash",
"annotate-snippets",
"base64 0.22.1",
"encoding_rs_io",
"figment",
"garde",
"getrandom 0.3.4",
"miette",
"nohash-hasher",
"num-traits",
"regex",
@@ -4983,7 +4822,6 @@ dependencies = [
"serde",
"serde_json",
"smallvec",
"validator",
"zmij",
]
@@ -5244,12 +5082,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strict"
version = "0.2.0"
@@ -5281,12 +5113,6 @@ dependencies = [
"quote",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subslice"
version = "0.2.3"
@@ -5302,32 +5128,11 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
[[package]]
name = "supports-unicode"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.115"
version = "2.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
dependencies = [
"proc-macro2",
"quote",
@@ -5387,26 +5192,6 @@ dependencies = [
"unicode-width 0.1.14",
]
[[package]]
name = "terminal_size"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix",
"windows-sys 0.60.2",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"unicode-linebreak",
"unicode-width 0.2.2",
]
[[package]]
name = "thingbuf"
version = "0.1.6"
@@ -5981,12 +5766,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@@ -6087,36 +5866,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "validator"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
dependencies = [
"idna",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
dependencies = [
"darling",
"once_cell",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "valuable"
version = "0.1.1"
@@ -6587,7 +6336,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"askama 0.15.4",
"cargo_metadata",

View File

@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.4"
version = "0.5.5"
[workspace.metadata.crane]
name = "conduwuit"
@@ -158,7 +158,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.18"
version = "0.0.19"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -549,6 +549,12 @@ features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.resolv-conf]
version = "0.7.5"
[workspace.dependencies.yansi]
version = "1.0.1"
[workspace.dependencies.askama]
version = "0.14.0"
#
# Patches
#

View File

@@ -1 +0,0 @@
Fixed invites sent to other users in the same homeserver not being properly sent down sync. Users with missing or broken invites should clear their client caches after updating to make them appear.

View File

@@ -1 +0,0 @@
LDAP-enabled servers will no longer have all admins demoted when LDAP-controlled admins are not configured. Contributed by @Jade

View File

@@ -1,2 +0,0 @@
Added unstable support for [MSC4406: `M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
Contributed by @nex

View File

@@ -1 +0,0 @@
Continuwuity will now print information to the console when it detects a deadlock

View File

@@ -1 +0,0 @@
Introduce a resolver command to allow flushing a server from the cache or to flush the complete cache. Contributed by @Omar007

View File

@@ -1 +0,0 @@
Improved the handling of restricted join rules and improved the performance of local-first joins. Contributed by @nex.

View File

@@ -1 +0,0 @@
Fixed sliding sync not resolving wildcard state key requests, enabling Video/Audio calls in Element X.

View File

@@ -1 +0,0 @@
You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be rejected. Contributed by @trashpanda

View File

@@ -433,7 +433,7 @@
# If you would like registration only via token reg, please configure
# `registration_token`.
#
#allow_registration = false
#allow_registration = true
# If registration is enabled, and this setting is true, new users
# registered after the first admin user will be automatically suspended

View File

@@ -15,9 +15,9 @@
"label": "Deploying"
},
{
"type": "file",
"name": "turn",
"label": "TURN"
"type": "dir",
"name": "calls",
"label": "Calls"
},
{
"type": "file",

View File

@@ -2,7 +2,7 @@
{
"text": "Guide",
"link": "/introduction",
"activeMatch": "^/(introduction|configuration|deploying|turn|appservices|maintenance|troubleshooting)"
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting)"
},
{
"text": "Development",

13
docs/calls.mdx Normal file
View File

@@ -0,0 +1,13 @@
# Calls
Matrix supports two types of calls:
- Element Call powered by [MatrixRTC](https://half-shot.github.io/msc-crafter/#msc/4143) and [LiveKit](https://github.com/livekit/livekit)
- Legacy calls, sometimes using Jitsi
Both types of calls are supported by different sets of clients, but most clients are moving towards MatrixRTC / Element Call.
For either one to work correctly, you have to do some additional setup.
- For legacy calls to work, you need to set up a TURN/STUN server. [Read the TURN guide for tips on how to set up coturn](./calls/turn.mdx)
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability, so you might want to configure your TURN server first. [Read the LiveKit guide](./calls/livekit.mdx)

12
docs/calls/_meta.json Normal file
View File

@@ -0,0 +1,12 @@
[
{
"type": "file",
"name": "turn",
"label": "TURN"
},
{
"type": "file",
"name": "livekit",
"label": "MatrixRTC / LiveKit"
}
]

269
docs/calls/livekit.mdx Normal file
View File

@@ -0,0 +1,269 @@
# Matrix RTC/Element Call Setup
:::info
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
:::
## Instructions
### 1. Domain
LiveKit should live on its own domain or subdomain. In this guide we use `livekit.example.com` - this should be replaced with a domain you control.
Make sure the DNS record for the (sub)domain you plan to use is pointed to your server.
### 2. Services
Using LiveKit with Matrix requires two services - Livekit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
You must generate a key and secret to allow the Matrix service to authenticate with LiveKit. `LK_MATRIX_KEY` should be around 20 random characters, and `LK_MATRIX_SECRET` should be around 64. Remember to replace these with the actual values!
:::tip Generating the secrets
LiveKit provides a utility to generate secure random keys
```bash
docker run --rm livekit/livekit-server:latest generate-keys
```
:::
```yaml
services:
lk-jwt-service:
image: ghcr.io/element-hq/lk-jwt-service:latest
container_name: lk-jwt-service
environment:
- LIVEKIT_JWT_BIND=:8081
- LIVEKIT_URL=wss://livekit.example.com
- LIVEKIT_KEY=LK_MATRIX_KEY
- LIVEKIT_SECRET=LK_MATRIX_SECRET
- LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com
restart: unless-stopped
ports:
- "8081:8081"
livekit:
image: livekit/livekit-server:latest
container_name: livekit
command: --config /etc/livekit.yaml
restart: unless-stopped
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
network_mode: "host" # /!\ LiveKit binds to all addresses by default.
# Make sure port 7880 is blocked by your firewall to prevent access bypassing your reverse proxy
# Alternatively, uncomment the lines below and comment `network_mode: "host"` above to specify port mappings.
# ports:
# - "127.0.0.1:7880:7880/tcp"
# - "7881:7881/tcp"
# - "50100-50200:50100-50200/udp"
```
Next, we need to configure LiveKit. In the same directory, create `livekit.yaml` with the following content - remembering to replace `LK_MATRIX_KEY` and `LK_MATRIX_SECRET` with the values you generated:
```yaml
port: 7880
bind_addresses:
- ""
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
use_external_ip: true
enable_loopback_candidate: false
keys:
LK_MATRIX_KEY: LK_MATRIX_SECRET
```
#### Firewall hints
You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firewall. If you use UFW, the commands are: `ufw allow 7881/tcp` and `ufw allow 50100:50200/udp`.
### 3. Telling clients where to find LiveKit
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to your client .well-known file. To do so, in the config section `global.well-known`, add (or modify) the option `rtc_focus_server_urls`.
The variable should be a list of servers serving as MatrixRTC endpoints to serve in the well-known file to the client.
```toml
rtc_focus_server_urls = [
{ type = "livekit", livekit_service_url = "https://livekit.example.com" },
]
```
Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.
#### Serving .well-known manually
If you don't let Continuwuity serve your `.well-known` files, you need to add the following lines to your `.well-known/matrix/client` file, remembering to replace the URL with your own `lk-jwt-service` deployment:
```json
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://livekit.example.com"
}
]
```
The final file should look something like this:
```json
{
"m.homeserver": {
"base_url":"https://matrix.example.com"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://livekit.example.com"
}
]
}
```
### 4. Configure your Reverse Proxy
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
By default, all routes should be forwarded to Livekit with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
- `/sfu/get`
- `/healthz`
- `/get_token`
<details>
<summary>Example caddy config</summary>
```
matrix-rtc.example.com {
# for lk-jwt-service
@lk-jwt-service path /sfu/get* /healthz* /get_token*
route @lk-jwt-service {
reverse_proxy 127.0.0.1:8081
}
# for livekit
reverse_proxy 127.0.0.1:7880
}
```
</details>
<details>
<summary>Example nginx config</summary>
```
server {
server_name matrix-rtc.example.com;
# for lk-jwt-service
location ~ ^/(sfu/get|healthz|get_token) {
proxy_pass http://127.0.0.1:8081$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_buffering off;
}
# for livekit
location / {
proxy_pass http://127.0.0.1:7880$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_buffering off;
# websocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
```
Note that for websockets to work, you need to have this somewhere outside your server block:
```
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
```
</details>
<details>
<summary>Example traefik router</summary>
```
# on LiveKit itself
traefik.http.routers.livekit.rule=Host(`livekit.example.com`)
# on the JWT service
traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))
```
</details>
### 6. Start Everything
Start up the services using your usual method - for example `docker compose up -d`.
## Additional Configuration
### TURN Integration
If you've already set up coturn, there may be a port clash between the two services. To fix this, make sure the `min-port` and `max-port` for coturn so it doesn't overlap with LiveKit's range:
```ini
min-port=50201
max-port=65535
```
To improve LiveKit's reliability, you can configure it to use your coturn server.
Generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want - so set a different one for each thing using your TURN server.
Then configure livekit, making sure to replace `COTURN_SECRET`:
```yaml
# livekit.yaml
rtc:
turn_servers:
- host: coturn.ellis.link
port: 3478
protocol: tcp
secret: "COTURN_SECRET"
- host: coturn.ellis.link
port: 5349
protocol: tls # Only if you've set up TLS in your coturn
secret: "COTURN_SECRET"
- host: coturn.ellis.link
port: 3478
protocol: udp
secret: "COTURN_SECRET"
```
## LiveKit's built in TURN server
Livekit includes a built in TURN server which can be used in place of an external option. This TURN server will only work with Livekit, so you can't use it for legacy Matrix calling - or anything else.
If you don't want to set up a separate TURN server, you can enable this with the following changes:
```yaml
### add this to livekit.yaml ###
turn:
enabled: true
udp_port: 3478
relay_range_start: 50300
relay_range_end: 50400
domain: matrix-rtc.example.com
```
```yaml
### Add these to docker-compose ###
- "3478:3478/udp"
- "50300-50400:50300-50400/udp"
```
### Related Documentation
- [LiveKit GitHub](https://github.com/livekit/livekit)
- [LiveKit Connection Tester](https://livekit.io/connection-test) - use with the token returned by `/sfu/get` or `/get_token`
- [MatrixRTC proposal](https://half-shot.github.io/msc-crafter/#msc/4143)
- [Synapse documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
- [Community guide](https://tomfos.tr/matrix/livekit/)
- [Community guide](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
-

214
docs/calls/turn.mdx Normal file
View File

@@ -0,0 +1,214 @@
# Setting up TURN/STUN
[TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) and [STUN](https://en.wikipedia.org/wiki/STUN) are used as a component in many calling systems. Matrix uses them directly for legacy calls and indirectly for MatrixRTC via Livekit.
Continuwuity recommends using [Coturn](https://github.com/coturn/coturn) as your TURN/STUN server, which is available as a Docker image or a distro package.
## Installing Coturn
### Configuration
Create a configuration file called `coturn.conf` containing:
```ini
use-auth-secret
static-auth-secret=<a secret key>
realm=<your server domain>
```
:::tip Generating a secure secret
A common way to generate a suitable alphanumeric secret key is by using:
```bash
pwgen -s 64 1
```
:::
#### Port Configuration
By default, coturn uses the following ports:
- `3478` (UDP/TCP): Standard TURN/STUN port
- `5349` (UDP/TCP): TURN/STUN over TLS
- `49152-65535` (UDP): Media relay ports
If you're also running LiveKit, you'll need to avoid port conflicts. Configure non-overlapping port ranges:
```ini
# In coturn.conf
min-port=50201
max-port=65535
```
This leaves ports `50100-50200` available for LiveKit's default configuration.
### Running with Docker
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using:
```bash
docker run -d --network=host \
-v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf \
coturn/coturn
```
### Running with Docker Compose
Create a `docker-compose.yml` file and run `docker compose up -d`:
```yaml
version: '3'
services:
turn:
container_name: coturn-server
image: docker.io/coturn/coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
```
:::info Why host networking?
Coturn uses host networking mode because it needs to bind to multiple ports and work with various network protocols. Using host networking is better for performance, and reduces configuration complexity. To understand alternative configuration options, visit [Coturn's Docker documentation](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md).
:::
### Security Recommendations
For security best practices, see Synapse's [Coturn documentation](https://element-hq.github.io/synapse/latest/turn-howto.html), which includes important firewall and access control recommendations.
## Configuring Continuwuity
Once your TURN server is running, configure Continuwuity to provide credentials to clients. Add the following to your Continuwuity configuration file:
### Shared Secret Authentication (Recommended)
This is the most secure method and generates time-limited credentials automatically:
```toml
# TURN URIs that clients should connect to
turn_uris = [
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp",
"turns:coturn.example.com?transport=udp",
"turns:coturn.example.com?transport=tcp"
]
# Shared secret for generating credentials (must match coturn's static-auth-secret)
turn_secret = "<your coturn static-auth-secret>"
# Optional: Read secret from a file instead (takes priority over turn_secret)
# turn_secret_file = "/etc/continuwuity/.turn_secret"
# TTL for generated credentials in seconds (default: 86400 = 24 hours)
turn_ttl = 86400
```
:::tip Using TLS
The `turns:` URI prefix instructs clients to connect to TURN over TLS, which is highly recommended for security. Make sure you've configured TLS in your coturn server first.
:::
### Static Credentials (Alternative)
If you prefer static username/password credentials instead of shared secrets:
```toml
turn_uris = [
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp"
]
turn_username = "your_username"
turn_password = "your_password"
```
:::warning
Static credentials are less secure than shared secrets because they don't expire and must be configured in coturn separately. It is strongly advised you use shared secret authentication.
:::
### Guest Access
By default, TURN credentials require client authentication. To allow unauthenticated access:
```toml
turn_allow_guests = true
```
:::caution
This is not recommended as it allows unauthenticated users to access your TURN server, potentially enabling abuse by bots. All major Matrix clients that support legacy calls *also* support authenticated TURN access.
:::
### Important Notes
- Replace `coturn.example.com` with your actual TURN server domain (the `realm` from coturn.conf)
- The `turn_secret` must match the `static-auth-secret` in your coturn configuration
- Restart or reload Continuwuity after making configuration changes
## Testing Your TURN Server
### Testing Credentials
Verify that Continuwuity is correctly serving TURN credentials to clients:
```bash
curl "https://matrix.example.com/_matrix/client/r0/voip/turnServer" \
-H "Authorization: Bearer <your_client_token>" | jq
```
You should receive a response like this:
```json
{
"username": "1752792167:@jade:example.com",
"password": "KjlDlawdPbU9mvP4bhdV/2c/h65=",
"uris": [
"turns:coturn.example.com?transport=udp",
"turns:coturn.example.com?transport=tcp",
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp"
],
"ttl": 86400
}
```
:::note MSC4166 Compliance
If no TURN URIs are configured (`turn_uris` is empty), Continuwuity will return a 404 Not Found response, as specified in MSC4166.
:::
### Testing Connectivity
Use [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/) to verify that the TURN credentials actually work:
1. Copy the credentials from the response above
2. Paste them into the Trickle ICE testing tool
3. Click "Gather candidates"
4. Look for successful `relay` candidates in the results
If you see relay candidates, your TURN server is working correctly!
## Troubleshooting
### Clients can't connect to TURN server
- Verify firewall rules allow the necessary ports (3478, 5349, and your media port range)
- Check that DNS resolves correctly for your TURN domain
- Ensure your `turn_secret` matches coturn's `static-auth-secret`
- Test with Trickle ICE to isolate the issue
### Port conflicts with LiveKit
- Make sure coturn's `min-port` starts above LiveKit's `port_range_end` (default: 50200)
- Or adjust LiveKit's port range to avoid coturn's default range
### 404 when calling turnServer endpoint
- Verify that `turn_uris` is not empty in your Continuwuity config
- This behavior is correct per MSC4166 if no TURN URIs are configured
### Credentials expire too quickly
- Adjust the `turn_ttl` value in your Continuwuity configuration
- Default is 86400 seconds (24 hours)
### Related Documentation
- [MatrixRTC/LiveKit Setup](./livekit.mdx) - Configure group calling with LiveKit
- [Coturn GitHub](https://github.com/coturn/coturn) - Official coturn repository
- [Synapse TURN Guide](https://element-hq.github.io/synapse/latest/turn-howto.html) - Additional security recommendations

View File

@@ -217,4 +217,4 @@ ### Use Traefik as Proxy
## Voice communication
See the [TURN](../turn.md) page.
See the [Calls](../calls.mdx) page.

View File

@@ -277,7 +277,7 @@ # What's next?
## Audio/Video calls
For Audio/Video call functionality see the [TURN Guide](../turn.md).
For Audio/Video call functionality see the [Calls](../calls.md) page.
## Appservices

View File

@@ -1,94 +0,0 @@
# Setting up TURN/STURN
In order to make or receive calls, a TURN server is required. Continuwuity suggests
using [Coturn](https://github.com/coturn/coturn) for this purpose, which is also
available as a Docker image.
### Configuration
Create a configuration file called `coturn.conf` containing:
```
use-auth-secret
static-auth-secret=<a secret key>
realm=<your server domain>
```
A common way to generate a suitable alphanumeric secret key is by using `pwgen
-s 64 1`.
These same values need to be set in Continuwuity. See the [example
config](./reference/config.mdx) in the TURN section for configuring these and
restart Continuwuity after.
`turn_secret` or a path to `turn_secret_file` must have a value of your
coturn `static-auth-secret`, or use `turn_username` and `turn_password`
if using legacy username:password TURN authentication (not preferred).
`turn_uris` must be the list of TURN URIs you would like to send to the client.
Typically you will just replace the example domain `example.turn.uri` with the
`realm` you set from the example config.
If you are using TURN over TLS, you can replace `turn:` with `turns:` in the
`turn_uris` config option to instruct clients to attempt to connect to
TURN over TLS. This is highly recommended.
If you need unauthenticated access to the TURN URIs, or some clients may be
having trouble, you can enable `turn_guest_access` in Continuwuity which disables
authentication for the TURN URI endpoint `/_matrix/client/v3/voip/turnServer`
### Run
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using
```bash
docker run -d --network=host -v
$(pwd)/coturn.conf:/etc/coturn/turnserver.conf coturn/coturn
```
or docker-compose. For the latter, paste the following section into a file
called `docker-compose.yml` and run `docker compose up -d` in the same
directory.
```yml
version: 3
services:
turn:
container_name: coturn-server
image: docker.io/coturn/coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
```
To understand why the host networking mode is used and explore alternative
configuration options, please visit [Coturn's Docker
documentation](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md).
For security recommendations see Synapse's [Coturn
documentation](https://element-hq.github.io/synapse/latest/turn-howto.html).
### Testing
To make sure turn credentials are being correctly served to clients, you can manually make a HTTP request to the turnServer endpoint.
`curl "https://<matrix.example.com>/_matrix/client/r0/voip/turnServer" -H 'Authorization: Bearer <your_client_token>' | jq`
You should get a response like this:
```json
{
"username": "1752792167:@jade:example.com",
"password": "KjlDlawdPbU9mvP4bhdV/2c/h65=",
"uris": [
"turns:coturn.example.com?transport=udp",
"turns:coturn.example.com?transport=tcp",
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp"
],
"ttl": 86400
}
```
You can test these credentials work using [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/)

View File

@@ -20,7 +20,7 @@ rec {
# we need to keep the `web` directory which would be filtered out by the regular source filtering function
#
# https://crane.dev/API.html#cranelibcleancargosource
isWebTemplate = path: _type: builtins.match ".*src/web.*" path != null;
isWebTemplate = path: _type: builtins.match ".*(src/(web|service)|docs).*" path != null;
isRust = craneLib.filterCargoSources;
isNix = path: _type: builtins.match ".+/nix.*" path != null;
webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t);

View File

@@ -56,6 +56,9 @@ export default defineConfig({
}, {
from: '/community$',
to: '/community/guidelines'
}, {
from: "^/turn",
to: "/calls/turn",
}
]
})],

View File

@@ -4,12 +4,14 @@
use crate::{PAGE_SIZE, admin_command, get_room_info};
#[allow(clippy::fn_params_excessive_bools)]
#[admin_command]
pub(super) async fn list_rooms(
&self,
page: Option<usize>,
exclude_disabled: bool,
exclude_banned: bool,
include_empty: bool,
no_details: bool,
) -> Result {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
@@ -28,6 +30,20 @@ pub(super) async fn list_rooms(
.then_some(room_id)
})
.then(|room_id| get_room_info(self.services, room_id))
.then(|(room_id, total_members, name)| async move {
let local_members: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.collect()
.await;
let local_members = local_members.len();
(room_id, total_members, local_members, name)
})
.filter_map(|(room_id, total_members, local_members, name)| async move {
(include_empty || local_members > 0).then_some((room_id, total_members, name))
})
.collect::<Vec<_>>()
.await;

View File

@@ -30,6 +30,10 @@ pub enum RoomCommand {
#[arg(long)]
exclude_banned: bool,
/// Includes disconnected/empty rooms (rooms with zero members)
#[arg(long)]
include_empty: bool,
#[arg(long)]
/// Whether to only output room IDs without supplementary room
/// information

View File

@@ -5,7 +5,7 @@
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
use conduwuit::{
Err, Result, debug, debug_warn, error, info, is_equal_to,
Err, Result, debug_warn, error, info,
matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt},
warn,
@@ -167,27 +167,8 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// we dont add a device since we're not the user, just the creator
// if this account creation is from the CLI / --execute, invite the first user
// to admin room
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
if self
.services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
} else {
debug!("create_user admin command called without an admin room being available");
}
// Make the first user to register an administrator and disable first-run mode.
self.services.firstrun.empower_first_user(&user_id).await?;
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
.await

View File

@@ -3,7 +3,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Event, Result, debug_info, err, error, info, is_equal_to,
Err, Error, Event, Result, debug_info, err, error, info,
matrix::pdu::PduBuilder,
utils::{self, ReadyExt, stream::BroadbandExt},
warn,
@@ -148,7 +148,12 @@ pub(crate) async fn register_route(
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
if !services.config.allow_registration && body.appservice_info.is_none() {
// Allow registration if it's enabled in the config file or if this is the first
// run (so the first user account can be created)
let allow_registration =
services.config.allow_registration || services.firstrun.is_first_run();
if !allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
@@ -185,17 +190,10 @@ pub(crate) async fn register_route(
)));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services
.registration_tokens
.get_config_file_token()
.is_some()))
{
if is_guest && !services.config.allow_guest_registration {
info!(
"Guest registration disabled / registration enabled with token configured, \
rejecting guest registration attempt, initial device name: \"{}\"",
"Guest registration disabled, rejecting guest registration attempt, initial device \
name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
@@ -309,54 +307,63 @@ pub(crate) async fn register_route(
let skip_auth = body.appservice_info.is_some() || is_guest;
// Populate required UIAA flows
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
// Registration token required
if services.firstrun.is_first_run() {
// Registration token forced while in first-run mode
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
});
}
if services.config.recaptcha_private_site_key.is_some() {
if let Some(pubkey) = &services.config.recaptcha_site_key {
// ReCaptcha required
uiaainfo
.flows
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
"m.login.recaptcha": {
"public_key": pubkey,
},
}))
.expect("Failed to serialize recaptcha params");
}
}
if uiaainfo.flows.is_empty() && !skip_auth {
// 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
} else {
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
// Registration token required
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
});
}
// We have open registration enabled (😧), provide a dummy stage
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
if services.config.recaptcha_private_site_key.is_some() {
if let Some(pubkey) = &services.config.recaptcha_site_key {
// ReCaptcha required
uiaainfo
.flows
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
"m.login.recaptcha": {
"public_key": pubkey,
},
}))
.expect("Failed to serialize recaptcha params");
}
}
if uiaainfo.flows.is_empty() && !skip_auth {
// 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(),
params: Box::default(),
session: None,
auth_error: None,
};
}
}
if !skip_auth {
@@ -514,39 +521,29 @@ pub(crate) async fn register_route(
}
}
// If this is the first real user, grant them admin privileges except for guest
// users
// Note: the server user is generated first
if !is_guest {
if let Ok(admin_room) = services.admin.get_admin_room().await {
if services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).boxed().await?;
warn!("Granting {user_id} admin privileges as the first user");
} else if services.config.suspend_on_register {
// This is not an admin, suspend them.
// Note that we can still do auto joins for suspended users
// Make the first user to register an administrator and disable first-run mode.
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
// If the registering user was not the first and we're suspending users on
// register, suspend them.
if !was_first_user && services.config.suspend_on_register {
// Note that we can still do auto joins for suspended users
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user \
on this server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user on \
this server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
}
}

View File

@@ -2,7 +2,7 @@
use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, PduEvent, Result, err,
Err, Error, PduEvent, Result, err, error,
matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256},
warn,
@@ -199,20 +199,27 @@ pub(crate) async fn create_invite_route(
for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&recipient_user) {
let request = ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_format()],
txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes()))
.into(),
ephemeral: Vec::new(),
to_device: Vec::new(),
};
services
.sending
.send_appservice_request(
appservice.registration.clone(),
ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_format()],
txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes()))
.into(),
ephemeral: Vec::new(),
to_device: Vec::new(),
},
)
.await?;
.send_appservice_request(appservice.registration.clone(), request)
.await
.map_err(|e| {
error!(
"failed to notify appservice {} about incoming invite: {e}",
appservice.registration.id
);
err!(BadServerResponse(
"Failed to notify appservice about incoming invite."
))
})?;
}
}
}

View File

@@ -559,7 +559,7 @@ pub struct Config {
///
/// If you would like registration only via token reg, please configure
/// `registration_token`.
#[serde(default)]
#[serde(default = "true_fn")]
pub allow_registration: bool,
/// If registration is enabled, and this setting is true, new users

View File

@@ -39,7 +39,15 @@ pub(crate) async fn run(services: Arc<Services>) -> Result<()> {
.runtime()
.spawn(serve::serve(services.clone(), handle.clone(), tx.subscribe()));
// Focal point
// Run startup admin commands.
// This has to be done after the admin service is initialized otherwise it
// panics.
services.admin.startup_execute().await?;
// Print first-run banner if necessary. This needs to be done after the startup
// admin commands are run in case one of them created the first user.
services.firstrun.print_first_run_banner();
debug!("Running");
let res = tokio::select! {
res = &mut listener => res.map_err(Error::from).unwrap_or_else(Err),

View File

@@ -79,6 +79,7 @@ zstd_compression = [
]
[dependencies]
askama.workspace = true
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
@@ -118,6 +119,7 @@ webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false }
yansi.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true

View File

@@ -26,7 +26,7 @@ pub(super) async fn console_auto_stop(&self) {
/// Execute admin commands after startup
#[implement(super::Service)]
pub(super) async fn startup_execute(&self) -> Result {
pub async fn startup_execute(&self) -> Result {
// List of commands to execute
let commands = &self.services.server.config.admin_execute;

View File

@@ -9,7 +9,6 @@
RoomAccountDataEventType, StateEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
power_levels::RoomPowerLevelsEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
@@ -126,23 +125,6 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
}
}
if self.services.server.config.admin_room_notices {
let welcome_message = String::from(
"## Thank you for trying out Continuwuity!\n\nContinuwuity is a hard fork of conduwuit, which is also a hard fork of Conduit, currently in Beta. The Beta status initially was inherited from Conduit, however overtime this Beta status is rapidly becoming less and less relevant as our codebase significantly diverges more and more. Continuwuity is quite stable and very usable as a daily driver and for a low-medium sized homeserver. There is still a lot of more work to be done, but it is in a far better place than the project was in early 2024.\n\nHelpful links:\n> Source code: https://forgejo.ellis.link/continuwuation/continuwuity\n> Documentation: https://continuwuity.org/\n> Report issues: https://forgejo.ellis.link/continuwuation/continuwuity/issues\n\nFor a list of available commands, send the following message in this room: `!admin --help`\n\nHere are some rooms you can join (by typing the command into your client) -\n\nContinuwuity space: `/join #space:continuwuity.org`\nContinuwuity main room (Ask questions and get notified on updates): `/join #continuwuity:continuwuity.org`\nContinuwuity offtopic room: `/join #offtopic:continuwuity.org`",
);
// Send welcome message
self.services
.timeline
.build_and_append_pdu(
PduBuilder::timeline(&RoomMessageEventContent::text_markdown(welcome_message)),
server_user,
Some(&room_id),
&state_lock,
)
.await?;
}
Ok(())
}

View File

@@ -137,7 +137,6 @@ async fn worker(self: Arc<Self>) -> Result<()> {
let mut signals = self.services.server.signal.subscribe();
let receiver = self.channel.1.clone();
self.startup_execute().await?;
self.console_auto_start().await;
loop {

View File

@@ -18,7 +18,7 @@
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{Result, Server, debug, error, info, warn};
use conduwuit::{Result, Server, debug, error, warn};
use database::{Deserialized, Map};
use rand::Rng;
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
@@ -155,11 +155,6 @@ async fn check(&self) -> Result<()> {
#[tracing::instrument(skip_all)]
async fn handle(&self, announcement: &CheckForAnnouncementsResponseEntry) {
if let Some(date) = &announcement.date {
info!("[announcements] {date} {:#}", announcement.message);
} else {
info!("[announcements] {:#}", announcement.message);
}
let mut message = RoomMessageEventContent::text_markdown(format!(
"### New announcement{}\n\n{}",
announcement

View File

@@ -7,12 +7,25 @@
error, implement,
};
use crate::registration_tokens::{ValidToken, ValidTokenSource};
pub struct Service {
server: Arc<Server>,
}
const SIGNAL: &str = "SIGUSR1";
impl Service {
/// Get the registration token set in the config file, if it exists.
#[must_use]
pub fn get_config_file_token(&self) -> Option<ValidToken> {
self.registration_token.clone().map(|token| ValidToken {
token,
source: ValidTokenSource::ConfigFile,
})
}
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {

302
src/service/firstrun/mod.rs Normal file
View File

@@ -0,0 +1,302 @@
use std::{
io::IsTerminal,
sync::{Arc, OnceLock},
};
use askama::Template;
use async_trait::async_trait;
use conduwuit::{Result, info, utils::ReadyExt};
use futures::StreamExt;
use ruma::{UserId, events::room::message::RoomMessageEventContent};
use crate::{
Dep, admin, config, globals,
registration_tokens::{self, ValidToken, ValidTokenSource},
users,
};
pub struct Service {
services: Services,
/// Represents the state of first run mode.
///
/// First run mode is either active or inactive at server start. It may
/// transition from active to inactive, but only once, and can never
/// transition the other way. Additionally, whether the server is in first
/// run mode or not can only be determined when all services are
/// constructed. The outer `OnceLock` represents the unknown state of first
/// run mode, and the inner `OnceLock` enforces the one-time transition from
/// active to inactive.
///
/// Consequently, this marker may be in one of three states:
/// 1. OnceLock<uninitialized>, representing the unknown state of first run
/// mode during server startup. Once server startup is complete, the
/// marker transitions to state 2 or directly to state 3.
/// 2. OnceLock<OnceLock<uninitialized>>, representing first run mode being
/// active. The marker may only transition to state 3 from here.
/// 3. OnceLock<OnceLock<()>>, representing first run mode being inactive.
/// The marker may not transition out of this state.
first_run_marker: OnceLock<OnceLock<()>>,
/// A single-use registration token which may be used to create the first
/// account.
first_account_token: String,
}
struct Services {
config: Dep<config::Service>,
users: Dep<users::Service>,
globals: Dep<globals::Service>,
admin: Dep<admin::Service>,
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
config: args.depend::<config::Service>("config"),
users: args.depend::<users::Service>("users"),
globals: args.depend::<globals::Service>("globals"),
admin: args.depend::<admin::Service>("admin"),
},
// marker starts in an indeterminate state
first_run_marker: OnceLock::new(),
first_account_token: registration_tokens::Service::generate_token_string(),
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
async fn worker(self: Arc<Self>) -> Result {
// first run mode will be enabled if there are no local users
let is_first_run = self
.services
.users
.list_local_users()
.ready_filter(|user| *user != self.services.globals.server_user)
.next()
.await
.is_none();
self.first_run_marker
.set(if is_first_run {
// first run mode is active (empty inner lock)
OnceLock::new()
} else {
// first run mode is inactive (already filled inner lock)
OnceLock::from(())
})
.expect("Service worker should only be called once");
Ok(())
}
}
impl Service {
/// Check if first run mode is active.
pub fn is_first_run(&self) -> bool {
self.first_run_marker
.get()
.expect("First run mode should not be checked during server startup")
.get()
.is_none()
}
/// Disable first run mode and begin normal operation.
///
/// Returns true if first run mode was successfully disabled, and false if
/// first run mode was already disabled.
fn disable_first_run(&self) -> bool {
self.first_run_marker
.get()
.expect("First run mode should not be disabled during server startup")
.set(())
.is_ok()
}
/// If first-run mode is active, grant admin powers to the specified user
/// and disable first-run mode.
///
/// Returns Ok(true) if the specified user was the first user, and Ok(false)
/// if they were not.
pub async fn empower_first_user(&self, user: &UserId) -> Result<bool> {
#[derive(Template)]
#[template(path = "welcome.md.j2")]
struct WelcomeMessage<'a> {
config: &'a Dep<config::Service>,
domain: &'a str,
}
// If first run mode isn't active, do nothing.
if !self.disable_first_run() {
return Ok(false);
}
self.services.admin.make_user_admin(user).await?;
// Send the welcome message
let welcome_message = WelcomeMessage {
config: &self.services.config,
domain: self.services.globals.server_name().as_str(),
}
.render()
.expect("should have been able to render welcome message template");
self.services
.admin
.send_loud_message(RoomMessageEventContent::text_markdown(welcome_message))
.await?;
info!("{user} has been invited to the admin room as the first user.");
Ok(true)
}
/// Get the single-use registration token which may be used to create the
/// first account.
pub fn get_first_account_token(&self) -> Option<ValidToken> {
if self.is_first_run() {
Some(ValidToken {
token: self.first_account_token.clone(),
source: ValidTokenSource::FirstAccount,
})
} else {
None
}
}
pub fn print_first_run_banner(&self) {
use yansi::Paint;
// This function is specially called by the core after all other
// services have started. It runs last to ensure that the banner it
// prints comes after any other logging which may occur on startup.
if !self.is_first_run() {
return;
}
eprintln!();
eprintln!("{}", "============".bold());
eprintln!(
"Welcome to {} {}!",
"Continuwuity".bold().bright_magenta(),
conduwuit::version::version().bold()
);
eprintln!();
eprintln!(
"In order to use your new homeserver, you need to create its first user account."
);
eprintln!(
"Open your Matrix client of choice and register an account on {} using the \
registration token {} . Pick your own username and password!",
self.services.globals.server_name().bold().green(),
self.first_account_token.as_str().bold().green()
);
match (
self.services.config.allow_registration,
self.services.config.get_config_file_token().is_some(),
) {
| (true, true) => {
eprintln!(
"{} until you create an account using the token above.",
"The registration token you set in your configuration will not function"
.red()
);
},
| (true, false) => {
eprintln!(
"{} until you create an account using the token above.",
"Nobody else will be able to register".green()
);
},
| (false, true) => {
eprintln!(
"{} because you have disabled registration in your configuration. If this \
is not desired, set `allow_registration` to true and restart Continuwuity.",
"The registration token you set in your configuration will not be usable"
.yellow()
);
},
| (false, false) => {
eprintln!(
"{} to allow you to create an account. Because registration is not enabled \
in your configuration, it will be disabled again once your account is \
created.",
"Registration has been temporarily enabled".yellow()
);
},
}
eprintln!(
"{} https://matrix.org/ecosystem/clients/",
"Find a list of Matrix clients here:".bold()
);
if self.services.config.suspend_on_register {
eprintln!(
"{} Because you enabled suspend-on-register in your configuration, accounts \
created after yours will be automatically suspended.",
"Your account will not be suspended when you register.".green()
);
}
if self
.services
.config
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
{
eprintln!();
eprintln!(
"{}",
"You have enabled open registration in your configuration! You almost certainly \
do not want to do this."
.bold()
.on_red()
);
eprintln!(
"{}",
"Servers with open, unrestricted registration are prone to abuse by spammers. \
Users on your server may be unable to join chatrooms which block open \
registration servers."
.red()
);
eprintln!(
"If you enabled it only for the purpose of creating the first account, {} and \
create the first account using the token above.",
"disable it now, restart Continuwuity,".red(),
);
// TODO link to a guide on setting up reCAPTCHA
}
if self.services.config.emergency_password.is_some() {
eprintln!();
eprintln!(
"{}",
"You have set an emergency password for the server user! You almost certainly \
do not want to do this."
.red()
);
eprintln!(
"If you set the password only for the purpose of creating the first account, {} \
and create the first account using the token above.",
"disable it now, restart Continuwuity,".red(),
);
}
eprintln!();
if std::io::stdin().is_terminal() && self.services.config.admin_console_automatic {
eprintln!(
"You may also create the first user through the admin console below using the \
`users create-user` command."
);
} else {
eprintln!(
"If you're running the server interactively, you may also create the first user \
through the admin console using the `users create-user` command. Press Ctrl-C \
to open the console."
);
}
eprintln!("If you need assistance setting up your homeserver, make a Matrix account on another homeserver and join our chatroom: https://matrix.to/#/#continuwuity:continuwuity.org");
eprintln!("{}", "============".bold());
}
}

View File

@@ -18,6 +18,7 @@
pub mod config;
pub mod emergency;
pub mod federation;
pub mod firstrun;
pub mod globals;
pub mod key_backups;
pub mod media;

View File

@@ -1,14 +1,17 @@
mod data;
use std::sync::Arc;
use std::{future::ready, pin::Pin, sync::Arc};
use conduwuit::{Err, Result, utils};
use data::Data;
pub use data::{DatabaseTokenInfo, TokenExpires};
use futures::{Stream, StreamExt, stream};
use futures::{
Stream, StreamExt,
stream::{iter, once},
};
use ruma::OwnedUserId;
use crate::{Dep, config};
use crate::{Dep, config, firstrun};
const RANDOM_TOKEN_LENGTH: usize = 16;
@@ -19,6 +22,7 @@ pub struct Service {
struct Services {
config: Dep<config::Service>,
firstrun: Dep<firstrun::Service>,
}
/// A validated registration token which may be used to create an account.
@@ -46,6 +50,9 @@ pub enum ValidTokenSource {
ConfigFile,
/// A database token which has been checked to be valid.
Database(DatabaseTokenInfo),
/// The single-use token which may be used to create the homeserver's first
/// account.
FirstAccount,
}
impl std::fmt::Display for ValidTokenSource {
@@ -53,6 +60,7 @@ 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),
| Self::FirstAccount => write!(f, "Initial setup token."),
}
}
}
@@ -63,6 +71,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
db: Data::new(args.db),
services: Services {
config: args.depend::<config::Service>("config"),
firstrun: args.depend::<firstrun::Service>("firstrun"),
},
}))
}
@@ -71,45 +80,51 @@ fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Generate a random string suitable to be used as a registration token.
#[must_use]
pub fn generate_token_string() -> String { utils::random_string(RANDOM_TOKEN_LENGTH) }
/// 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 token = Self::generate_token_string();
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,
})
/// Get all the "special" registration tokens that aren't defined in the
/// database.
fn iterate_static_tokens(&self) -> impl Iterator<Item = ValidToken> {
// This does not include the first-account token, because it's special:
// no other registration tokens are valid when it is set.
self.services.config.get_config_file_token().into_iter()
}
/// 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,
});
// Check for the first-account token first
if let Some(first_account_token) = self.services.firstrun.get_first_account_token() {
if first_account_token == *token {
return Some(first_account_token);
}
// If the first-account token is set, no other tokens are valid
return None;
}
// Now check the database
// Then static registration tokens
for static_token in self.iterate_static_tokens() {
if static_token == *token {
return Some(static_token);
}
}
// Then check the database
if let Some(token_info) = self.db.lookup_token_info(&token).await
&& token_info.is_valid()
{
@@ -126,14 +141,14 @@ pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
/// 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);
},
| _ => {
// Do nothing for other token sources.
},
}
}
@@ -144,7 +159,6 @@ pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
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."
@@ -154,11 +168,19 @@ pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
self.db.revoke_token(&token);
Ok(())
},
| ValidTokenSource::FirstAccount => {
Err!("The initial setup token cannot be revoked.")
},
}
}
/// Iterate over all valid registration tokens.
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
pub fn iterate_tokens(&self) -> Pin<Box<dyn Stream<Item = ValidToken> + Send + '_>> {
// If the first-account token is set, no other tokens are valid
if let Some(first_account_token) = self.services.firstrun.get_first_account_token() {
return once(ready(first_account_token)).boxed();
}
let db_tokens = self
.db
.iterate_and_clean_tokens()
@@ -167,6 +189,6 @@ pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
source: ValidTokenSource::Database(info),
});
stream::iter(self.get_config_file_token()).chain(db_tokens)
iter(self.iterate_static_tokens()).chain(db_tokens).boxed()
}
}

View File

@@ -58,7 +58,11 @@ pub async fn ask_policy_server(
.state_accessor
.room_state_get_content(room_id, &StateEventType::RoomPolicy, "")
.await
.inspect_err(|e| debug_error!("failed to load room policy server state event: {e}"))
.inspect_err(|e| {
if !e.is_not_found() {
debug_error!("failed to load room policy server state event: {e}");
}
})
.map(|c: RoomPolicyEventContent| c)
else {
debug!("room has no policy server configured");

View File

@@ -9,7 +9,7 @@
use crate::{
account_data, admin, announcements, antispam, appservice, client, config, emergency,
federation, globals, key_backups,
federation, firstrun, globals, key_backups,
manager::Manager,
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
server_keys,
@@ -33,6 +33,7 @@ pub struct Services {
pub resolver: Arc<resolver::Service>,
pub rooms: rooms::Service,
pub federation: Arc<federation::Service>,
pub firstrun: Arc<firstrun::Service>,
pub sending: Arc<sending::Service>,
pub server_keys: Arc<server_keys::Service>,
pub sync: Arc<sync::Service>,
@@ -67,6 +68,9 @@ macro_rules! build {
}
Ok(Arc::new(Self {
// firstrun service should be built first so other services
// can check first-run state
firstrun: build!(firstrun::Service),
account_data: build!(account_data::Service),
admin: build!(admin::Service),
appservice: build!(appservice::Service),
@@ -144,6 +148,7 @@ pub async fn start(self: &Arc<Self>) -> Result<Arc<Self>> {
}
debug_info!("Services startup complete.");
Ok(Arc::clone(self))
}

View File

@@ -0,0 +1,29 @@
## Thank you for trying out Continuwuity!
Your new homeserver is ready to use! {%- if config.allow_federation %} To make sure you can federate with the rest of the Matrix network, consider checking your domain (`{{ domain }}`) with a federation tester like [this one](https://connectivity-tester.mtrnord.blog/). {%- endif %}
{% if config.get_config_file_token().is_some() -%}
Users may now create accounts normally using the configured registration token.
{%- else if config.recaptcha_site_key.is_some() -%}
Users may now create accounts normally after solving a CAPTCHA.
{%- else if config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse -%}
**This server has open, unrestricted registration enabled!** Anyone, including spammers, may now create an account with no further steps. If this is not desired behavior, set `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` to `false` in your configuration and restart the server.
{%- else if config.allow_registration -%}
To allow more users to register, use the `!admin token` admin commands to issue registration tokens, or set a registration token in the configuration.
{%- else -%}
You've disabled registration. To create more accounts, use the `!admin users create-user` admin command.
{%- endif %}
This room is your server's admin room. You can send messages starting with `!admin` in this room to perform a range of administrative actions.
To view a list of available commands, send the following message: `!admin --help`
Project chatrooms:
> Support chatroom: https://matrix.to/#/#continuwuity:continuwuity.org
> Update announcements: https://matrix.to/#/#announcements:continuwuity.org
> Other chatrooms: https://matrix.to/#/#space:continuwuity.org
>
Helpful links:
> Source code: https://forgejo.ellis.link/continuwuation/continuwuity
> Documentation: https://continuwuity.org/
> Report issues: https://forgejo.ellis.link/continuwuation/continuwuity/issues

View File

@@ -187,7 +187,9 @@ pub async fn create(
self.db
.userid_origin
.insert(user_id, origin.unwrap_or("password"));
self.set_password(user_id, password).await
self.set_password(user_id, password).await?;
Ok(())
}
/// Deactivate account

View File

@@ -20,9 +20,7 @@ crate-type = [
[dependencies]
conduwuit-build-metadata.workspace = true
conduwuit-service.workspace = true
askama = "0.14.0"
askama.workspace = true
axum.workspace = true
futures.workspace = true
tracing.workspace = true

View File

@@ -83,3 +83,12 @@ footer {
color: transparent;
filter: brightness(1.2);
}
b {
color: oklch(from var(--c2) var(--name-lightness) c h);
}
.logo {
width: 100%;
height: 64px;
}

View File

@@ -10,8 +10,9 @@
use conduwuit_service::state;
pub fn build() -> Router<state::State> {
let router = Router::<state::State>::new();
router.route("/", get(index_handler))
Router::<state::State>::new()
.route("/", get(index_handler))
.route("/_continuwuity/logo.svg", get(logo_handler))
}
async fn index_handler(
@@ -19,22 +20,34 @@ async fn index_handler(
) -> Result<impl IntoResponse, WebError> {
#[derive(Debug, Template)]
#[template(path = "index.html.j2")]
struct Tmpl<'a> {
struct Index<'a> {
nonce: &'a str,
server_name: &'a str,
first_run: bool,
}
let nonce = rand::random::<u64>().to_string();
let template = Tmpl {
let template = Index {
nonce: &nonce,
server_name: services.config.server_name.as_str(),
first_run: services.firstrun.is_first_run(),
};
Ok((
[(header::CONTENT_SECURITY_POLICY, format!("default-src 'none' 'nonce-{nonce}';"))],
[(
header::CONTENT_SECURITY_POLICY,
format!("default-src 'nonce-{nonce}'; img-src 'self';"),
)],
Html(template.render()?),
))
}
async fn logo_handler() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "image/svg+xml")],
include_str!("templates/logo.svg").to_owned(),
)
}
#[derive(Debug, thiserror::Error)]
enum WebError {
#[error("Failed to render template: {0}")]
@@ -45,7 +58,7 @@ impl IntoResponse for WebError {
fn into_response(self) -> Response {
#[derive(Debug, Template)]
#[template(path = "error.html.j2")]
struct Tmpl<'a> {
struct Error<'a> {
nonce: &'a str,
err: WebError,
}
@@ -55,7 +68,7 @@ struct Tmpl<'a> {
let status = match &self {
| Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let tmpl = Tmpl { nonce: &nonce, err: self };
let tmpl = Error { nonce: &nonce, err: self };
if let Ok(body) = tmpl.render() {
(
status,

View File

@@ -6,6 +6,7 @@
<title>{% block title %}Continuwuity{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/_continuwuity/logo.svg">
<style type="text/css" nonce="{{ nonce }}">
/*<![CDATA[*/
{{ include_str !("css/index.css") | safe }}
@@ -17,7 +18,8 @@
<main>{%~ block content %}{% endblock ~%}</main>
{%~ block footer ~%}
<footer>
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a>
<img class="logo" src="/_continuwuity/logo.svg">
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }}
{%~ if let Some(version_info) = self::version_tag() ~%}
{%~ if let Some(url) = GIT_REMOTE_COMMIT_URL.or(GIT_REMOTE_WEB_URL) ~%}
(<a href="{{ url }}">{{ version_info }}</a>)

View File

@@ -1,16 +1,16 @@
{% extends "_layout.html.j2" %}
{%- block content -%}
<div class="orb"></div>
<div class="panel">
<h1>Welcome to <a class="project-name" href="https://continuwuity.org">Continuwuity</a>!</h1>
<p>Continuwuity is successfully installed and working. </p>
<p>To get started, you can:</p>
<ul>
<li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
<li>Join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a> or <a href="https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">space</a></li>
<li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
<li>Ensure <a href="https://federationtester.mtrnord.blog/?serverName={{ server_name }}">federation</a> works</li>
</ul>
<h1>
Welcome to <a class="project-name" href="https://continuwuity.org">Continuwuity</a>!
</h1>
<p>Continuwuity is successfully installed and working.</p>
{%- if first_run %}
<p>To get started, <b>check the server logs</b> for instructions on how to create the first account.</p>
<p>For support, take a look at the <a href="https://continuwuity.org/introduction">documentation</a> or join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a>.</p>
{%- else %}
<p>To get started, <a href="https://matrix.org/ecosystem/clients">choose a client</a> and connect to <code>{{ server_name }}</code>.</p>
{%- endif %}
</div>
{%- endblock content -%}

1
src/web/templates/logo.svg Symbolic link
View File

@@ -0,0 +1 @@
../../../docs/public/assets/logo.svg