Compare commits

..

1 Commits

Author SHA1 Message Date
Jade Ellis b8e476626f docs: Add links to matrix guides 2026-02-11 18:25:11 +00:00
137 changed files with 2234 additions and 3810 deletions
+1 -1
View File
@@ -1,9 +1,9 @@
# Local build and dev artifacts # Local build and dev artifacts
target/ target/
!target/debug/conduwuit
# Docker files # Docker files
Dockerfile* Dockerfile*
docker/
# IDE files # IDE files
.vscode .vscode
+1 -23
View File
@@ -30,28 +30,6 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
echo "Debian distribution: $DISTRIBUTION ($VERSION)" 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 - name: Checkout repository with full history
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -127,7 +105,7 @@ jobs:
run: | run: |
apt-get update -y apt-get update -y
# Build dependencies for rocksdb # Build dependencies for rocksdb
apt-get install -y liburing-dev ${{ steps.clang-version.outputs.version }} apt-get install -y clang liburing-dev
- name: Run cargo-deb - name: Run cargo-deb
id: cargo-deb id: cargo-deb
+1 -1
View File
@@ -23,7 +23,7 @@ repos:
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: v1.43.5 rev: v1.43.4
hooks: hooks:
- id: typos - id: typos
- id: typos - id: typos
-2
View File
@@ -24,5 +24,3 @@ extend-ignore-re = [
"continuwity" = "continuwuity" "continuwity" = "continuwuity"
"execuse" = "execuse" "execuse" = "execuse"
"oltp" = "OTLP" "oltp" = "OTLP"
rememvering = "remembering"
+17 -69
View File
@@ -1,65 +1,25 @@
# 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) # Continuwuity v0.5.4 (2026-02-08)
## Features ## Features
- The announcement checker will now announce errors it encounters in the first run to the admin room, plus a few other - 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)) 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 - Drastically improved the performance and reliability of account deactivations. Contributed by @nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
@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 - 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 and improves performance). Contributed by @nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
@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 - 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)) ([#1301](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1301))
## Bugfixes ## Bugfixes
- Fix the generated configuration containing uncommented optional sections. Contributed by - Fix the generated configuration containing uncommented optional sections. Contributed by @Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
@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))
- 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 - 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)) 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`. - Use exists instead of contains to save writing to a buffer in `src/service/users/mod.rs`: `is_login_disabled`.
Contributed Contributed
by @aprilgrimoire. ([#1340](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1340)) by @aprilgrimoire. ([#1340](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1340))
- Fixed backtraces being swallowed during panics. Contributed by - Fixed backtraces being swallowed during panics. Contributed by @jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
@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 - 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). 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. - Fixed a race condition that could result in outlier PDUs being incorrectly marked as visible to a remote server.
@@ -68,30 +28,25 @@
## Docs ## Docs
- Fixed Fedora install instructions. Contributed by - Fixed Fedora install instructions. Contributed by @julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
@julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
# Continuwuity 0.5.3 (2026-01-12) # Continuwuity 0.5.3 (2026-01-12)
## Features ## Features
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by - 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))
@Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
## Bugfixes ## Bugfixes
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by - 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))
@nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
## Docs ## Docs
- Improve admin command documentation generation. Contributed by - Improve admin command documentation generation. Contributed by @ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
@ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
## Misc ## Misc
- Improve timeout-related code for federation and URL previews. Contributed by - Improve timeout-related code for federation and URL previews. Contributed by @Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
@Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
# Continuwuity 0.5.2 (2026-01-09) # Continuwuity 0.5.2 (2026-01-09)
@@ -102,14 +57,11 @@
after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is 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 superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration
tokens. Contributed by @ginger (#783). tokens. Contributed by @ginger (#783).
- Implemented a configuration defined admin list independent of the admin room. Contributed by - Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
@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. - 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)) Contributed by @nex. ([#1263](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1263))
- Implemented account locking functionality, to complement user suspension. Contributed by - Implemented account locking functionality, to complement user suspension. Contributed by @nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
@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))
- 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. ( - 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)) [#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. - Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs.
@@ -119,8 +71,7 @@
## Bugfixes ## Bugfixes
- Fixed unreliable room summary fetching and improved error messages. Contributed by - Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
@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 - 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)) 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 - Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by
@@ -139,12 +90,9 @@
## Features ## Features
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. ( - Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
## Bug Fixes ## Bug Fixes
- Don't allow admin room upgrades, as this can break the admin room ( - Don't allow admin room upgrades, as this can break the admin room (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
@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))
- Fix invalid creators in power levels during upgrade to v12 (
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
+8 -16
View File
@@ -85,31 +85,24 @@ If your changes are done to fix Matrix tests, please note that in your pull requ
### Writing documentation ### Writing documentation
Continuwuity's website uses [`rspress`][rspress] and is deployed via CI using Cloudflare Pages Continuwuity's website uses [`mdbook`][mdbook] and is deployed via CI using Cloudflare Pages
in the [`documentation.yml`][documentation.yml] workflow file. All documentation is in the `docs/` in the [`documentation.yml`][documentation.yml] workflow file. All documentation is in the `docs/`
directory at the top level. directory at the top level.
To load the documentation locally: To build the documentation locally:
1. Install NodeJS and npm from their [official website][nodejs-download] or via your package manager of choice
2. From the project's root directory, install the relevant npm modules
1. Install mdbook if you don't have it already:
```bash ```bash
npm ci cargo install mdbook # or cargo binstall, or another method
``` ```
3. Make changes to the document pages as you see fit 2. Build the documentation:
4. Generate a live preview of the documentation
```bash ```bash
npm run docs:dev mdbook build
``` ```
A webserver for the docs will be spun up for you (e.g. at `http://localhost:3000`). Any changes you make to the documentation will be live-reloaded on the webpage. The output of the mdbook generation is in `public/`. You can open the HTML files directly in your browser without needing a web server.
Alternatively, you can build the documentation using `npm run docs:build` - the output of this will be in the `/doc_build` directory. Once you're happy with your documentation updates, you can commit the changes.
### Commit Messages ### Commit Messages
@@ -176,6 +169,5 @@ continuwuity Matrix rooms for Code of Conduct violations.
[continuwuity-matrix]: https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org [continuwuity-matrix]: https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org
[complement]: https://github.com/matrix-org/complement/ [complement]: https://github.com/matrix-org/complement/
[sytest]: https://github.com/matrix-org/sytest/ [sytest]: https://github.com/matrix-org/sytest/
[nodejs-download]: https://nodejs.org/en/download [mdbook]: https://rust-lang.github.io/mdBook/
[rspress]: https://rspress.rs/
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml [documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
Generated
+836 -576
View File
File diff suppressed because it is too large Load Diff
+15 -24
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml` # See also `rust-toolchain.toml`
readme = "README.md" readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity" repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.5" version = "0.5.4"
[workspace.metadata.crane] [workspace.metadata.crane]
name = "conduwuit" name = "conduwuit"
@@ -68,7 +68,7 @@ default-features = false
version = "0.1.3" version = "0.1.3"
[workspace.dependencies.rand] [workspace.dependencies.rand]
version = "0.10.0" version = "0.8.5"
# Used for the http request / response body type for Ruma endpoints used with reqwest # Used for the http request / response body type for Ruma endpoints used with reqwest
[workspace.dependencies.bytes] [workspace.dependencies.bytes]
@@ -84,7 +84,7 @@ version = "1.3.1"
version = "1.11.1" version = "1.11.1"
[workspace.dependencies.axum] [workspace.dependencies.axum]
version = "0.8.8" version = "0.7.9"
default-features = false default-features = false
features = [ features = [
"form", "form",
@@ -97,7 +97,7 @@ features = [
] ]
[workspace.dependencies.axum-extra] [workspace.dependencies.axum-extra]
version = "0.12.0" version = "0.9.6"
default-features = false default-features = false
features = ["typed-header", "tracing"] features = ["typed-header", "tracing"]
@@ -110,7 +110,7 @@ default-features = false
version = "0.7" version = "0.7"
[workspace.dependencies.axum-client-ip] [workspace.dependencies.axum-client-ip]
version = "0.7" version = "0.6.1"
[workspace.dependencies.tower] [workspace.dependencies.tower]
version = "0.5.2" version = "0.5.2"
@@ -118,7 +118,7 @@ default-features = false
features = ["util"] features = ["util"]
[workspace.dependencies.tower-http] [workspace.dependencies.tower-http]
version = "0.6.8" version = "0.6.2"
default-features = false default-features = false
features = [ features = [
"add-extension", "add-extension",
@@ -158,7 +158,7 @@ features = ["raw_value"]
# Used for appservice registration files # Used for appservice registration files
[workspace.dependencies.serde-saphyr] [workspace.dependencies.serde-saphyr]
version = "0.0.19" version = "0.0.17"
# Used to load forbidden room/user regex from config # Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex] [workspace.dependencies.serde_regex]
@@ -253,7 +253,7 @@ features = [
version = "0.4.0" version = "0.4.0"
[workspace.dependencies.libloading] [workspace.dependencies.libloading]
version = "0.9.0" version = "0.8.6"
# Validating urls in config, was already a transitive dependency # Validating urls in config, was already a transitive dependency
[workspace.dependencies.url] [workspace.dependencies.url]
@@ -298,7 +298,7 @@ default-features = false
features = ["env", "toml"] features = ["env", "toml"]
[workspace.dependencies.hickory-resolver] [workspace.dependencies.hickory-resolver]
version = "0.25.2" version = "0.25.1"
default-features = false default-features = false
features = [ features = [
"serde", "serde",
@@ -342,8 +342,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers # Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma" git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes" rev = "458d52bdc7f9a07c497be94a1420ebd3d87d7b2b"
rev = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
features = [ features = [
"compat", "compat",
"rand", "rand",
@@ -379,9 +378,7 @@ features = [
"unstable-msc4210", # remove legacy mentions "unstable-msc4210", # remove legacy mentions
"unstable-extensible-events", "unstable-extensible-events",
"unstable-pdu", "unstable-pdu",
"unstable-msc4155", "unstable-msc4155"
"unstable-msc4143", # livekit well_known response
"unstable-msc4284"
] ]
[workspace.dependencies.rust-rocksdb] [workspace.dependencies.rust-rocksdb]
@@ -426,7 +423,7 @@ features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
# optional sentry metrics for crash/panic reporting # optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry] [workspace.dependencies.sentry]
version = "0.46.0" version = "0.45.0"
default-features = false default-features = false
features = [ features = [
"backtrace", "backtrace",
@@ -442,9 +439,9 @@ features = [
] ]
[workspace.dependencies.sentry-tracing] [workspace.dependencies.sentry-tracing]
version = "0.46.0" version = "0.45.0"
[workspace.dependencies.sentry-tower] [workspace.dependencies.sentry-tower]
version = "0.46.0" version = "0.45.0"
# jemalloc usage # jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys] [workspace.dependencies.tikv-jemalloc-sys]
@@ -473,7 +470,7 @@ features = ["use_std"]
version = "0.5" version = "0.5"
[workspace.dependencies.nix] [workspace.dependencies.nix]
version = "0.31.0" version = "0.30.1"
default-features = false default-features = false
features = ["resource"] features = ["resource"]
@@ -551,12 +548,6 @@ features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.resolv-conf] [workspace.dependencies.resolv-conf]
version = "0.7.5" version = "0.7.5"
[workspace.dependencies.yansi]
version = "1.0.1"
[workspace.dependencies.askama]
version = "0.15.0"
# #
# Patches # Patches
# #
+3 -8
View File
@@ -57,15 +57,10 @@ Continuwuity aims to:
### Can I try it out? ### Can I try it out?
Check out the [documentation](https://continuwuity.org) for installation instructions. Check out the [documentation](https://continuwuity.org) for installation instructions, or join one of these vetted public homeservers running Continuwuity to get a feel for things!
If you want to try it out as a user, we have some partnered homeservers you can use: - https://continuwuity.rocks -- A public demo server operated by the Continuwuity Team.
* You can head over to [https://federated.nexus](https://federated.nexus/) in your browser. - https://federated.nexus -- Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo.
* Hit the `Apply to Join` button. Once your request has been accepted, you will receive an email with your username and password.
* Head over to [https://app.federated.nexus](https://app.federated.nexus/) and you can sign in there, or use any other matrix chat client you wish elsewhere.
* Your username for matrix will be in the form of `@username:federated.nexus`, however you can simply use the `username` part to log in. Your password is your password.
* There's also [https://continuwuity.rocks/](https://continuwuity.rocks/). You can register a new account using Cinny via [this convenient link](https://app.cinny.in/register/continuwuity.rocks), or you can use Element or another matrix client *that supports registration*.
### What are we working on? ### What are we working on?
+2 -2
View File
@@ -6,10 +6,10 @@ set -euo pipefail
COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}" COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}"
# A `.jsonl` file to write test logs to # A `.jsonl` file to write test logs to
LOG_FILE="${2:-tests/test_results/complement/test_logs.jsonl}" LOG_FILE="${2:-complement_test_logs.jsonl}"
# A `.jsonl` file to write test results to # A `.jsonl` file to write test results to
RESULTS_FILE="${3:-tests/test_results/complement/test_results.jsonl}" RESULTS_FILE="${3:-complement_test_results.jsonl}"
# The base docker image to use for complement tests # The base docker image to use for complement tests
# You can build the default with `docker build -t continuwuity:complement -f ./docker/complement.Dockerfile .` # You can build the default with `docker build -t continuwuity:complement -f ./docker/complement.Dockerfile .`
+1
View File
@@ -0,0 +1 @@
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.
+1
View File
@@ -0,0 +1 @@
Introduce a resolver command to allow flushing a server from the cache or to flush the complete cache. Contributed by @Omar007
-1
View File
@@ -1 +0,0 @@
Removed non-compliant nor functional room alias lookups over federation. Contributed by @nex
-1
View File
@@ -1 +0,0 @@
Outgoing presence is now disabled by default, and the config option documentation has been adjusted to more accurately represent the weight of presence, typing indicators, and read receipts. Contributed by @nex.
-1
View File
@@ -1 +0,0 @@
Removed ability to set rocksdb as read only. Doing so would cause unintentional and buggy behaviour. Contributed by @Terryiscool160.
-1
View File
@@ -1 +0,0 @@
Fixed a startup crash in the sender service if we can't detect the number of CPU cores, even if the `sender_workers' config option is set correctly. Contributed by @katie.
-1
View File
@@ -1 +0,0 @@
Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted work. Contributed by @nex and @Jade.
-1
View File
@@ -1 +0,0 @@
Added MSC3202 Device masquerading (not all of MSC3202). This should fix issues with enabling MSC4190 for some Mautrix bridges. Contributed by @Jade
-1
View File
@@ -1 +0,0 @@
Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim.
-1
View File
@@ -1 +0,0 @@
Implement MSC4143 MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new `[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim
-1
View File
@@ -1 +0,0 @@
Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim
@@ -1 +0,0 @@
Updated `list-backups` admin command to output one backup per line.
-1
View File
@@ -1 +0,0 @@
Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and broken.
+3 -3
View File
@@ -9,9 +9,10 @@ address = "0.0.0.0"
allow_device_name_federation = true allow_device_name_federation = true
allow_guest_registration = true allow_guest_registration = true
allow_public_room_directory_over_federation = true allow_public_room_directory_over_federation = true
allow_public_room_directory_without_auth = true
allow_registration = true allow_registration = true
database_path = "/database" database_path = "/database"
log = "trace,h2=debug,hyper=debug,conduwuit_database=warn,conduwuit_service::manager=info,conduwuit_api::router=error,conduwuit_router=error,tower_http=error" log = "trace,h2=debug,hyper=debug"
port = [8008, 8448] port = [8008, 8448]
trusted_servers = [] trusted_servers = []
only_query_trusted_key_servers = false only_query_trusted_key_servers = false
@@ -24,7 +25,7 @@ url_preview_domain_explicit_denylist = ["*"]
media_compat_file_link = false media_compat_file_link = false
media_startup_check = true media_startup_check = true
prune_missing_media = true prune_missing_media = true
log_colors = false log_colors = true
admin_room_notices = false admin_room_notices = false
allow_check_for_updates = false allow_check_for_updates = false
intentionally_unknown_config_option_for_testing = true intentionally_unknown_config_option_for_testing = true
@@ -47,7 +48,6 @@ federation_idle_timeout = 300
sender_timeout = 300 sender_timeout = 300
sender_idle_timeout = 300 sender_idle_timeout = 300
sender_retry_backoff_limit = 300 sender_retry_backoff_limit = 300
force_disable_first_run_mode = true
[global.tls] [global.tls]
dual_protocol = true dual_protocol = true
+27 -78
View File
@@ -290,25 +290,6 @@
# #
#max_fetch_prev_events = 192 #max_fetch_prev_events = 192
# How many incoming federation transactions the server is willing to be
# processing at any given time before it becomes overloaded and starts
# rejecting further transactions until some slots become available.
#
# Setting this value too low or too high may result in unstable
# federation, and setting it too high may cause runaway resource usage.
#
#max_concurrent_inbound_transactions = 150
# Maximum age (in seconds) for cached federation transaction responses.
# Entries older than this will be removed during cleanup.
#
#transaction_id_cache_max_age_secs = 7200 (2 hours)
# Maximum number of cached federation transaction responses.
# When the cache exceeds this limit, older entries will be removed.
#
#transaction_id_cache_max_entries = 8192
# Default/base connection timeout (seconds). This is used only by URL # Default/base connection timeout (seconds). This is used only by URL
# previews and update/news endpoint checks. # previews and update/news endpoint checks.
# #
@@ -452,7 +433,7 @@
# If you would like registration only via token reg, please configure # If you would like registration only via token reg, please configure
# `registration_token`. # `registration_token`.
# #
#allow_registration = true #allow_registration = false
# If registration is enabled, and this setting is true, new users # If registration is enabled, and this setting is true, new users
# registered after the first admin user will be automatically suspended # registered after the first admin user will be automatically suspended
@@ -546,6 +527,12 @@
# #
#allow_public_room_directory_over_federation = false #allow_public_room_directory_over_federation = false
# Set this to true to allow your server's public room directory to be
# queried without client authentication (access token) through the Client
# APIs. Set this to false to protect against /publicRooms spiders.
#
#allow_public_room_directory_without_auth = false
# Allow guests/unauthenticated users to access TURN credentials. # Allow guests/unauthenticated users to access TURN credentials.
# #
# This is the equivalent of Synapse's `turn_allow_guests` config option. # This is the equivalent of Synapse's `turn_allow_guests` config option.
@@ -1069,6 +1056,14 @@
# #
#rocksdb_repair = false #rocksdb_repair = false
# This item is undocumented. Please contribute documentation for it.
#
#rocksdb_read_only = false
# This item is undocumented. Please contribute documentation for it.
#
#rocksdb_secondary = false
# Enables idle CPU priority for compaction thread. This is not enabled by # Enables idle CPU priority for compaction thread. This is not enabled by
# default to prevent compaction from falling too far behind on busy # default to prevent compaction from falling too far behind on busy
# systems. # systems.
@@ -1125,34 +1120,27 @@
# Allow local (your server only) presence updates/requests. # Allow local (your server only) presence updates/requests.
# #
# Local presence must be enabled for outgoing presence to function. # Note that presence on continuwuity is very fast unlike Synapse's. If
# # using outgoing presence, this MUST be enabled.
# Note that local presence is not as heavy on the CPU as federated
# presence, but will still become more expensive the more local users you
# have.
# #
#allow_local_presence = true #allow_local_presence = true
# Allow incoming federated presence updates. # Allow incoming federated presence updates/requests.
# #
# This option enables processing inbound presence updates from other # This option receives presence updates from other servers, but does not
# servers. Without it, remote users will appear as if they are always # send any unless `allow_outgoing_presence` is true. Note that presence on
# offline to your local users. This does not affect typing indicators or # continuwuity is very fast unlike Synapse's.
# read receipts.
# #
#allow_incoming_presence = true #allow_incoming_presence = true
# Allow outgoing presence updates/requests. # Allow outgoing presence updates/requests.
# #
# This option sends presence updates to other servers, and requires that # This option sends presence updates to other servers, but does not
# `allow_local_presence` is also enabled. # receive any unless `allow_incoming_presence` is true. Note that presence
# on continuwuity is very fast unlike Synapse's. If using outgoing
# presence, you MUST enable `allow_local_presence` as well.
# #
# Note that outgoing presence is very heavy on the CPU and network, and #allow_outgoing_presence = true
# will typically cause extreme strain and slowdowns for no real benefit.
# There are only a few clients that even implement presence, so you
# probably don't want to enable this.
#
#allow_outgoing_presence = false
# How many seconds without presence updates before you become idle. # How many seconds without presence updates before you become idle.
# Defaults to 5 minutes. # Defaults to 5 minutes.
@@ -1186,10 +1174,6 @@
# Allow sending read receipts to remote servers. # Allow sending read receipts to remote servers.
# #
# Note that sending read receipts to remote servers in large rooms with
# lots of other homeservers may cause additional strain on the CPU and
# network.
#
#allow_outgoing_read_receipts = true #allow_outgoing_read_receipts = true
# Allow local typing updates. # Allow local typing updates.
@@ -1201,10 +1185,6 @@
# Allow outgoing typing updates to federation. # Allow outgoing typing updates to federation.
# #
# Note that sending typing indicators to remote servers in large rooms
# with lots of other homeservers may cause additional strain on the CPU
# and network.
#
#allow_outgoing_typing = true #allow_outgoing_typing = true
# Allow incoming typing updates from federation. # Allow incoming typing updates from federation.
@@ -1338,7 +1318,7 @@
# sender user's server name, inbound federation X-Matrix origin, and # sender user's server name, inbound federation X-Matrix origin, and
# outbound federation handler. # outbound federation handler.
# #
# You can set this to [".*"] to block all servers by default, and then # You can set this to ["*"] to block all servers by default, and then
# use `allowed_remote_server_names` to allow only specific servers. # use `allowed_remote_server_names` to allow only specific servers.
# #
# example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"] # example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"]
@@ -1494,10 +1474,6 @@
# #
#url_preview_check_root_domain = false #url_preview_check_root_domain = false
# User agent that is used specifically when fetching url previews.
#
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
# List of forbidden room aliases and room IDs as strings of regex # List of forbidden room aliases and room IDs as strings of regex
# patterns. # patterns.
# #
@@ -1844,16 +1820,6 @@
# #
#support_mxid = #support_mxid =
# **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
#
# A list of MatrixRTC foci URLs which will be served as part of the
# MSC4143 client endpoint at /.well-known/matrix/client.
#
# This option is deprecated and will be removed in a future release.
# Please migrate to the new `[global.matrix_rtc]` config section.
#
#rtc_focus_server_urls = []
[global.blurhashing] [global.blurhashing]
# blurhashing x component, 4 is recommended by https://blurha.sh/ # blurhashing x component, 4 is recommended by https://blurha.sh/
@@ -1872,23 +1838,6 @@
# #
#blurhash_max_raw_size = 33554432 #blurhash_max_raw_size = 33554432
[global.matrix_rtc]
# A list of MatrixRTC foci (transports) which will be served via the
# MSC4143 RTC transports endpoint at
# `/_matrix/client/v1/rtc/transports`. If you're setting up livekit,
# you'd want something like:
# ```toml
# [global.matrix_rtc]
# foci = [
# { type = "livekit", livekit_service_url = "https://livekit.example.com" },
# ]
# ```
#
# To disable, set this to an empty list (`[]`).
#
#foci = []
[global.ldap] [global.ldap]
# Whether to enable LDAP login. # Whether to enable LDAP login.
+2 -12
View File
@@ -48,11 +48,11 @@ EOF
# Developer tool versions # Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall # renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.17.5 ENV BINSTALL_VERSION=1.17.4
# renovate: datasource=github-releases depName=psastras/sbom-rs # renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1 ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree # renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.5.0 ENV LDDTREE_VERSION=0.4.0
# renovate: datasource=crate depName=timelord-cli # renovate: datasource=crate depName=timelord-cli
ENV TIMELORD_VERSION=3.0.1 ENV TIMELORD_VERSION=3.0.1
@@ -162,7 +162,6 @@ ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA
ARG RUST_PROFILE=release ARG RUST_PROFILE=release
ARG CARGO_FEATURES="default,http3"
# Build the binary # Build the binary
RUN --mount=type=cache,target=/usr/local/cargo/registry \ RUN --mount=type=cache,target=/usr/local/cargo/registry \
@@ -172,20 +171,11 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
set -o allexport set -o allexport
set -o xtrace set -o xtrace
. /etc/environment . /etc/environment
# Check if http3 feature is enabled and set appropriate RUSTFLAGS
if echo "${CARGO_FEATURES}" | grep -q "http3"; then
export RUSTFLAGS="${RUSTFLAGS} --cfg reqwest_unstable"
else
export RUSTFLAGS="${RUSTFLAGS}"
fi
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \ TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
jq -r ".target_directory")) jq -r ".target_directory"))
mkdir /out/sbin mkdir /out/sbin
PACKAGE=conduwuit PACKAGE=conduwuit
xx-cargo build --locked --profile ${RUST_PROFILE} \ xx-cargo build --locked --profile ${RUST_PROFILE} \
--no-default-features --features ${CARGO_FEATURES} \
-p $PACKAGE; -p $PACKAGE;
BINARIES=($(cargo metadata --no-deps --format-version 1 | \ BINARIES=($(cargo metadata --no-deps --format-version 1 | \
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name")) jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
+3 -3
View File
@@ -2,9 +2,9 @@ FROM ubuntu:latest
EXPOSE 8008 EXPOSE 8008
EXPOSE 8448 EXPOSE 8448
RUN apt-get update && apt-get install -y ca-certificates liburing2 && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y ca-certificates liburing2 && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /etc/continuwuity /var/lib/continuwuity /usr/local/bin/ RUN mkdir -p /etc/continuwuity /var/lib/continuwuity
COPY complement/complement-entrypoint.sh /usr/local/bin/complement-entrypoint.sh COPY docker/complement-entrypoint.sh /usr/local/bin/complement-entrypoint.sh
COPY complement/complement.config.toml /etc/continuwuity/config.toml COPY docker/complement.config.toml /etc/continuwuity/config.toml
COPY target/debug/conduwuit /usr/local/bin/conduwuit COPY target/debug/conduwuit /usr/local/bin/conduwuit
RUN chmod +x /usr/local/bin/conduwuit /usr/local/bin/complement-entrypoint.sh RUN chmod +x /usr/local/bin/conduwuit /usr/local/bin/complement-entrypoint.sh
#HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://localhost:8008/_continuwuity/server_version || exit 1 #HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://localhost:8008/_continuwuity/server_version || exit 1
+2 -2
View File
@@ -18,11 +18,11 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions # Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall # renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.17.5 ENV BINSTALL_VERSION=1.17.4
# renovate: datasource=github-releases depName=psastras/sbom-rs # renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1 ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree # renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.5.0 ENV LDDTREE_VERSION=0.4.0
# Install unpackaged tools # Install unpackaged tools
RUN <<EOF RUN <<EOF
+3 -3
View File
@@ -15,9 +15,9 @@
"label": "Deploying" "label": "Deploying"
}, },
{ {
"type": "dir", "type": "file",
"name": "calls", "name": "turn",
"label": "Calls" "label": "TURN"
}, },
{ {
"type": "file", "type": "file",
+1 -1
View File
@@ -2,7 +2,7 @@
{ {
"text": "Guide", "text": "Guide",
"link": "/introduction", "link": "/introduction",
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting)" "activeMatch": "^/(introduction|configuration|deploying|turn|appservices|maintenance|troubleshooting)"
}, },
{ {
"text": "Development", "text": "Development",
-13
View File
@@ -1,13 +0,0 @@
# 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
View File
@@ -1,12 +0,0 @@
[
{
"type": "file",
"name": "turn",
"label": "TURN"
},
{
"type": "file",
"name": "livekit",
"label": "MatrixRTC / LiveKit"
}
]
-240
View File
@@ -1,240 +0,0 @@
# 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 the `[global.matrix_rtc]` config section using the `foci` option.
The variable should be a list of servers serving as MatrixRTC endpoints. Clients discover these via the `/_matrix/client/v1/rtc/transports` endpoint (MSC4143).
```toml
[global.matrix_rtc]
foci = [
{ 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.
### 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
View File
@@ -1,214 +0,0 @@
# 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
+1 -1
View File
@@ -217,4 +217,4 @@ Alternatively, you can use Continuwuity's built-in delegation file capability. S
## Voice communication ## Voice communication
See the [Calls](../calls.mdx) page. See the [TURN](../turn.md) page.
-2
View File
@@ -3,5 +3,3 @@
Continuwuity currently does not provide FreeBSD builds or FreeBSD packaging. However, Continuwuity does build and work on FreeBSD using the system-provided RocksDB. Continuwuity currently does not provide FreeBSD builds or FreeBSD packaging. However, Continuwuity does build and work on FreeBSD using the system-provided RocksDB.
Contributions to get Continuwuity packaged for FreeBSD are welcome. Contributions to get Continuwuity packaged for FreeBSD are welcome.
Please join our [Continuwuity BSD](https://matrix.to/#/%23bsd:continuwuity.org) community room.
+2 -4
View File
@@ -56,8 +56,6 @@ If wanting to build using standard Rust toolchains, make sure you install:
You can build Continuwuity using `cargo build --release`. You can build Continuwuity using `cargo build --release`.
Continuwuity supports various optional features that can be enabled during compilation. Please see the Cargo.toml file for a comprehensive list, or ask in our rooms.
### Building with Nix ### Building with Nix
If you prefer, you can use Nix (or [Lix](https://lix.systems)) to build Continuwuity. This provides improved reproducibility and makes it easy to set up a build environment and generate output. This approach also allows for easy cross-compilation. If you prefer, you can use Nix (or [Lix](https://lix.systems)) to build Continuwuity. This provides improved reproducibility and makes it easy to set up a build environment and generate output. This approach also allows for easy cross-compilation.
@@ -271,7 +269,7 @@ curl https://your.server.name:8448/_matrix/federation/v1/version
``` ```
- To check if your server can communicate with other homeservers, use the - To check if your server can communicate with other homeservers, use the
[Matrix Federation Tester](https://federationtester.mtrnord.blog/). If you can [Matrix Federation Tester](https://federationtester.matrix.org/). If you can
register but cannot join federated rooms, check your configuration and verify register but cannot join federated rooms, check your configuration and verify
that port 8448 is open and forwarded correctly. that port 8448 is open and forwarded correctly.
@@ -279,7 +277,7 @@ that port 8448 is open and forwarded correctly.
## Audio/Video calls ## Audio/Video calls
For Audio/Video call functionality see the [Calls](../calls.md) page. For Audio/Video call functionality see the [TURN Guide](../turn.md).
## Appservices ## Appservices
+1 -103
View File
@@ -1,109 +1,7 @@
# Continuwuity for Kubernetes # Continuwuity for Kubernetes
Continuwuity doesn't support horizontal scalability or distributed loading Continuwuity doesn't support horizontal scalability or distributed loading
natively. However, a deployment in Kubernetes is very similar to the docker natively. However, [a community-maintained Helm Chart is available here to run
setup. This is because Continuwuity can be fully configured using environment
variables. A sample StatefulSet is shared below. The only thing missing is
a PVC definition (named `continuwuity-data`) for the volume mounted to
the StatefulSet, an Ingress resources to point your webserver to the
Continuwuity Pods, and a Service resource (targeting `app.kubernetes.io/name: continuwuity`)
to glue the Ingress and Pod together.
Carefully go through the `env` section and add, change, and remove any env vars you like using the [Configuration reference](https://continuwuity.org/reference/config.html)
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: continuwuity
namespace: matrix
labels:
app.kubernetes.io/name: continuwuity
spec:
replicas: 1
serviceName: continuwuity
podManagementPolicy: Parallel
selector:
matchLabels:
app.kubernetes.io/name: continuwuity
template:
metadata:
labels:
app.kubernetes.io/name: continuwuity
spec:
securityContext:
sysctls:
- name: net.ipv4.ip_unprivileged_port_start
value: "0"
containers:
- name: continuwuity
# use a sha hash <3
image: forgejo.ellis.link/continuwuation/continuwuity:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
volumeMounts:
- mountPath: /data
name: data
subPath: data
securityContext:
capabilities:
add:
- NET_BIND_SERVICE
env:
- name: TOKIO_WORKER_THREADS
value: "2"
- name: CONTINUWUITY_SERVER_NAME
value: "example.com"
- name: CONTINUWUITY_DATABASE_PATH
value: "/data/db"
- name: CONTINUWUITY_DATABASE_BACKEND
value: "rocksdb"
- name: CONTINUWUITY_PORT
value: "80"
- name: CONTINUWUITY_MAX_REQUEST_SIZE
value: "20000000"
- name: CONTINUWUITY_ALLOW_FEDERATION
value: "true"
- name: CONTINUWUITY_TRUSTED_SERVERS
value: '["matrix.org"]'
- name: CONTINUWUITY_ADDRESS
value: "0.0.0.0"
- name: CONTINUWUITY_ROCKSDB_PARALLELISM_THREADS
value: "1"
- name: CONTINUWUITY_WELL_KNOWN__SERVER
value: "matrix.example.com:443"
- name: CONTINUWUITY_WELL_KNOWN__CLIENT
value: "https://matrix.example.com"
- name: CONTINUWUITY_ALLOW_REGISTRATION
value: "false"
- name: RUST_LOG
value: info
readinessProbe:
httpGet:
path: /_matrix/federation/v1/version
port: http
periodSeconds: 4
failureThreshold: 5
resources:
# Continuwuity might use quite some RAM :3
requests:
cpu: "2"
memory: "512Mi"
limits:
cpu: "4"
memory: "2048Mi"
volumes:
- name: data
persistentVolumeClaim:
claimName: continuwuity-data
```
---
Apart from manually configuring the containers,
[a community-maintained Helm Chart is available here to run
conduwuit on Kubernetes](https://gitlab.cronce.io/charts/conduwuit) conduwuit on Kubernetes](https://gitlab.cronce.io/charts/conduwuit)
This should be compatible with Continuwuity, but you will need to change the image reference. This should be compatible with Continuwuity, but you will need to change the image reference.
+1 -7
View File
@@ -51,13 +51,7 @@ continuwuity aims to:
Check out the [documentation](https://continuwuity.org) for installation instructions. Check out the [documentation](https://continuwuity.org) for installation instructions.
If you want to try it out as a user, we have some partnered homeservers you can use: There are currently no open registration continuwuity instances available.
* You can head over to [https://federated.nexus](https://federated.nexus/) in your browser.
* Hit the `Apply to Join` button. Once your request has been accepted, you will receive an email with your username and password.
* Head over to [https://app.federated.nexus](https://app.federated.nexus/) and you can sign in there, or use any other matrix chat client you wish elsewhere.
* Your username for matrix will be in the form of `@username:federated.nexus`, however you can simply use the `username` part to log in. Your password is your password.
* There's also [https://continuwuity.rocks/](https://continuwuity.rocks/). You can register a new account using Cinny via [this convenient link](https://app.cinny.in/register/continuwuity.rocks), or you can use Element or another matrix client *that supports registration*.
## What are we working on? ## What are we working on?
-4
View File
@@ -36,7 +36,3 @@ Deletes all the local media from a local user on our server. This will always ig
## `!admin media delete-all-from-server` ## `!admin media delete-all-from-server`
Deletes all remote media from the specified remote server. This will always ignore errors by default Deletes all remote media from the specified remote server. This will always ignore errors by default
## `!admin media delete-url-preview`
Deletes a cached URL preview, forcing it to be re-fetched. Use --all to purge all cached URL previews
+5 -20
View File
@@ -1,28 +1,13 @@
# Troubleshooting Continuwuity # Troubleshooting Continuwuity
:::warning{title="Docker users:"} > **Docker users ⚠️**
Docker can be difficult to use and debug. It's common for Docker >
misconfigurations to cause issues, particularly with networking and permissions. > Docker can be difficult to use and debug. It's common for Docker
Please check that your issues are not due to problems with your Docker setup. > misconfigurations to cause issues, particularly with networking and permissions.
::: > Please check that your issues are not due to problems with your Docker setup.
## Continuwuity and Matrix issues ## Continuwuity and Matrix issues
### Slow joins to rooms
Some slowness is to be expected if you're the first person on your homserver to join a room (which will
always be the case for single-user homeservers). In this situation, your homeserver has to verify the signatures of
all of the state events sent by other servers before your join. To make this process as fast as possible, make sure you have
multiple fast, trusted servers listed in `trusted_servers` in your configuration, and ensure
`query_trusted_key_servers_first_on_join` is set to true (the default).
If you need suggestions for trusted servers, ask in the Continuwuity main room.
However, _very_ slow joins, especially to rooms with only a few users in them or rooms created by another user
on your homeserver, may be caused by [issue !779](https://forgejo.ellis.link/continuwuation/continuwuity/issues/779),
which is a longstanding bug with synchronizing room joins to clients. In this situation, you did succeed in joining the room, but
the bug caused your homeserver to forget to tell your client. **To fix this, clear your client's cache.** Both Element and Cinny
have a button to clear their cache in the "About" section of their settings.
### Lost access to admin room ### Lost access to admin room
You can reinvite yourself to the admin room through the following methods: You can reinvite yourself to the admin room through the following methods:
+94
View File
@@ -0,0 +1,94 @@
# 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/)
+3 -14
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 # we need to keep the `web` directory which would be filtered out by the regular source filtering function
# #
# https://crane.dev/API.html#cranelibcleancargosource # https://crane.dev/API.html#cranelibcleancargosource
isWebTemplate = path: _type: builtins.match ".*(src/(web|service)|docs).*" path != null; isWebTemplate = path: _type: builtins.match ".*src/web.*" path != null;
isRust = craneLib.filterCargoSources; isRust = craneLib.filterCargoSources;
isNix = path: _type: builtins.match ".+/nix.*" path != null; isNix = path: _type: builtins.match ".+/nix.*" path != null;
webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t); webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t);
@@ -77,12 +77,7 @@ rec {
craneLib.buildDepsOnly ( craneLib.buildDepsOnly (
(commonAttrs commonAttrsArgs) (commonAttrs commonAttrsArgs)
// { // {
env = uwuenv.buildDepsOnlyEnv env = uwuenv.buildDepsOnlyEnv // (makeRocksDBEnv { inherit rocksdb; });
// (makeRocksDBEnv { inherit rocksdb; })
// {
# required since we started using unstable reqwest apparently ... otherwise the all-features build will fail
RUSTFLAGS = "--cfg reqwest_unstable";
};
inherit (features) cargoExtraArgs; inherit (features) cargoExtraArgs;
} }
@@ -107,13 +102,7 @@ rec {
''; '';
cargoArtifacts = deps; cargoArtifacts = deps;
doCheck = true; doCheck = true;
env = env = uwuenv.buildPackageEnv // rocksdbEnv;
uwuenv.buildPackageEnv
// rocksdbEnv
// {
# required since we started using unstable reqwest apparently ... otherwise the all-features build will fail
RUSTFLAGS = "--cfg reqwest_unstable";
};
passthru.env = uwuenv.buildPackageEnv // rocksdbEnv; passthru.env = uwuenv.buildPackageEnv // rocksdbEnv;
meta.mainProgram = crateInfo.pname; meta.mainProgram = crateInfo.pname;
inherit (features) cargoExtraArgs; inherit (features) cargoExtraArgs;
+76 -76
View File
@@ -119,13 +119,13 @@
} }
}, },
"node_modules/@rsbuild/core": { "node_modules/@rsbuild/core": {
"version": "2.0.0-beta.3", "version": "2.0.0-beta.1",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.3.tgz", "resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.1.tgz",
"integrity": "sha512-dfH+Pt2GuF3rWOWGsf5XOhn3Zarvr4DoHwoI1arAsCGvpzoeud3DNGmWPy13tngj0r/YvQRcPTRBCRV4RP5CMw==", "integrity": "sha512-m7L3oi4evTDODcY+Qk3cmY/p7GCaauSRe00D0AkXVohNvxFBt7F49uPwBSThS24I9d31zFuAED2jFqBeBlDqWw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rspack/core": "2.0.0-beta.0", "@rspack/core": "2.0.0-alpha.1",
"@swc/helpers": "^0.5.18", "@swc/helpers": "^0.5.18",
"jiti": "^2.6.1" "jiti": "^2.6.1"
}, },
@@ -159,28 +159,28 @@
} }
}, },
"node_modules/@rspack/binding": { "node_modules/@rspack/binding": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-alpha.1.tgz",
"integrity": "sha512-L6PPqhwZWC2vzwdhBItNPXw+7V4sq+MBDRXLdd8NMqaJSCB5iKdJIbpbEQucST9Nn7V28IYoQTXs6+ol5vWUBA==", "integrity": "sha512-Glz0SNFYPtNVM+ExJ4ocSzW+oQhb1iHTmxqVEAILbL17Hq3N/nwZpo1cWEs6hJjn8cosJIb1VKbbgb/1goEtCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optionalDependencies": { "optionalDependencies": {
"@rspack/binding-darwin-arm64": "2.0.0-beta.0", "@rspack/binding-darwin-arm64": "2.0.0-alpha.1",
"@rspack/binding-darwin-x64": "2.0.0-beta.0", "@rspack/binding-darwin-x64": "2.0.0-alpha.1",
"@rspack/binding-linux-arm64-gnu": "2.0.0-beta.0", "@rspack/binding-linux-arm64-gnu": "2.0.0-alpha.1",
"@rspack/binding-linux-arm64-musl": "2.0.0-beta.0", "@rspack/binding-linux-arm64-musl": "2.0.0-alpha.1",
"@rspack/binding-linux-x64-gnu": "2.0.0-beta.0", "@rspack/binding-linux-x64-gnu": "2.0.0-alpha.1",
"@rspack/binding-linux-x64-musl": "2.0.0-beta.0", "@rspack/binding-linux-x64-musl": "2.0.0-alpha.1",
"@rspack/binding-wasm32-wasi": "2.0.0-beta.0", "@rspack/binding-wasm32-wasi": "2.0.0-alpha.1",
"@rspack/binding-win32-arm64-msvc": "2.0.0-beta.0", "@rspack/binding-win32-arm64-msvc": "2.0.0-alpha.1",
"@rspack/binding-win32-ia32-msvc": "2.0.0-beta.0", "@rspack/binding-win32-ia32-msvc": "2.0.0-alpha.1",
"@rspack/binding-win32-x64-msvc": "2.0.0-beta.0" "@rspack/binding-win32-x64-msvc": "2.0.0-alpha.1"
} }
}, },
"node_modules/@rspack/binding-darwin-arm64": { "node_modules/@rspack/binding-darwin-arm64": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-alpha.1.tgz",
"integrity": "sha512-PPx1+SPEROSvDKmBuCbsE7W9tk07ajPosyvyuafv2wbBI6PW2rNcz62uzpIFS+FTgwwZ5u/06WXRtlD2xW9bKg==", "integrity": "sha512-+6E6pYgpKvs41cyOlqRjpCT3djjL9hnntF61JumM/TNo1aTYXMNNG4b8ZsLMpBq5ZwCy9Dg8oEDe8AZ84rfM7A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -192,9 +192,9 @@
] ]
}, },
"node_modules/@rspack/binding-darwin-x64": { "node_modules/@rspack/binding-darwin-x64": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-alpha.1.tgz",
"integrity": "sha512-GucsfjrSKBZ9cuOTXmHWxeY2wPmaNyvGNxTyzttjRcfwqOWz8r+ku6PCsMSXUqxZRYWW1L9mvtTdlDrzTYJZ0w==", "integrity": "sha512-Ccf9NNupVe67vlaS9zKQJ+BvsAn385uBC1vXnYaUxxHoY/tEwNJf6t+XyDARt7mCtT7+Bu4L/iJ/JEF/MsO5zg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -206,9 +206,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-arm64-gnu": { "node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-alpha.1.tgz",
"integrity": "sha512-nTtYtklRZD4sb2RIFCF9YS8tZ/MjpqIBKVS3YIvdXcfHUdVfmQHTZGtwEuZGg6AxTC5L1hcvkYmTXCG0ok7auw==", "integrity": "sha512-B7omNsPSsinOq2VRD4d4VFrLgHceMQobqlLg0txFUZ7PDjE307gpTcGViWQlUhNCbkZXMPzDeXBFa5ZlEmxgnA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -220,9 +220,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-arm64-musl": { "node_modules/@rspack/binding-linux-arm64-musl": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-alpha.1.tgz",
"integrity": "sha512-S2fshx0Rf7/XYwoMLaqFsVg4y+VAfHzubrczy8AW5xIs6UNC3eRLVTgShLerUPtF6SG+v6NQxQ9JI3vOo2qPOA==", "integrity": "sha512-NCG401ofZcDKlTWD8VHv76Y+02Stmd9Nu5MRbVUBOCTVgXMj8Mgrm5XsGBWUjzd5J/Mvo2hstCKIZxNzmPd8uQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -234,9 +234,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-x64-gnu": { "node_modules/@rspack/binding-linux-x64-gnu": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-alpha.1.tgz",
"integrity": "sha512-yx5Fk1gl7lfkvqcjolNLCNeduIs6C2alMsQ/kZ1pLeP5MPquVOYNqs6EcDPIp+fUjo3lZYtnJBiZKK+QosbzYg==", "integrity": "sha512-Xgp8wJ5gjpPG8I3VMEsVAesfckWryQVUhJkHcxPfNi72QTv8UkMER7Jl+JrlQk7K7nMO5ltokx/VGl1c3tMx+w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -248,9 +248,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-x64-musl": { "node_modules/@rspack/binding-linux-x64-musl": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-alpha.1.tgz",
"integrity": "sha512-sBX4b2W0PgehlAVT224k0Q6GaH6t9HP+hBNDrbX/g6d0hfxZN56gm5NfOTOD1Rien4v7OBEejJ3/uFbm1WjwYQ==", "integrity": "sha512-lrYKcOgsPA1UMswxzFAV37ofkznbtTLCcEas6lxtlT3Dr28P6VRzC8TgVbIiprkm10I0BlThQWDJ3aGzzLj9Kg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -262,9 +262,9 @@
] ]
}, },
"node_modules/@rspack/binding-wasm32-wasi": { "node_modules/@rspack/binding-wasm32-wasi": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-alpha.1.tgz",
"integrity": "sha512-o6OatnNvb4kCzXbCaomhENGaCsO3naIyAqqErew90HeAwa1lfY3NhRfDLeIyuANQ+xqFl34/R7n8q3ZDx3nd4Q==", "integrity": "sha512-rppGiT7CtXlM8st+IgzBDqb7V//1xx5Oe0SY1sxxw0cfOGMpIQCwhJqx/uI6ioqJLZLGX/obt359+hPXyqGl4w==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -276,9 +276,9 @@
} }
}, },
"node_modules/@rspack/binding-win32-arm64-msvc": { "node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-alpha.1.tgz",
"integrity": "sha512-neCzVllXzIqM8p8qKb89qV7wyk233gC/V9VrHIKbGeQjAEzpBsk5GOWlFbq5DDL6tivQ+uzYaTrZWm9tb2qxXg==", "integrity": "sha512-yD2g1JmnCxrix/344r7lBn+RH+Nv8uWP0UDP8kwv4kQGCWr4U7IP8PKFpoyulVOgOUjvJpgImeyrDJ7R8he+5w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -290,9 +290,9 @@
] ]
}, },
"node_modules/@rspack/binding-win32-ia32-msvc": { "node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-alpha.1.tgz",
"integrity": "sha512-/f0n2eO+DxMKQm9IebeMQJITx8M/+RvY/i8d3sAQZBgR53izn8y7EcDlidXpr24/2DvkLbiub8IyCKPlhLB+1A==", "integrity": "sha512-5qpQL5Qz3uYb56pwffEGzznXSX9TNkLpigQbIObfnUwX7WkdjgTT7oTHpjn2sRSLLNiJ/jCp2r4ZHvjmnNRsRA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -304,9 +304,9 @@
] ]
}, },
"node_modules/@rspack/binding-win32-x64-msvc": { "node_modules/@rspack/binding-win32-x64-msvc": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-alpha.1.tgz",
"integrity": "sha512-dx4zgiAT88EQE7kEUpr7Z9EZAwLnO5FhzWzvd/cDK4bkqYsx+rTklgf/c0EYPBeroXCxlGiMsuC9wHAFNK7sFw==", "integrity": "sha512-dZ76NN9tXLaF2gnB/pU+PcK4Adf9tj8dY06KcWk5F81ur2V4UbrMfkWJkQprur8cgL/F49YtFMRWa4yp/qNbpQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -318,13 +318,13 @@
] ]
}, },
"node_modules/@rspack/core": { "node_modules/@rspack/core": {
"version": "2.0.0-beta.0", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-alpha.1.tgz",
"integrity": "sha512-aEqlQQjiXixT5i9S4DFtiAap8ZjF6pOgfY2ALHOizins/QqWyB8dyLxSoXdzt7JixmKcFmHkbL9XahO28BlVUA==", "integrity": "sha512-2KK3hbxrRqzxtzg+ka7LsiEKIWIGIQz317k9HHC2U4IC5yLJ31K8y/vQfA1aIT2QcFls9gW7GyRjp8A4X5cvLA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rspack/binding": "2.0.0-beta.0", "@rspack/binding": "2.0.0-alpha.1",
"@rspack/lite-tapable": "1.1.0" "@rspack/lite-tapable": "1.1.0"
}, },
"engines": { "engines": {
@@ -371,20 +371,20 @@
} }
}, },
"node_modules/@rspress/core": { "node_modules/@rspress/core": {
"version": "2.0.3", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.2.tgz",
"integrity": "sha512-a+JJFiALqMxGJBqR38/lkN6tas42UF4jRIhu6RilC/3DdqpfqR8j6jjQFOmqoNKo6ZGXW2W+i1Pscn6drvoG3w==", "integrity": "sha512-tU8rUVaPyC8o8k4ezgigRVQuZhBAC41KWdwZZ0BldN6o+QXSEIb722RnxCTpa9FGK2riqcwJgM+OqqcqXsFpmw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mdx-js/mdx": "^3.1.1", "@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1", "@mdx-js/react": "^3.1.1",
"@rsbuild/core": "2.0.0-beta.3", "@rsbuild/core": "2.0.0-beta.1",
"@rsbuild/plugin-react": "~1.4.5", "@rsbuild/plugin-react": "~1.4.5",
"@rspress/shared": "2.0.3", "@rspress/shared": "2.0.2",
"@shikijs/rehype": "^3.21.0", "@shikijs/rehype": "^3.21.0",
"@types/unist": "^3.0.3", "@types/unist": "^3.0.3",
"@unhead/react": "^2.1.4", "@unhead/react": "^2.1.2",
"body-scroll-lock": "4.0.0-beta.0", "body-scroll-lock": "4.0.0-beta.0",
"cac": "^6.7.14", "cac": "^6.7.14",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
@@ -428,39 +428,39 @@
} }
}, },
"node_modules/@rspress/plugin-client-redirects": { "node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.3", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.2.tgz",
"integrity": "sha512-9+SoAbfoxM6OCRWx8jWHHi2zwJDcNaej/URx0CWZk8tvQ618yJW5mXJydknlac62399eYh/F7C3w8TZM3ORGVA==", "integrity": "sha512-FOxUBDOGP06+1hL4jgbIxUe0XoEduXIQ0rSjWjzpo2mC+qTdhZUGJ0xYE2laQIfJXYv/up5zk25zjxUBnxsejw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"peerDependencies": { "peerDependencies": {
"@rspress/core": "^2.0.3" "@rspress/core": "^2.0.2"
} }
}, },
"node_modules/@rspress/plugin-sitemap": { "node_modules/@rspress/plugin-sitemap": {
"version": "2.0.3", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.2.tgz",
"integrity": "sha512-SKa7YEAdkUqya2YjMKbakg3kcYMkXgXhTQdDsHd+QlJWN8j8cDPiCcctMZu8iIPeKZlb+hTJkTWvh27LSIKdOA==", "integrity": "sha512-3E0yEif4Pj3RX+QVOsyWXW6IIjuhwh93bhVSmhShmTKi8opH5vnHcRVZZ1z7X/P3MHXFTrC925F8383Sl2qOEg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"peerDependencies": { "peerDependencies": {
"@rspress/core": "^2.0.3" "@rspress/core": "^2.0.2"
} }
}, },
"node_modules/@rspress/shared": { "node_modules/@rspress/shared": {
"version": "2.0.3", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.2.tgz",
"integrity": "sha512-yI9G4P165fSsmm6QoYTUrdgUis1aFnDh04GcM4SQIpL3itvEZhGtItgoeGkX9EWbnEjhriwI8mTqDDJIp+vrGA==", "integrity": "sha512-9+QC8UL1gV2KpRZx4n55vAl6bE38y7eDnGJhdFSHdJkpFbUCiJDk9ZcR6jD/Rrtq7vlT0gfumUk640pxpi3IDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rsbuild/core": "2.0.0-beta.3", "@rsbuild/core": "2.0.0-beta.1",
"@shikijs/rehype": "^3.21.0", "@shikijs/rehype": "^3.21.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.23",
@@ -664,13 +664,13 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/@unhead/react": { "node_modules/@unhead/react": {
"version": "2.1.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.2.tgz",
"integrity": "sha512-3DzMi5nJkUyLVfQF/q78smCvcSy84TTYgTwXVz5s3AjUcLyHro5Z7bLWriwk1dn5+YRfEsec8aPkLCMi5VjMZg==", "integrity": "sha512-VNKa0JJZq5Jp28VuiOMfjAA7CTLHI0SdW/Hs1ZPq2PsNV/cgxGv8quFBGXWx4gfoHB52pejO929RKjIpYX5+iQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"unhead": "2.1.4" "unhead": "2.1.2"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/harlan-zw" "url": "https://github.com/sponsors/harlan-zw"
@@ -3563,9 +3563,9 @@
} }
}, },
"node_modules/unhead": { "node_modules/unhead": {
"version": "2.1.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.4.tgz", "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.2.tgz",
"integrity": "sha512-+5091sJqtNNmgfQ07zJOgUnMIMKzVKAWjeMlSrTdSGPB6JSozhpjUKuMfWEoLxlMAfhIvgOU8Me0XJvmMA/0fA==", "integrity": "sha512-vSihrxyb+zsEUfEbraZBCjdE0p/WSoc2NGDrpwwSNAwuPxhYK1nH3eegf02IENLpn1sUhL8IoO84JWmRQ6tILA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
-14
View File
@@ -1,7 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", "replacements:all"], "extends": ["config:recommended", "replacements:all"],
"dependencyDashboard": true,
"osvVulnerabilityAlerts": true, "osvVulnerabilityAlerts": true,
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
@@ -58,25 +57,12 @@
"matchUpdateTypes": ["minor", "patch"], "matchUpdateTypes": ["minor", "patch"],
"groupName": "github-actions-non-major" "groupName": "github-actions-non-major"
}, },
{
"description": "Batch patch-level Node.js dependency updates",
"matchManagers": ["npm"],
"matchUpdateTypes": ["patch"],
"groupName": "node-patch-updates"
},
{ {
"description": "Pin forgejo artifact actions to prevent breaking changes", "description": "Pin forgejo artifact actions to prevent breaking changes",
"matchManagers": ["github-actions"], "matchManagers": ["github-actions"],
"matchPackageNames": ["forgejo/upload-artifact", "forgejo/download-artifact"], "matchPackageNames": ["forgejo/upload-artifact", "forgejo/download-artifact"],
"enabled": false "enabled": false
}, },
{
"description": "Auto-merge crate-ci/typos minor updates",
"matchPackageNames": ["crate-ci/typos"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeStrategy": "fast-forward"
},
{ {
"description": "Auto-merge renovatebot docker image updates", "description": "Auto-merge renovatebot docker image updates",
"matchDatasources": ["docker"], "matchDatasources": ["docker"],
-3
View File
@@ -56,9 +56,6 @@ export default defineConfig({
}, { }, {
from: '/community$', from: '/community$',
to: '/community/guidelines' to: '/community/guidelines'
}, {
from: "^/turn",
to: "/calls/turn",
} }
] ]
})], })],
+2 -5
View File
@@ -30,15 +30,12 @@ pub(super) async fn incoming_federation(&self) -> Result {
.federation_handletime .federation_handletime
.read(); .read();
let mut msg = format!( let mut msg = format!("Handling {} incoming pdus:\n", map.len());
"Handling {} incoming PDUs across {} active transactions:\n",
map.len(),
self.services.transactions.txn_active_handle_count()
);
for (r, (e, i)) in map.iter() { for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed(); let elapsed = i.elapsed();
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?; writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?;
} }
msg msg
}; };
+1 -19
View File
@@ -29,9 +29,7 @@ pub(super) async fn delete(
.delete(&mxc.as_str().try_into()?) .delete(&mxc.as_str().try_into()?)
.await?; .await?;
return self return Err!("Deleted the MXC from our database and on our filesystem.",);
.write_str("Deleted the MXC from our database and on our filesystem.")
.await;
} }
if let Some(event_id) = event_id { if let Some(event_id) = event_id {
@@ -390,19 +388,3 @@ pub(super) async fn get_remote_thumbnail(
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```")) self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
.await .await
} }
#[admin_command]
pub(super) async fn delete_url_preview(&self, url: Option<String>, all: bool) -> Result {
if all {
self.services.media.clear_url_previews().await;
return self.write_str("Deleted all cached URL previews.").await;
}
let url = url.expect("clap enforces url is required unless --all");
self.services.media.remove_url_preview(&url).await?;
self.write_str(&format!("Deleted cached URL preview for: {url}"))
.await
}
-12
View File
@@ -108,16 +108,4 @@ pub enum MediaCommand {
#[arg(long, default_value("800"))] #[arg(long, default_value("800"))]
height: u32, height: u32,
}, },
/// Deletes a cached URL preview, forcing it to be re-fetched.
/// Use --all to purge all cached URL previews.
DeleteUrlPreview {
/// The URL to clear from the saved preview data
#[arg(required_unless_present = "all")]
url: Option<String>,
/// Purge all cached URL previews
#[arg(long, conflicts_with = "url")]
all: bool,
},
} }
+1 -1
View File
@@ -209,7 +209,7 @@ pub(super) async fn compact(
let parallelism = parallelism.unwrap_or(1); let parallelism = parallelism.unwrap_or(1);
let results = maps let results = maps
.into_iter() .into_iter()
.try_stream::<conduwuit::Error>() .try_stream()
.paralleln_and_then(runtime, parallelism, move |map| { .paralleln_and_then(runtime, parallelism, move |map| {
map.compact_blocking(options.clone())?; map.compact_blocking(options.clone())?;
Ok(map.name().to_owned()) Ok(map.name().to_owned())
+1 -11
View File
@@ -20,17 +20,7 @@ pub enum ResolverCommand {
name: Option<String>, name: Option<String>,
}, },
/// Flush a given server from the resolver caches or flush them completely /// Flush a specific server from the resolver caches or everything
///
/// * Examples:
/// * Flush a specific server:
///
/// `!admin query resolver flush-cache matrix.example.com`
///
/// * Flush all resolver caches completely:
///
/// `!admin query resolver flush-cache --all`
#[command(verbatim_doc_comment)]
FlushCache { FlushCache {
name: Option<OwnedServerName>, name: Option<OwnedServerName>,
-16
View File
@@ -4,14 +4,12 @@ use ruma::OwnedRoomId;
use crate::{PAGE_SIZE, admin_command, get_room_info}; use crate::{PAGE_SIZE, admin_command, get_room_info};
#[allow(clippy::fn_params_excessive_bools)]
#[admin_command] #[admin_command]
pub(super) async fn list_rooms( pub(super) async fn list_rooms(
&self, &self,
page: Option<usize>, page: Option<usize>,
exclude_disabled: bool, exclude_disabled: bool,
exclude_banned: bool, exclude_banned: bool,
include_empty: bool,
no_details: bool, no_details: bool,
) -> Result { ) -> Result {
// TODO: i know there's a way to do this with clap, but i can't seem to find it // TODO: i know there's a way to do this with clap, but i can't seem to find it
@@ -30,20 +28,6 @@ pub(super) async fn list_rooms(
.then_some(room_id) .then_some(room_id)
}) })
.then(|room_id| get_room_info(self.services, 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<_>>() .collect::<Vec<_>>()
.await; .await;
-4
View File
@@ -30,10 +30,6 @@ pub enum RoomCommand {
#[arg(long)] #[arg(long)]
exclude_banned: bool, exclude_banned: bool,
/// Includes disconnected/empty rooms (rooms with zero members)
#[arg(long)]
include_empty: bool,
#[arg(long)] #[arg(long)]
/// Whether to only output room IDs without supplementary room /// Whether to only output room IDs without supplementary room
/// information /// information
+15 -3
View File
@@ -89,7 +89,13 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
locally, if not using get_alias_helper to fetch room ID remotely" locally, if not using get_alias_helper to fetch room ID remotely"
); );
match self.services.rooms.alias.resolve_alias(room_alias).await { match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => { | Ok((room_id, servers)) => {
debug!( debug!(
%room_id, %room_id,
@@ -229,7 +235,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.services .services
.rooms .rooms
.alias .alias
.resolve_alias(room_alias) .resolve_alias(room_alias, None)
.await .await
{ {
| Ok((room_id, servers)) => { | Ok((room_id, servers)) => {
@@ -382,7 +388,13 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
room ID over federation" room ID over federation"
); );
match self.services.rooms.alias.resolve_alias(room_alias).await { match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => { | Ok((room_id, servers)) => {
debug!( debug!(
%room_id, %room_id,
+1 -1
View File
@@ -86,7 +86,7 @@ pub(super) async fn list_backups(&self) -> Result {
.db .db
.backup_list()? .backup_list()?
.try_stream() .try_stream()
.try_for_each(|result| writeln!(self, "{result}")) .try_for_each(|result| write!(self, "{result}"))
.await .await
} }
+27 -4
View File
@@ -5,7 +5,7 @@ use std::{
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room}; use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
use conduwuit::{ use conduwuit::{
Err, Result, debug_warn, error, info, Err, Result, debug, debug_warn, error, info, is_equal_to,
matrix::{Event, pdu::PduBuilder}, matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt}, utils::{self, ReadyExt},
warn, warn,
@@ -140,6 +140,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services.globals.server_name().to_owned(), self.services.globals.server_name().to_owned(),
room_server_name.to_owned(), room_server_name.to_owned(),
], ],
None,
&None, &None,
) )
.await .await
@@ -167,8 +168,27 @@ 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 // we dont add a device since we're not the user, just the creator
// Make the first user to register an administrator and disable first-run mode. // if this account creation is from the CLI / --execute, invite the first user
self.services.firstrun.empower_first_user(&user_id).await?; // 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");
}
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`")) self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
.await .await
@@ -529,6 +549,7 @@ pub(super) async fn force_join_list_of_local_users(
&room_id, &room_id,
Some(String::from(BULK_JOIN_REASON)), Some(String::from(BULK_JOIN_REASON)),
&servers, &servers,
None,
&None, &None,
) )
.await .await
@@ -614,6 +635,7 @@ pub(super) async fn force_join_all_local_users(
&room_id, &room_id,
Some(String::from(BULK_JOIN_REASON)), Some(String::from(BULK_JOIN_REASON)),
&servers, &servers,
None,
&None, &None,
) )
.await .await
@@ -653,7 +675,8 @@ pub(super) async fn force_join_room(
self.services.globals.user_is_local(&user_id), self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user" "Parsed user_id must be a local user"
); );
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, &None).await?; join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, None, &None)
.await?;
self.write_str(&format!("{user_id} has been joined to {room_id}.",)) self.write_str(&format!("{user_id} has been joined to {room_id}.",))
.await .await
-4
View File
@@ -28,10 +28,6 @@ gzip_compression = [
"conduwuit-service/gzip_compression", "conduwuit-service/gzip_compression",
"reqwest/gzip", "reqwest/gzip",
] ]
http3 = [
"conduwuit-core/http3",
"conduwuit-service/http3",
]
io_uring = [ io_uring = [
"conduwuit-service/io_uring", "conduwuit-service/io_uring",
] ]
+31 -34
View File
@@ -3,7 +3,7 @@ use std::fmt::Write;
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Error, Event, Result, debug_info, err, error, info, Err, Error, Event, Result, debug_info, err, error, info, is_equal_to,
matrix::pdu::PduBuilder, matrix::pdu::PduBuilder,
utils::{self, ReadyExt, stream::BroadbandExt}, utils::{self, ReadyExt, stream::BroadbandExt},
warn, warn,
@@ -148,12 +148,7 @@ pub(crate) async fn register_route(
let is_guest = body.kind == RegistrationKind::Guest; let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some(); let emergency_mode_enabled = services.config.emergency_password.is_some();
// Allow registration if it's enabled in the config file or if this is the first if !services.config.allow_registration && body.appservice_info.is_none() {
// 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()) { match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => { | (Some(username), Some(device_display_name)) => {
info!( info!(
@@ -190,10 +185,17 @@ pub(crate) async fn register_route(
))); )));
} }
if is_guest && !services.config.allow_guest_registration { if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services
.registration_tokens
.get_config_file_token()
.is_some()))
{
info!( info!(
"Guest registration disabled, rejecting guest registration attempt, initial device \ "Guest registration disabled / registration enabled with token configured, \
name: \"{}\"", rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("") body.initial_device_display_name.as_deref().unwrap_or("")
); );
return Err!(Request(GuestAccessForbidden("Guest registration is disabled."))); return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
@@ -252,13 +254,6 @@ pub(crate) async fn register_route(
} }
} }
// Don't allow registration with user IDs that aren't local
if !services.globals.user_is_local(&user_id) {
return Err!(Request(InvalidUsername(
"Username {body_username} is not local to this server"
)));
}
user_id user_id
}, },
| Err(e) => { | Err(e) => {
@@ -314,13 +309,6 @@ pub(crate) async fn register_route(
let skip_auth = body.appservice_info.is_some() || is_guest; let skip_auth = body.appservice_info.is_some() || is_guest;
// Populate required UIAA flows // Populate required UIAA flows
if services.firstrun.is_first_run() {
// Registration token forced while in first-run mode
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
});
} else {
if services if services
.registration_tokens .registration_tokens
.iterate_tokens() .iterate_tokens()
@@ -333,7 +321,6 @@ pub(crate) async fn register_route(
stages: vec![AuthType::RegistrationToken], stages: vec![AuthType::RegistrationToken],
}); });
} }
if services.config.recaptcha_private_site_key.is_some() { if services.config.recaptcha_private_site_key.is_some() {
if let Some(pubkey) = &services.config.recaptcha_site_key { if let Some(pubkey) = &services.config.recaptcha_site_key {
// ReCaptcha required // ReCaptcha required
@@ -371,7 +358,6 @@ pub(crate) async fn register_route(
auth_error: None, auth_error: None,
}; };
} }
}
if !skip_auth { if !skip_auth {
match &body.auth { match &body.auth {
@@ -528,13 +514,22 @@ 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 !is_guest {
// Make the first user to register an administrator and disable first-run mode. if let Ok(admin_room) = services.admin.get_admin_room().await {
let was_first_user = services.firstrun.empower_first_user(&user_id).await?; if services
.rooms
// If the registering user was not the first and we're suspending users on .state_cache
// register, suspend them. .room_joined_count(&admin_room)
if !was_first_user && services.config.suspend_on_register { .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 // Note that we can still do auto joins for suspended users
services services
.users .users
@@ -546,14 +541,15 @@ pub(crate) async fn register_route(
services services
.admin .admin
.send_loud_message(RoomMessageEventContent::text_plain(format!( .send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user on \ "User {user_id} has been suspended as they are not the first user \
this server. Please review and unsuspend them if appropriate." on this server. Please review and unsuspend them if appropriate."
))) )))
.await .await
.ok(); .ok();
} }
} }
} }
}
if body.appservice_info.is_none() if body.appservice_info.is_none()
&& !services.server.config.auto_join_rooms.is_empty() && !services.server.config.auto_join_rooms.is_empty()
@@ -587,6 +583,7 @@ pub(crate) async fn register_route(
&room_id, &room_id,
Some("Automatically joining this room upon registration".to_owned()), Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()], &[services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&body.appservice_info, &body.appservice_info,
) )
.boxed() .boxed()
+7 -1
View File
@@ -9,7 +9,7 @@ use ruma::{
}, },
events::{ events::{
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent, AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
RoomAccountDataEventType, GlobalAccountDataEventType, RoomAccountDataEventType,
}, },
serde::Raw, serde::Raw,
}; };
@@ -126,6 +126,12 @@ async fn set_account_data(
))); )));
} }
if event_type_s == GlobalAccountDataEventType::PushRules.to_cow_str() {
return Err!(Request(BadJson(
"This endpoint cannot be used for setting/configuring push rules."
)));
}
let data: serde_json::Value = serde_json::from_str(data.get()) let data: serde_json::Value = serde_json::from_str(data.get())
.map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?; .map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?;
+65 -3
View File
@@ -1,6 +1,12 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result}; use conduwuit::{Err, Result, debug};
use ruma::api::client::alias::{create_alias, delete_alias, get_alias}; use conduwuit_service::Services;
use futures::StreamExt;
use rand::seq::SliceRandom;
use ruma::{
OwnedServerName, RoomAliasId, RoomId,
api::client::alias::{create_alias, delete_alias, get_alias},
};
use crate::Ruma; use crate::Ruma;
@@ -90,9 +96,65 @@ pub(crate) async fn get_alias_route(
) -> Result<get_alias::v3::Response> { ) -> Result<get_alias::v3::Response> {
let room_alias = body.body.room_alias; let room_alias = body.body.room_alias;
let Ok((room_id, servers)) = services.rooms.alias.resolve_alias(&room_alias).await else { let Ok((room_id, servers)) = services.rooms.alias.resolve_alias(&room_alias, None).await
else {
return Err!(Request(NotFound("Room with alias not found."))); return Err!(Request(NotFound("Room with alias not found.")));
}; };
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
debug!(%room_alias, %room_id, "available servers: {servers:?}");
Ok(get_alias::v3::Response::new(room_id, servers)) Ok(get_alias::v3::Response::new(room_id, servers))
} }
async fn room_available_servers(
services: &Services,
room_id: &RoomId,
room_alias: &RoomAliasId,
pre_servers: Vec<OwnedServerName>,
) -> Vec<OwnedServerName> {
// find active servers in room state cache to suggest
let mut servers: Vec<OwnedServerName> = services
.rooms
.state_cache
.room_servers(room_id)
.map(ToOwned::to_owned)
.collect()
.await;
// push any servers we want in the list already (e.g. responded remote alias
// servers, room alias server itself)
servers.extend(pre_servers);
servers.sort_unstable();
servers.dedup();
// shuffle list of servers randomly after sort and dedupe
servers.shuffle(&mut rand::thread_rng());
// insert our server as the very first choice if in list, else check if we can
// prefer the room alias server first
match servers
.iter()
.position(|server_name| services.globals.server_is_ours(server_name))
{
| Some(server_index) => {
servers.swap_remove(server_index);
servers.insert(0, services.globals.server_name().to_owned());
},
| _ => {
match servers
.iter()
.position(|server| server == room_alias.server_name())
{
| Some(alias_server_index) => {
servers.swap_remove(alias_server_index);
servers.insert(0, room_alias.server_name().into());
},
| _ => {},
}
},
}
servers
}
+1 -7
View File
@@ -16,10 +16,7 @@ use ruma::{OwnedEventId, UserId, api::client::context::get_context, events::Stat
use crate::{ use crate::{
Ruma, Ruma,
client::{ client::message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
is_ignored_pdu,
message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
},
}; };
const LIMIT_MAX: usize = 100; const LIMIT_MAX: usize = 100;
@@ -81,9 +78,6 @@ pub(crate) async fn get_context_route(
return Err!(Request(NotFound("Event not found."))); return Err!(Request(NotFound("Event not found.")));
} }
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
is_ignored_pdu(&services, &base_pdu, sender_user).await?;
let base_count = base_id.pdu_count(); let base_count = base_id.pdu_count();
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user); let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
+3 -25
View File
@@ -6,7 +6,6 @@ use conduwuit::{
Err, Result, err, Err, Result, err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize}, utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
}; };
use conduwuit_core::error;
use conduwuit_service::{ use conduwuit_service::{
Services, Services,
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH}, media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
@@ -145,22 +144,12 @@ pub(crate) async fn get_content_route(
server_name: &body.server_name, server_name: &body.server_name,
media_id: &body.media_id, media_id: &body.media_id,
}; };
let FileMeta { let FileMeta {
content, content,
content_type, content_type,
content_disposition, content_disposition,
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await { } = fetch_file(&services, &mxc, user, body.timeout_ms, None).await?;
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
| std::io::ErrorKind::PermissionDenied => {
error!("Permission denied when trying to read file: {e:?}");
return Err!(Request(Unknown("Unknown error when fetching file.")));
},
| _ => return Err!(Request(Unknown("Unknown error when fetching file."))),
},
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
Ok(get_content::v1::Response { Ok(get_content::v1::Response {
file: content.expect("entire file contents"), file: content.expect("entire file contents"),
@@ -196,18 +185,7 @@ pub(crate) async fn get_content_as_filename_route(
content, content,
content_type, content_type,
content_disposition, content_disposition,
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await { } = fetch_file(&services, &mxc, user, body.timeout_ms, Some(&body.filename)).await?;
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
| std::io::ErrorKind::PermissionDenied => {
error!("Permission denied when trying to read file: {e:?}");
return Err!(Request(Unknown("Unknown error when fetching file.")));
},
| _ => return Err!(Request(Unknown("Unknown error when fetching file."))),
},
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
Ok(get_content_as_filename::v1::Response { Ok(get_content_as_filename::v1::Response {
file: content.expect("entire file contents"), file: content.expect("entire file contents"),
+269 -81
View File
@@ -3,7 +3,7 @@ use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, error, info, is_true, Err, Result, debug, debug_info, debug_warn, err, error, info,
matrix::{ matrix::{
StateKey, StateKey,
event::{gen_event_id, gen_event_id_canonical_json}, event::{gen_event_id, gen_event_id_canonical_json},
@@ -26,7 +26,7 @@ use ruma::{
api::{ api::{
client::{ client::{
error::ErrorKind, error::ErrorKind,
membership::{join_room_by_id, join_room_by_id_or_alias}, membership::{ThirdPartySigned, join_room_by_id, join_room_by_id_or_alias},
}, },
federation::{self}, federation::{self},
}, },
@@ -34,7 +34,7 @@ use ruma::{
events::{ events::{
StateEventType, StateEventType,
room::{ room::{
join_rules::JoinRule, join_rules::{AllowRule, JoinRule},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
}, },
}, },
@@ -48,13 +48,9 @@ use service::{
timeline::pdu_fits, timeline::pdu_fits,
}, },
}; };
use tokio::join;
use super::{banned_room_check, validate_remote_member_event_stub}; use super::{banned_room_check, validate_remote_member_event_stub};
use crate::{ use crate::Ruma;
Ruma,
server::{select_authorising_user, user_can_perform_restricted_join},
};
/// # `POST /_matrix/client/r0/rooms/{roomId}/join` /// # `POST /_matrix/client/r0/rooms/{roomId}/join`
/// ///
@@ -120,6 +116,7 @@ pub(crate) async fn join_room_by_id_route(
&body.room_id, &body.room_id,
body.reason.clone(), body.reason.clone(),
&servers, &servers,
body.third_party_signed.as_ref(),
&body.appservice_info, &body.appservice_info,
) )
.boxed() .boxed()
@@ -198,7 +195,11 @@ pub(crate) async fn join_room_by_id_or_alias_route(
(servers, room_id) (servers, room_id)
}, },
| Err(room_alias) => { | Err(room_alias) => {
let (room_id, mut servers) = services.rooms.alias.resolve_alias(&room_alias).await?; let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check( banned_room_check(
&services, &services,
@@ -247,6 +248,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
&room_id, &room_id,
body.reason.clone(), body.reason.clone(),
&servers, &servers,
body.third_party_signed.as_ref(),
appservice_info, appservice_info,
) )
.boxed() .boxed()
@@ -261,6 +263,7 @@ pub async fn join_room_by_id_helper(
room_id: &RoomId, room_id: &RoomId,
reason: Option<String>, reason: Option<String>,
servers: &[OwnedServerName], servers: &[OwnedServerName],
third_party_signed: Option<&ThirdPartySigned>,
appservice_info: &Option<RegistrationInfo>, appservice_info: &Option<RegistrationInfo>,
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await; let state_lock = services.rooms.state.mutex.lock(room_id).await;
@@ -348,7 +351,15 @@ pub async fn join_room_by_id_helper(
} }
if server_in_room { if server_in_room {
join_room_by_id_helper_local(services, sender_user, room_id, reason, servers, state_lock) join_room_by_id_helper_local(
services,
sender_user,
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed() .boxed()
.await?; .await?;
} else { } else {
@@ -359,6 +370,7 @@ pub async fn join_room_by_id_helper(
room_id, room_id,
reason, reason,
servers, servers,
third_party_signed,
state_lock, state_lock,
) )
.boxed() .boxed()
@@ -374,6 +386,7 @@ async fn join_room_by_id_helper_remote(
room_id: &RoomId, room_id: &RoomId,
reason: Option<String>, reason: Option<String>,
servers: &[OwnedServerName], servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard, state_lock: RoomMutexGuard,
) -> Result { ) -> Result {
info!("Joining {room_id} over federation."); info!("Joining {room_id} over federation.");
@@ -383,10 +396,11 @@ async fn join_room_by_id_helper_remote(
info!("make_join finished"); info!("make_join finished");
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1); let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) { if !services.server.supported_room_version(&room_version_id) {
// How did we get here?
return Err!(BadServerResponse( return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit" "Remote room version {room_version_id} is not supported by conduwuit"
)); ));
@@ -415,6 +429,10 @@ async fn join_room_by_id_helper_remote(
} }
}; };
join_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
join_event_stub.insert( join_event_stub.insert(
"origin_server_ts".to_owned(), "origin_server_ts".to_owned(),
CanonicalJsonValue::Integer( CanonicalJsonValue::Integer(
@@ -726,45 +744,87 @@ async fn join_room_by_id_helper_local(
room_id: &RoomId, room_id: &RoomId,
reason: Option<String>, reason: Option<String>,
servers: &[OwnedServerName], servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard, state_lock: RoomMutexGuard,
) -> Result { ) -> Result {
info!("Joining room locally"); debug_info!("We can join locally");
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
let (room_version, join_rules, is_invited) = join!( let mut restricted_join_authorized = None;
services.rooms.state.get_room_version(room_id), match join_rules {
services.rooms.state_accessor.get_join_rules(room_id), | JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
services.rooms.state_cache.is_invited(sender_user, room_id) for restriction in restricted.allow {
); match restriction {
| AllowRule::RoomMembership(membership) => {
let room_version = room_version?; if services
let mut auth_user: Option<OwnedUserId> = None; .rooms
if !is_invited && matches!(join_rules, JoinRule::Restricted(_) | JoinRule::KnockRestricted(_)) .state_cache
{ .is_joined(sender_user, &membership.room_id)
use RoomVersionId::*;
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
// This is a restricted room, check if we can complete the join requirements
// locally.
let needs_auth_user =
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
.await;
if needs_auth_user.is_ok_and(is_true!()) {
// If there was an error or the value is false, we'll try joining over
// federation. Since it's Ok(true), we can authorise this locally.
// If we can't select a local user, this will remain None, the join will fail,
// and we'll fall back to federation.
auth_user = select_authorising_user(services, room_id, sender_user, &state_lock)
.await .await
.ok(); {
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)
.filter(|user| {
trace!("Checking if {user} can invite {sender_user} to {room_id}");
services.rooms.state_accessor.user_can_invite(
room_id,
user,
sender_user,
&state_lock,
)
})
.boxed()
.next()
.await
.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.")));
},
}
};
let content = RoomMemberEventContent { let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(), displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(), avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(), blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(), reason: reason.clone(),
join_authorized_via_users_server: auth_user, join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join) ..RoomMemberEventContent::new(MembershipState::Join)
}; };
@@ -780,7 +840,6 @@ async fn join_room_by_id_helper_local(
) )
.await .await
else { else {
info!("Joined room locally");
return Ok(()); return Ok(());
}; };
@@ -788,13 +847,138 @@ async fn join_room_by_id_helper_local(
return Err(error); return Err(error);
} }
info!( warn!(
?error, ?error,
remote_servers = %servers.len(), servers = %servers.len(),
"Could not join room locally, attempting remote join", "Could not join restricted room locally, attempting remote join",
); );
join_room_by_id_helper_remote(services, sender_user, room_id, reason, servers, state_lock) let Ok((make_join_response, remote_server)) =
.await make_join_request(services, sender_user, room_id, servers).await
else {
return Err(error);
};
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
})?;
validate_remote_member_event_stub(
&MembershipState::Join,
sender_user,
room_id,
&join_event_stub,
)?;
let join_authorized_via_users_server = join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok());
join_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
join_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let join_event = join_event_stub;
let send_join_response = services
.sending
.send_synapse_request(
&remote_server,
federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
},
)
.await?;
if let Some(signed_raw) = send_join_response.room_state.event {
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(&signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(
warn!(%signed_event_id, %event_id, "Server {remote_server} sent event with wrong event ID")
)));
}
drop(state_lock);
services
.rooms
.event_handler
.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true)
.boxed()
.await?;
} else {
return Err(error);
}
Ok(())
} }
async fn make_join_request( async fn make_join_request(
@@ -803,16 +987,17 @@ async fn make_join_request(
room_id: &RoomId, room_id: &RoomId,
servers: &[OwnedServerName], servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> { ) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
let mut make_join_counter: usize = 1; let mut make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
let mut make_join_counter: usize = 0;
let mut incompatible_room_version_count: usize = 0;
for remote_server in servers { for remote_server in servers {
if services.globals.server_is_ours(remote_server) { if services.globals.server_is_ours(remote_server) {
continue; continue;
} }
info!( info!("Asking {remote_server} for make_join ({make_join_counter})");
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
servers.len()
);
let make_join_response = services let make_join_response = services
.sending .sending
.send_federation_request( .send_federation_request(
@@ -840,44 +1025,47 @@ async fn make_join_request(
warn!("make_join response from {remote_server} failed validation: {e}"); warn!("make_join response from {remote_server} failed validation: {e}");
continue; continue;
} }
return Ok((response, remote_server.clone())); make_join_response_and_server = Ok((response, remote_server.clone()));
break;
}, },
| Err(e) => match e.kind() { | Err(e) => {
| ErrorKind::UnableToAuthorizeJoin => { info!("make_join request to {remote_server} failed: {e}");
if matches!(
e.kind(),
ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
) {
incompatible_room_version_count =
incompatible_room_version_count.saturating_add(1);
}
if incompatible_room_version_count > 15 {
info!( info!(
"{remote_server} was unable to verify the joining user satisfied \ "15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
restricted join requirements: {e}. Will continue trying." M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support \
the room version {room_id}: {e}"
); );
}, make_join_response_and_server =
| ErrorKind::UnableToGrantJoin => { Err!(BadServerResponse("Room version is not supported by Conduwuit"));
info!( return make_join_response_and_server;
"{remote_server} believes the joining user satisfies restricted join \ }
rules, but is unable to authorise a join for us. Will continue trying."
); if make_join_counter > 40 {
},
| ErrorKind::IncompatibleRoomVersion { room_version } => {
warn!( warn!(
"{remote_server} reports the room we are trying to join is \ "40 servers failed to provide valid make_join response, assuming no \
v{room_version}, which we do not support." server can assist in joining."
); );
return Err(e); make_join_response_and_server =
}, Err!(BadServerResponse("No server available to assist in joining."));
| ErrorKind::Forbidden { .. } => {
warn!("{remote_server} refuses to let us join: {e}."); return make_join_response_and_server;
return Err(e); }
},
| ErrorKind::NotFound => {
info!(
"{remote_server} does not know about {room_id}: {e}. Will continue \
trying."
);
},
| _ => {
info!("{remote_server} failed to make_join: {e}. Will continue trying.");
},
}, },
} }
if make_join_response_and_server.is_ok() {
break;
} }
info!("All {} servers were unable to assist in joining {room_id} :(", servers.len()); }
Err!(BadServerResponse("No server available to assist in joining."))
make_join_response_and_server
} }
+6 -1
View File
@@ -102,7 +102,11 @@ pub(crate) async fn knock_room_route(
(servers, room_id) (servers, room_id)
}, },
| Err(room_alias) => { | Err(room_alias) => {
let (room_id, mut servers) = services.rooms.alias.resolve_alias(&room_alias).await?; let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check( banned_room_check(
&services, &services,
@@ -249,6 +253,7 @@ async fn knock_room_by_id_helper(
room_id, room_id,
reason.clone(), reason.clone(),
servers, servers,
None,
&None, &None,
) )
.await .await
+8 -21
View File
@@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Error, Result, at, debug_warn, Err, Result, at, debug_warn,
matrix::{ matrix::{
event::{Event, Matches}, event::{Event, Matches},
pdu::PduCount, pdu::PduCount,
@@ -26,7 +26,7 @@ use ruma::{
DeviceId, RoomId, UserId, DeviceId, RoomId, UserId,
api::{ api::{
Direction, Direction,
client::{error::ErrorKind, filter::RoomEventFilter, message::get_message_events}, client::{filter::RoomEventFilter, message::get_message_events},
}, },
events::{ events::{
AnyStateEvent, StateEventType, AnyStateEvent, StateEventType,
@@ -279,30 +279,23 @@ pub(crate) async fn ignored_filter(
is_ignored_pdu(services, pdu, user_id) is_ignored_pdu(services, pdu, user_id)
.await .await
.unwrap_or(true)
.eq(&false) .eq(&false)
.then_some(item) .then_some(item)
} }
/// Determine whether a PDU should be ignored for a given recipient user.
/// Returns True if this PDU should be ignored, returns False otherwise.
///
/// The error SenderIgnored is returned if the sender or the sender's server is
/// ignored by the relevant user. If the error cannot be returned to the user,
/// it should equate to a true value (i.e. ignored).
#[inline] #[inline]
pub(crate) async fn is_ignored_pdu<Pdu>( pub(crate) async fn is_ignored_pdu<Pdu>(
services: &Services, services: &Services,
event: &Pdu, event: &Pdu,
recipient_user: &UserId, recipient_user: &UserId,
) -> Result<bool> ) -> bool
where where
Pdu: Event + Send + Sync, Pdu: Event + Send + Sync,
{ {
// exclude Synapse's dummy events from bloating up response bodies. clients // exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this. // don't need to see this.
if event.kind().to_cow_str() == "org.matrix.dummy_event" { if event.kind().to_cow_str() == "org.matrix.dummy_event" {
return Ok(true); return true;
} }
let sender_user = event.sender(); let sender_user = event.sender();
@@ -317,27 +310,21 @@ where
if !type_ignored { if !type_ignored {
// We cannot safely ignore this type // We cannot safely ignore this type
return Ok(false); return false;
} }
if server_ignored { if server_ignored {
// the sender's server is ignored, so ignore this event // the sender's server is ignored, so ignore this event
return Err(Error::BadRequest( return true;
ErrorKind::SenderIgnored { sender: None },
"The sender's server is ignored by this server.",
));
} }
if user_ignored && !services.config.send_messages_from_ignored_users_to_client { if user_ignored && !services.config.send_messages_from_ignored_users_to_client {
// the recipient of this PDU has the sender ignored, and we're not // the recipient of this PDU has the sender ignored, and we're not
// configured to send ignored messages to clients // configured to send ignored messages to clients
return Err(Error::BadRequest( return true;
ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) },
"You have ignored this sender.",
));
} }
Ok(false) false
} }
#[inline] #[inline]
+2 -25
View File
@@ -1,6 +1,6 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, at, debug_warn, err, Err, Result, at, debug_warn,
matrix::{Event, event::RelationTypeEqual, pdu::PduCount}, matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt}, utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
}; };
@@ -18,7 +18,7 @@ use ruma::{
events::{TimelineEventType, relation::RelationType}, events::{TimelineEventType, relation::RelationType},
}; };
use crate::{Ruma, client::is_ignored_pdu}; use crate::Ruma;
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}` /// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route( pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
@@ -118,14 +118,6 @@ async fn paginate_relations_with_filter(
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."))); return Err!(Request(NotFound("Event not found.")));
} }
let target_pdu = services
.rooms
.timeline
.get_pdu(target)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
is_ignored_pdu(services, &target_pdu, sender_user).await?;
let start: PduCount = from let start: PduCount = from
.map(str::parse) .map(str::parse)
@@ -167,7 +159,6 @@ async fn paginate_relations_with_filter(
.ready_take_while(|(count, _)| Some(*count) != to) .ready_take_while(|(count, _)| Some(*count) != to)
.take(limit) .take(limit)
.wide_filter_map(|item| visibility_filter(services, sender_user, item)) .wide_filter_map(|item| visibility_filter(services, sender_user, item))
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
.then(async |mut pdu| { .then(async |mut pdu| {
if let Err(e) = services if let Err(e) = services
.rooms .rooms
@@ -223,17 +214,3 @@ async fn visibility_filter<Pdu: Event + Send + Sync>(
.await .await
.then_some(item) .then_some(item)
} }
async fn ignored_filter<Pdu: Event + Send + Sync>(
services: &Services,
item: (PduCount, Pdu),
sender_user: &UserId,
) -> Option<(PduCount, Pdu)> {
let (_, pdu) = &item;
if is_ignored_pdu(services, pdu, sender_user).await.ok()? {
None
} else {
Some(item)
}
}
+2 -1
View File
@@ -4,6 +4,7 @@ use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt}; use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services; use conduwuit_service::Services;
use rand::Rng;
use ruma::{ use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{ api::client::{
@@ -243,7 +244,7 @@ fn build_report(report: Report) -> RoomMessageEventContent {
/// random delay sending a response per spec suggestion regarding /// random delay sending a response per spec suggestion regarding
/// enumerating for potential events existing in our server. /// enumerating for potential events existing in our server.
async fn delay_response() { async fn delay_response() {
let time_to_wait = rand::random_range(2..5); let time_to_wait = rand::thread_rng().gen_range(2..5);
debug_info!( debug_info!(
"Got successful /report request, waiting {time_to_wait} seconds before sending \ "Got successful /report request, waiting {time_to_wait} seconds before sending \
successful response." successful response."
+1 -1
View File
@@ -29,7 +29,7 @@ pub(crate) async fn get_room_event_route(
let (mut event, visible) = try_join(event, visible).await?; let (mut event, visible) = try_join(event, visible).await?;
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await? { if !visible || is_ignored_pdu(services, &event, body.sender_user()).await {
return Err!(Request(Forbidden("You don't have permission to view this event."))); return Err!(Request(Forbidden("You don't have permission to view this event.")));
} }
+3 -3
View File
@@ -50,8 +50,8 @@ pub(crate) async fn send_message_event_route(
// Check if this is a new transaction id // Check if this is a new transaction id
if let Ok(response) = services if let Ok(response) = services
.transactions .transaction_ids
.get_client_txn(sender_user, sender_device, &body.txn_id) .existing_txnid(sender_user, sender_device, &body.txn_id)
.await .await
{ {
// The client might have sent a txnid of the /sendToDevice endpoint // The client might have sent a txnid of the /sendToDevice endpoint
@@ -92,7 +92,7 @@ pub(crate) async fn send_message_event_route(
) )
.await?; .await?;
services.transactions.add_client_txnid( services.transaction_ids.add_txnid(
sender_user, sender_user,
sender_device, sender_device,
&body.txn_id, &body.txn_id,
+1 -5
View File
@@ -107,7 +107,7 @@ pub(super) async fn ldap_login(
) -> Result<OwnedUserId> { ) -> Result<OwnedUserId> {
let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() { let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() {
| Some(bind_dn) if bind_dn.contains("{username}") => | Some(bind_dn) if bind_dn.contains("{username}") =>
(bind_dn.replace("{username}", lowercased_user_id.localpart()), None), (bind_dn.replace("{username}", lowercased_user_id.localpart()), false),
| _ => { | _ => {
debug!("Searching user in LDAP"); debug!("Searching user in LDAP");
@@ -144,9 +144,6 @@ pub(super) async fn ldap_login(
.await?; .await?;
} }
// Only sync admin status if LDAP can actually determine it.
// None means LDAP cannot determine admin status (manual config required).
if let Some(is_ldap_admin) = is_ldap_admin {
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await; let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
if is_ldap_admin && !is_conduwuit_admin { if is_ldap_admin && !is_conduwuit_admin {
@@ -154,7 +151,6 @@ pub(super) async fn ldap_login(
} else if !is_ldap_admin && is_conduwuit_admin { } else if !is_ldap_admin && is_conduwuit_admin {
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?; Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
} }
}
Ok(user_id) Ok(user_id)
} }
+2 -2
View File
@@ -342,10 +342,10 @@ async fn allowed_to_send_state_event(
} }
for alias in aliases { for alias in aliases {
let (alias_room_id, _) = services let (alias_room_id, _servers) = services
.rooms .rooms
.alias .alias
.resolve_alias(&alias) .resolve_alias(&alias, None)
.await .await
.map_err(|e| { .map_err(|e| {
err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}"))) err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}")))
+17 -53
View File
@@ -30,8 +30,7 @@ use ruma::{
api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount}, api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount},
directory::RoomTypeFilter, directory::RoomTypeFilter,
events::{ events::{
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent, StateEventType, AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent}, room::member::{MembershipState, RoomMemberEventContent},
typing::TypingEventContent, typing::TypingEventContent,
}, },
@@ -336,9 +335,7 @@ where
let ranges = list.ranges.clone(); let ranges = list.ranges.clone();
for mut range in ranges { for mut range in ranges {
range.0 = range range.0 = uint!(0);
.0
.min(UInt::try_from(active_rooms.len()).unwrap_or(UInt::MAX));
range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1); range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1);
range.1 = range range.1 = range
.1 .1
@@ -536,9 +533,6 @@ where
} }
}); });
let required_state =
collect_required_state(services, room_id, required_state_request).await;
let room_events: Vec<_> = timeline_pdus let room_events: Vec<_> = timeline_pdus
.iter() .iter()
.stream() .stream()
@@ -557,6 +551,21 @@ where
} }
} }
let required_state = required_state_request
.iter()
.stream()
.filter_map(|state| async move {
services
.rooms
.state_accessor
.room_state_get(room_id, &state.0, &state.1)
.await
.map(Event::into_format)
.ok()
})
.collect()
.await;
// Heroes // Heroes
let heroes: Vec<_> = services let heroes: Vec<_> = services
.rooms .rooms
@@ -680,51 +689,6 @@ where
Ok(rooms) Ok(rooms)
} }
/// Collect the required state events for a room
async fn collect_required_state(
services: &Services,
room_id: &RoomId,
required_state_request: &BTreeSet<TypeStateKey>,
) -> Vec<Raw<AnySyncStateEvent>> {
let mut required_state = Vec::new();
let mut wildcard_types: HashSet<&StateEventType> = HashSet::new();
for (event_type, state_key) in required_state_request {
if wildcard_types.contains(event_type) {
continue;
}
if state_key.as_str() == "*" {
wildcard_types.insert(event_type);
if let Ok(keys) = services
.rooms
.state_accessor
.room_state_keys(room_id, event_type)
.await
{
for key in keys {
if let Ok(event) = services
.rooms
.state_accessor
.room_state_get(room_id, event_type, &key)
.await
{
required_state.push(Event::into_format(event));
}
}
}
} else if let Ok(event) = services
.rooms
.state_accessor
.room_state_get(room_id, event_type, state_key)
.await
{
required_state.push(Event::into_format(event));
}
}
required_state
}
async fn collect_typing_events( async fn collect_typing_events(
services: &Services, services: &Services,
sender_user: &UserId, sender_user: &UserId,
+4 -4
View File
@@ -26,8 +26,8 @@ pub(crate) async fn send_event_to_device_route(
// Check if this is a new transaction id // Check if this is a new transaction id
if services if services
.transactions .transaction_ids
.get_client_txn(sender_user, sender_device, &body.txn_id) .existing_txnid(sender_user, sender_device, &body.txn_id)
.await .await
.is_ok() .is_ok()
{ {
@@ -104,8 +104,8 @@ pub(crate) async fn send_event_to_device_route(
// Save transaction id with empty data // Save transaction id with empty data
services services
.transactions .transaction_ids
.add_client_txnid(sender_user, sender_device, &body.txn_id, &[]); .add_txnid(sender_user, sender_device, &body.txn_id, &[]);
Ok(send_event_to_device::v3::Response {}) Ok(send_event_to_device::v3::Response {})
} }
-23
View File
@@ -27,32 +27,9 @@ pub(crate) async fn well_known_client(
identity_server: None, identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }), sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
tile_server: None, tile_server: None,
rtc_foci: services
.config
.matrix_rtc
.effective_foci(&services.config.well_known.rtc_focus_server_urls)
.to_vec(),
}) })
} }
/// # `GET /_matrix/client/v1/rtc/transports`
/// # `GET /_matrix/client/unstable/org.matrix.msc4143/rtc/transports`
///
/// Returns the list of MatrixRTC foci (transports) configured for this
/// homeserver, implementing MSC4143.
pub(crate) async fn get_rtc_transports(
State(services): State<crate::State>,
_body: Ruma<ruma::api::client::discovery::get_rtc_transports::Request>,
) -> Result<ruma::api::client::discovery::get_rtc_transports::Response> {
Ok(ruma::api::client::discovery::get_rtc_transports::Response::new(
services
.config
.matrix_rtc
.effective_foci(&services.config.well_known.rtc_focus_server_urls)
.to_vec(),
))
}
/// # `GET /.well-known/matrix/support` /// # `GET /.well-known/matrix/support`
/// ///
/// Server support contact and support page of a homeserver's domain. /// Server support contact and support page of a homeserver's domain.
+16 -17
View File
@@ -122,23 +122,23 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
// Ruma doesn't have support for multiple paths for a single endpoint yet, and these routes // Ruma doesn't have support for multiple paths for a single endpoint yet, and these routes
// share one Ruma request / response type pair with {get,send}_state_event_for_key_route // share one Ruma request / response type pair with {get,send}_state_event_for_key_route
.route( .route(
"/_matrix/client/r0/rooms/{room_id}/state/{event_type}", "/_matrix/client/r0/rooms/:room_id/state/:event_type",
get(client::get_state_events_for_empty_key_route) get(client::get_state_events_for_empty_key_route)
.put(client::send_state_event_for_empty_key_route), .put(client::send_state_event_for_empty_key_route),
) )
.route( .route(
"/_matrix/client/v3/rooms/{room_id}/state/{event_type}", "/_matrix/client/v3/rooms/:room_id/state/:event_type",
get(client::get_state_events_for_empty_key_route) get(client::get_state_events_for_empty_key_route)
.put(client::send_state_event_for_empty_key_route), .put(client::send_state_event_for_empty_key_route),
) )
// These two endpoints allow trailing slashes // These two endpoints allow trailing slashes
.route( .route(
"/_matrix/client/r0/rooms/{room_id}/state/{event_type}/", "/_matrix/client/r0/rooms/:room_id/state/:event_type/",
get(client::get_state_events_for_empty_key_route) get(client::get_state_events_for_empty_key_route)
.put(client::send_state_event_for_empty_key_route), .put(client::send_state_event_for_empty_key_route),
) )
.route( .route(
"/_matrix/client/v3/rooms/{room_id}/state/{event_type}/", "/_matrix/client/v3/rooms/:room_id/state/:event_type/",
get(client::get_state_events_for_empty_key_route) get(client::get_state_events_for_empty_key_route)
.put(client::send_state_event_for_empty_key_route), .put(client::send_state_event_for_empty_key_route),
) )
@@ -177,14 +177,13 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_mutual_rooms_route) .ruma_route(&client::get_mutual_rooms_route)
.ruma_route(&client::get_room_summary) .ruma_route(&client::get_room_summary)
.route( .route(
"/_matrix/client/unstable/im.nheko.summary/rooms/{room_id_or_alias}/summary", "/_matrix/client/unstable/im.nheko.summary/rooms/:room_id_or_alias/summary",
get(client::get_room_summary_legacy) get(client::get_room_summary_legacy)
) )
.ruma_route(&client::get_suspended_status) .ruma_route(&client::get_suspended_status)
.ruma_route(&client::put_suspended_status) .ruma_route(&client::put_suspended_status)
.ruma_route(&client::well_known_support) .ruma_route(&client::well_known_support)
.ruma_route(&client::well_known_client) .ruma_route(&client::well_known_client)
.ruma_route(&client::get_rtc_transports)
.route("/_conduwuit/server_version", get(client::conduwuit_server_version)) .route("/_conduwuit/server_version", get(client::conduwuit_server_version))
.route("/_continuwuity/server_version", get(client::conduwuit_server_version)) .route("/_continuwuity/server_version", get(client::conduwuit_server_version))
.ruma_route(&client::room_initial_sync_route) .ruma_route(&client::room_initial_sync_route)
@@ -197,7 +196,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&server::get_server_version_route) .ruma_route(&server::get_server_version_route)
.route("/_matrix/key/v2/server", get(server::get_server_keys_route)) .route("/_matrix/key/v2/server", get(server::get_server_keys_route))
.route( .route(
"/_matrix/key/v2/server/{key_id}", "/_matrix/key/v2/server/:key_id",
get(server::get_server_keys_deprecated_route), get(server::get_server_keys_deprecated_route),
) )
.ruma_route(&server::get_public_rooms_route) .ruma_route(&server::get_public_rooms_route)
@@ -233,9 +232,9 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.route("/_continuwuity/local_user_count", get(client::conduwuit_local_user_count)); .route("/_continuwuity/local_user_count", get(client::conduwuit_local_user_count));
} else { } else {
router = router router = router
.route("/_matrix/federation/{*path}", any(federation_disabled)) .route("/_matrix/federation/*path", any(federation_disabled))
.route("/.well-known/matrix/server", any(federation_disabled)) .route("/.well-known/matrix/server", any(federation_disabled))
.route("/_matrix/key/{*path}", any(federation_disabled)) .route("/_matrix/key/*path", any(federation_disabled))
.route("/_conduwuit/local_user_count", any(federation_disabled)) .route("/_conduwuit/local_user_count", any(federation_disabled))
.route("/_continuwuity/local_user_count", any(federation_disabled)); .route("/_continuwuity/local_user_count", any(federation_disabled));
} }
@@ -254,27 +253,27 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
get(client::get_media_preview_legacy_legacy_route), get(client::get_media_preview_legacy_legacy_route),
) )
.route( .route(
"/_matrix/media/v1/download/{server_name}/{media_id}", "/_matrix/media/v1/download/:server_name/:media_id",
get(client::get_content_legacy_legacy_route), get(client::get_content_legacy_legacy_route),
) )
.route( .route(
"/_matrix/media/v1/download/{server_name}/{media_id}/{file_name}", "/_matrix/media/v1/download/:server_name/:media_id/:file_name",
get(client::get_content_as_filename_legacy_legacy_route), get(client::get_content_as_filename_legacy_legacy_route),
) )
.route( .route(
"/_matrix/media/v1/thumbnail/{server_name}/{media_id}", "/_matrix/media/v1/thumbnail/:server_name/:media_id",
get(client::get_content_thumbnail_legacy_legacy_route), get(client::get_content_thumbnail_legacy_legacy_route),
); );
} else { } else {
router = router router = router
.route("/_matrix/media/v1/{*path}", any(legacy_media_disabled)) .route("/_matrix/media/v1/*path", any(legacy_media_disabled))
.route("/_matrix/media/v3/config", any(legacy_media_disabled)) .route("/_matrix/media/v3/config", any(legacy_media_disabled))
.route("/_matrix/media/v3/download/{*path}", any(legacy_media_disabled)) .route("/_matrix/media/v3/download/*path", any(legacy_media_disabled))
.route("/_matrix/media/v3/thumbnail/{*path}", any(legacy_media_disabled)) .route("/_matrix/media/v3/thumbnail/*path", any(legacy_media_disabled))
.route("/_matrix/media/v3/preview_url", any(redirect_legacy_preview)) .route("/_matrix/media/v3/preview_url", any(redirect_legacy_preview))
.route("/_matrix/media/r0/config", any(legacy_media_disabled)) .route("/_matrix/media/r0/config", any(legacy_media_disabled))
.route("/_matrix/media/r0/download/{*path}", any(legacy_media_disabled)) .route("/_matrix/media/r0/download/*path", any(legacy_media_disabled))
.route("/_matrix/media/r0/thumbnail/{*path}", any(legacy_media_disabled)) .route("/_matrix/media/r0/thumbnail/*path", any(legacy_media_disabled))
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview)); .route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
} }
+2
View File
@@ -1,5 +1,6 @@
use std::{mem, ops::Deref}; use std::{mem, ops::Deref};
use async_trait::async_trait;
use axum::{body::Body, extract::FromRequest}; use axum::{body::Body, extract::FromRequest};
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut, Bytes, BytesMut};
use conduwuit::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY}; use conduwuit::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
@@ -78,6 +79,7 @@ where
fn deref(&self) -> &Self::Target { &self.body } fn deref(&self) -> &Self::Target { &self.body }
} }
#[async_trait]
impl<T> FromRequest<State, Body> for Args<T> impl<T> FromRequest<State, Body> for Args<T>
where where
T: IncomingRequest + Send + Sync + 'static, T: IncomingRequest + Send + Sync + 'static,
+9 -28
View File
@@ -14,8 +14,7 @@ use futures::{
pin_mut, pin_mut,
}; };
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName, CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
OwnedUserId, UserId,
api::{ api::{
AuthScheme, IncomingRequest, Metadata, AuthScheme, IncomingRequest, Metadata,
client::{ client::{
@@ -55,8 +54,7 @@ pub(super) async fn auth(
json_body: Option<&CanonicalJsonValue>, json_body: Option<&CanonicalJsonValue>,
metadata: &Metadata, metadata: &Metadata,
) -> Result<Auth> { ) -> Result<Auth> {
let bearer: Option<TypedHeader<Authorization<Bearer>>> = let bearer: Option<TypedHeader<Authorization<Bearer>>> = request.parts.extract().await?;
request.parts.extract().await.unwrap_or(None);
let token = match &bearer { let token = match &bearer {
| Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()), | Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
| None => request.query.access_token.as_deref(), | None => request.query.access_token.as_deref(),
@@ -67,6 +65,11 @@ pub(super) async fn auth(
if metadata.authentication == AuthScheme::None { if metadata.authentication == AuthScheme::None {
match metadata { match metadata {
| &get_public_rooms::v3::Request::METADATA => { | &get_public_rooms::v3::Request::METADATA => {
if !services
.server
.config
.allow_public_room_directory_without_auth
{
match token { match token {
| Token::Appservice(_) | Token::User(_) => { | Token::Appservice(_) | Token::User(_) => {
// we should have validated the token above // we should have validated the token above
@@ -79,6 +82,7 @@ pub(super) async fn auth(
)); ));
}, },
} }
}
}, },
| &get_profile::v3::Request::METADATA | &get_profile::v3::Request::METADATA
| &get_profile_key::unstable::Request::METADATA | &get_profile_key::unstable::Request::METADATA
@@ -229,33 +233,10 @@ async fn auth_appservice(
return Err!(Request(Exclusive("User is not in namespace."))); return Err!(Request(Exclusive("User is not in namespace.")));
} }
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
// The device_id can be provided via `device_id` or
// `org.matrix.msc3202.device_id` query parameter.
let sender_device = if let Some(ref device_id_str) = request.query.device_id {
let device_id: &DeviceId = device_id_str.as_str().into();
// Verify the device exists for this user
if services
.users
.get_device_metadata(&user_id, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as this device."
)));
}
Some(device_id.to_owned())
} else {
None
};
Ok(Auth { Ok(Auth {
origin: None, origin: None,
sender_user: Some(user_id), sender_user: Some(user_id),
sender_device, sender_device: None,
appservice_info: Some(*info), appservice_info: Some(*info),
}) })
} }
-4
View File
@@ -11,10 +11,6 @@ use service::Services;
pub(super) struct QueryParams { pub(super) struct QueryParams {
pub(super) access_token: Option<String>, pub(super) access_token: Option<String>,
pub(super) user_id: Option<String>, pub(super) user_id: Option<String>,
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
#[serde(alias = "org.matrix.msc3202.device_id")]
pub(super) device_id: Option<String>,
} }
pub(super) struct Request { pub(super) struct Request {
+41 -61
View File
@@ -1,9 +1,6 @@
use std::collections::{HashSet, VecDeque};
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Event, Result, debug, info, trace, utils::to_canonical_object, warn}; use conduwuit::{Err, Result, debug, debug_error, info, utils::to_canonical_object};
use ruma::{OwnedEventId, api::federation::event::get_missing_events}; use ruma::api::federation::event::get_missing_events;
use serde_json::{json, value::RawValue};
use super::AccessCheck; use super::AccessCheck;
use crate::Ruma; use crate::Ruma;
@@ -48,76 +45,59 @@ pub(crate) async fn get_missing_events_route(
.unwrap_or(LIMIT_DEFAULT) .unwrap_or(LIMIT_DEFAULT)
.min(LIMIT_MAX); .min(LIMIT_MAX);
let room_version = services.rooms.state.get_room_version(&body.room_id).await?; let mut queued_events = body.latest_events.clone();
// the vec will never have more entries the limit
let mut events = Vec::with_capacity(limit);
let mut queue: VecDeque<OwnedEventId> = VecDeque::from(body.latest_events.clone()); let mut i: usize = 0;
let mut results: Vec<Box<RawValue>> = Vec::with_capacity(limit); while i < queued_events.len() && events.len() < limit {
let mut seen: HashSet<OwnedEventId> = HashSet::from_iter(body.earliest_events.clone()); let Ok(pdu) = services.rooms.timeline.get_pdu(&queued_events[i]).await else {
debug!(
while let Some(next_event_id) = queue.pop_front() { body.origin = body.origin.as_ref().map(tracing::field::display),
if seen.contains(&next_event_id) { "Event {} does not exist locally, skipping", &queued_events[i]
trace!(%next_event_id, "already seen event, skipping"); );
i = i.saturating_add(1);
continue; continue;
}
if results.len() >= limit {
debug!(%next_event_id, "reached limit of events to return, breaking");
break;
}
let mut pdu = match services.rooms.timeline.get_pdu(&next_event_id).await {
| Ok(pdu) => pdu,
| Err(e) => {
warn!("could not find event {next_event_id} while walking missing events: {e}");
continue;
},
}; };
if pdu.room_id_or_hash() != body.room_id {
return Err!(Request(Unknown( if body.earliest_events.contains(&queued_events[i]) {
"Event {next_event_id} is not in room {}", i = i.saturating_add(1);
body.room_id continue;
)));
} }
if !services if !services
.rooms .rooms
.state_accessor .state_accessor
.server_can_see_event(body.origin(), &body.room_id, pdu.event_id()) .server_can_see_event(body.origin(), &body.room_id, &queued_events[i])
.await .await
{ {
debug!(%next_event_id, origin = %body.origin(), "redacting event origin cannot see"); debug!(
pdu.redact(&room_version, json!({}))?; 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);
continue;
} }
trace!( i = i.saturating_add(1);
%next_event_id, let Ok(event) = to_canonical_object(&pdu) else {
prev_events = ?pdu.prev_events().collect::<Vec<_>>(), debug_error!(
"adding event to results and queueing prev events" body.origin = body.origin.as_ref().map(tracing::field::display),
"Failed to convert PDU in database to canonical JSON: {pdu:?}"
); );
queue.extend(pdu.prev_events.clone()); continue;
seen.insert(next_event_id.clone()); };
if body.latest_events.contains(&next_event_id) {
continue; // Don't include latest_events in results, let prev_events = pdu.prev_events.iter().map(ToOwned::to_owned);
// but do include their prev_events in the queue
} let event = services
results.push(
services
.sending .sending
.convert_to_outgoing_federation_event(to_canonical_object(pdu)?) .convert_to_outgoing_federation_event(event)
.await, .await;
);
trace!( queued_events.extend(prev_events);
%next_event_id, events.push(event);
queue_len = queue.len(),
seen_len = seen.len(),
results_len = results.len(),
"event added to results"
);
} }
if !queue.is_empty() { Ok(get_missing_events::v1::Response { events })
debug!("limit reached before queue was empty");
}
results.reverse(); // return oldest first
Ok(get_missing_events::v1::Response { events: results })
} }
+9 -16
View File
@@ -2,7 +2,7 @@ use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use conduwuit::{ use conduwuit::{
Err, Error, PduEvent, Result, err, error, Err, Error, PduEvent, Result, err,
matrix::{Event, event::gen_event_id}, matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256}, utils::{self, hash::sha256},
warn, warn,
@@ -199,27 +199,20 @@ pub(crate) async fn create_invite_route(
for appservice in services.appservice.read().await.values() { for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&recipient_user) { if appservice.is_user_match(&recipient_user) {
let request = ruma::api::appservice::event::push_events::v1::Request { services
.sending
.send_appservice_request(
appservice.registration.clone(),
ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_format()], events: vec![pdu.to_format()],
txn_id: general_purpose::URL_SAFE_NO_PAD txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes())) .encode(sha256::hash(pdu.event_id.as_bytes()))
.into(), .into(),
ephemeral: Vec::new(), ephemeral: Vec::new(),
to_device: Vec::new(), to_device: Vec::new(),
}; },
services )
.sending .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."
))
})?;
} }
} }
} }
+43 -88
View File
@@ -16,8 +16,6 @@ use ruma::{
}, },
}; };
use serde_json::value::to_raw_value; use serde_json::value::to_raw_value;
use service::rooms::state::RoomMutexGuard;
use tokio::join;
use crate::Ruma; use crate::Ruma;
@@ -87,24 +85,16 @@ pub(crate) async fn create_join_event_template_route(
} }
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let (is_invited, is_joined) = join!( let is_invited = services
services
.rooms .rooms
.state_cache .state_cache
.is_invited(&body.user_id, &body.room_id), .is_invited(&body.user_id, &body.room_id)
services .await;
.rooms
.state_cache
.is_joined(&body.user_id, &body.room_id)
);
let join_authorized_via_users_server: Option<OwnedUserId> = { let join_authorized_via_users_server: Option<OwnedUserId> = {
use RoomVersionId::*; use RoomVersionId::*;
if is_joined || is_invited { if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) || is_invited {
// User is already joined or invited and consequently does not need an // room version does not support restricted join rules, or the user is currently
// authorising user // already invited
None
} else if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
// room version does not support restricted join rules
None None
} else if user_can_perform_restricted_join( } else if user_can_perform_restricted_join(
&services, &services,
@@ -114,10 +104,32 @@ pub(crate) async fn create_join_event_template_route(
) )
.await? .await?
{ {
Some( let Some(auth_user) = services
select_authorising_user(&services, &body.room_id, &body.user_id, &state_lock) .rooms
.await?, .state_cache
.local_users_in_room(&body.room_id)
.filter(|user| {
services.rooms.state_accessor.user_can_invite(
&body.room_id,
user,
&body.user_id,
&state_lock,
) )
})
.boxed()
.next()
.await
.map(ToOwned::to_owned)
else {
info!(
"No local user is able to authorize the join of {} into {}",
&body.user_id, &body.room_id
);
return Err!(Request(UnableToGrantJoin(
"No user on this server is able to assist in joining."
)));
};
Some(auth_user)
} else { } else {
None None
} }
@@ -147,7 +159,9 @@ pub(crate) async fn create_join_event_template_route(
) )
.await?; .await?;
drop(state_lock); drop(state_lock);
pdu_json.remove("event_id");
// room v3 and above removed the "event_id" field from remote PDU format
maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
Ok(prepare_join_event::v1::Response { Ok(prepare_join_event::v1::Response {
room_version: Some(room_version_id), room_version: Some(room_version_id),
@@ -155,38 +169,6 @@ pub(crate) async fn create_join_event_template_route(
}) })
} }
/// Attempts to find a user who is able to issue an invite in the target room.
pub(crate) async fn select_authorising_user(
services: &Services,
room_id: &RoomId,
user_id: &UserId,
state_lock: &RoomMutexGuard,
) -> Result<OwnedUserId> {
let auth_user = services
.rooms
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
services
.rooms
.state_accessor
.user_can_invite(room_id, user, user_id, state_lock)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned);
match auth_user {
| Some(auth_user) => Ok(auth_user),
| None => {
Err!(Request(UnableToGrantJoin(
"No user on this server is able to assist in joining."
)))
},
}
}
/// Checks whether the given user can join the given room via a restricted join. /// Checks whether the given user can join the given room via a restricted join.
pub(crate) async fn user_can_perform_restricted_join( pub(crate) async fn user_can_perform_restricted_join(
services: &Services, services: &Services,
@@ -198,9 +180,12 @@ pub(crate) async fn user_can_perform_restricted_join(
// restricted rooms are not supported on <=v7 // restricted rooms are not supported on <=v7
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) { if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
// This should be impossible as it was checked earlier on, but retain this check return Ok(false);
// for safety. }
unreachable!("user_can_perform_restricted_join got incompatible room version");
if services.rooms.state_cache.is_joined(user_id, room_id).await {
// joining user is already joined, there is nothing we need to do
return Ok(false);
} }
let Ok(join_rules_event_content) = services let Ok(join_rules_event_content) = services
@@ -220,31 +205,17 @@ pub(crate) async fn user_can_perform_restricted_join(
let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) = let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) =
join_rules_event_content.join_rule join_rules_event_content.join_rule
else { else {
// This is not a restricted room
return Ok(false); return Ok(false);
}; };
if r.allow.is_empty() { if r.allow.is_empty() {
// This will never be authorisable, return forbidden. debug_info!("{room_id} is restricted but the allow key is empty");
return Err!(Request(Forbidden("You are not invited to this room."))); return Ok(false);
} }
let mut could_satisfy = true;
for allow_rule in &r.allow { for allow_rule in &r.allow {
match allow_rule { match allow_rule {
| AllowRule::RoomMembership(membership) => { | AllowRule::RoomMembership(membership) => {
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &membership.room_id)
.await
{
// Since we can't check this room, mark could_satisfy as false
// so that we can return M_UNABLE_TO_AUTHORIZE_JOIN later.
could_satisfy = false;
continue;
}
if services if services
.rooms .rooms
.state_cache .state_cache
@@ -268,8 +239,6 @@ pub(crate) async fn user_can_perform_restricted_join(
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))), | Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
}, },
| _ => { | _ => {
// We don't recognise this join rule, so we cannot satisfy the request.
could_satisfy = false;
debug_info!( debug_info!(
"Unsupported allow rule in restricted join for room {}: {:?}", "Unsupported allow rule in restricted join for room {}: {:?}",
room_id, room_id,
@@ -279,24 +248,10 @@ pub(crate) async fn user_can_perform_restricted_join(
} }
} }
if could_satisfy {
// We were able to check all the restrictions and can be certain that the
// prospective member is not permitted to join.
Err!(Request(Forbidden(
"You do not belong to any of the rooms or spaces required to join this room."
)))
} else {
// We were unable to check all the restrictions. This usually means we aren't in
// one of the rooms this one is restricted to, ergo can't check its state for
// the user's membership, and consequently the user *might* be able to join if
// they ask another server.
Err!(Request(UnableToAuthorizeJoin( Err!(Request(UnableToAuthorizeJoin(
"You do not belong to any of the recognised rooms or spaces required to join this \ "Joining user is not known to be in any required room."
room, but this server is unable to verify every requirement. You may be able to \
join via another server."
))) )))
} }
}
pub(crate) fn maybe_strip_event_id( pub(crate) fn maybe_strip_event_id(
pdu_json: &mut CanonicalJsonObject, pdu_json: &mut CanonicalJsonObject,
+1 -1
View File
@@ -40,7 +40,7 @@ pub(crate) async fn get_room_information_route(
servers.sort_unstable(); servers.sort_unstable();
servers.dedup(); servers.dedup();
servers.shuffle(&mut rand::rng()); servers.shuffle(&mut rand::thread_rng());
// insert our server as the very first choice if in list // insert our server as the very first choice if in list
if let Some(server_index) = servers if let Some(server_index) = servers
+59 -209
View File
@@ -1,33 +1,27 @@
use std::{ use std::{collections::BTreeMap, net::IpAddr, time::Instant};
collections::{BTreeMap, HashMap, HashSet},
net::IpAddr,
time::{Duration, Instant},
};
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Error, Result, debug, debug_warn, err, error, Err, Error, Result, debug, debug_warn, err, error,
result::LogErr, result::LogErr,
state_res::lexicographical_topological_sort,
trace, trace,
utils::{ utils::{
IterStream, ReadyExt, millis_since_unix_epoch, IterStream, ReadyExt, millis_since_unix_epoch,
stream::{BroadbandExt, TryBroadbandExt, automatic_width}, stream::{BroadbandExt, TryBroadbandExt, automatic_width},
}, },
warn,
}; };
use conduwuit_service::{ use conduwuit_service::{
Services, Services,
sending::{EDU_LIMIT, PDU_LIMIT}, sending::{EDU_LIMIT, PDU_LIMIT},
}; };
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt}; use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
use http::StatusCode;
use itertools::Itertools; use itertools::Itertools;
use ruma::{ use ruma::{
CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
RoomId, ServerName, UserId,
api::{ api::{
client::error::{ErrorKind, ErrorKind::LimitExceeded}, client::error::ErrorKind,
federation::transactions::{ federation::transactions::{
edu::{ edu::{
DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent, DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
@@ -38,16 +32,9 @@ use ruma::{
}, },
}, },
events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType}, events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
int,
serde::Raw, serde::Raw,
to_device::DeviceIdOrAllDevices, to_device::DeviceIdOrAllDevices,
uint,
}; };
use service::transactions::{
FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse,
};
use tokio::sync::watch::{Receiver, Sender};
use tracing::instrument;
use crate::Ruma; use crate::Ruma;
@@ -57,6 +44,15 @@ type Pdu = (OwnedRoomId, OwnedEventId, CanonicalJsonObject);
/// # `PUT /_matrix/federation/v1/send/{txnId}` /// # `PUT /_matrix/federation/v1/send/{txnId}`
/// ///
/// Push EDUs and PDUs to this server. /// Push EDUs and PDUs to this server.
#[tracing::instrument(
name = "txn",
level = "debug",
skip_all,
fields(
%client,
origin = body.origin().as_str()
),
)]
pub(crate) async fn send_transaction_message_route( pub(crate) async fn send_transaction_message_route(
State(services): State<crate::State>, State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
@@ -80,73 +76,16 @@ pub(crate) async fn send_transaction_message_route(
))); )));
} }
let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
// Atomically check cache, join active, or start new transaction
match services
.transactions
.get_or_start_federation_txn(txn_key.clone())?
{
| FederationTxnState::Cached(response) => {
// Already responded
Ok(response)
},
| FederationTxnState::Active(receiver) => {
// Another thread is processing
wait_for_result(receiver).await
},
| FederationTxnState::Started { receiver, sender } => {
// We're the first, spawn the processing task
services
.server
.runtime()
.spawn(process_inbound_transaction(services, body, client, txn_key, sender));
// and wait for it
wait_for_result(receiver).await
},
}
}
async fn wait_for_result(
mut recv: Receiver<WrappedTransactionResponse>,
) -> Result<send_transaction_message::v1::Response> {
if tokio::time::timeout(Duration::from_secs(50), recv.changed())
.await
.is_err()
{
// Took too long, return 429 to encourage the sender to try again
return Err(Error::BadRequest(
LimitExceeded { retry_after: None },
"Transaction is being still being processed. Please try again later.",
));
}
let value = recv.borrow_and_update();
match value.clone() {
| Some(Ok(response)) => Ok(response),
| Some(Err(err)) => Err(transaction_error_to_response(&err)),
| None => Err(Error::Request(
ErrorKind::Unknown,
"Transaction processing failed unexpectedly".into(),
StatusCode::INTERNAL_SERVER_ERROR,
)),
}
}
#[instrument(
skip_all,
fields(
id = ?body.transaction_id.as_str(),
origin = ?body.origin()
)
)]
async fn process_inbound_transaction(
services: crate::State,
body: Ruma<send_transaction_message::v1::Request>,
client: IpAddr,
txn_key: TxnKey,
sender: Sender<WrappedTransactionResponse>,
) {
let txn_start_time = Instant::now(); let txn_start_time = Instant::now();
trace!(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = %body.transaction_id,
origin = %body.origin(),
"Starting txn",
);
let pdus = body let pdus = body
.pdus .pdus
.iter() .iter()
@@ -163,79 +102,40 @@ async fn process_inbound_transaction(
.filter_map(Result::ok) .filter_map(Result::ok)
.stream(); .stream();
debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",); let results = handle(&services, &client, body.origin(), txn_start_time, pdus, edus).await?;
let results = match handle(&services, &client, body.origin(), pdus, edus).await {
| Ok(results) => results,
| Err(err) => {
fail_federation_txn(services, &txn_key, &sender, err);
return;
},
};
for (id, result) in &results {
if let Err(e) = result {
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
debug_warn!("Incoming PDU failed {id}: {e:?}");
}
}
}
debug!( debug!(
pdus = body.pdus.len(), pdus = body.pdus.len(),
edus = body.edus.len(), edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(), elapsed = ?txn_start_time.elapsed(),
"Finished processing transaction" id = %body.transaction_id,
origin = %body.origin(),
"Finished txn",
); );
for (id, result) in &results {
if let Err(e) = result {
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
warn!("Incoming PDU failed {id}: {e:?}");
}
}
}
let response = send_transaction_message::v1::Response { Ok(send_transaction_message::v1::Response {
pdus: results pdus: results
.into_iter() .into_iter()
.map(|(e, r)| (e, r.map_err(error::sanitized_message))) .map(|(e, r)| (e, r.map_err(error::sanitized_message)))
.collect(), .collect(),
}; })
services
.transactions
.finish_federation_txn(txn_key, sender, response);
} }
/// Handles a failed federation transaction by sending the error through
/// the channel and cleaning up the transaction state. This allows waiters to
/// receive an appropriate error response.
fn fail_federation_txn(
services: crate::State,
txn_key: &TxnKey,
sender: &Sender<WrappedTransactionResponse>,
err: TransactionError,
) {
debug!("Transaction failed: {err}");
// Remove from active state so the transaction can be retried
services.transactions.remove_federation_txn(txn_key);
// Send the error to any waiters
if let Err(e) = sender.send(Some(Err(err))) {
debug_warn!("Failed to send transaction error to receivers: {e}");
}
}
/// Converts a TransactionError into an appropriate HTTP error response.
fn transaction_error_to_response(err: &TransactionError) -> Error {
match err {
| TransactionError::ShuttingDown => Error::Request(
ErrorKind::Unknown,
"Server is shutting down, please retry later".into(),
StatusCode::SERVICE_UNAVAILABLE,
),
}
}
async fn handle( async fn handle(
services: &Services, services: &Services,
client: &IpAddr, client: &IpAddr,
origin: &ServerName, origin: &ServerName,
started: Instant,
pdus: impl Stream<Item = Pdu> + Send, pdus: impl Stream<Item = Pdu> + Send,
edus: impl Stream<Item = Edu> + Send, edus: impl Stream<Item = Edu> + Send,
) -> std::result::Result<ResolvedMap, TransactionError> { ) -> Result<ResolvedMap> {
// group pdus by room // group pdus by room
let pdus = pdus let pdus = pdus
.collect() .collect()
@@ -252,7 +152,7 @@ async fn handle(
.into_iter() .into_iter()
.try_stream() .try_stream()
.broad_and_then(|(room_id, pdus): (_, Vec<_>)| { .broad_and_then(|(room_id, pdus): (_, Vec<_>)| {
handle_room(services, client, origin, room_id, pdus.into_iter()) handle_room(services, client, origin, started, room_id, pdus.into_iter())
.map_ok(Vec::into_iter) .map_ok(Vec::into_iter)
.map_ok(IterStream::try_stream) .map_ok(IterStream::try_stream)
}) })
@@ -269,51 +169,14 @@ async fn handle(
Ok(results) Ok(results)
} }
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
/// returning them in a topologically sorted order.
///
/// This is used to attempt to process PDUs in an order that respects their
/// dependencies, however it is ultimately the sender's responsibility to send
/// them in a processable order, so this is just a best effort attempt. It does
/// not account for power levels or other tie breaks.
async fn build_local_dag(
pdu_map: &HashMap<OwnedEventId, CanonicalJsonObject>,
) -> Result<Vec<OwnedEventId>> {
debug_assert!(pdu_map.len() >= 2, "needless call to build_local_dag with less than 2 PDUs");
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> = HashMap::new();
for (event_id, value) in pdu_map {
let prev_events = value
.get("prev_events")
.expect("pdu must have prev_events")
.as_array()
.expect("prev_events must be an array")
.iter()
.map(|v| {
OwnedEventId::parse(v.as_str().expect("prev_events values must be strings"))
.expect("prev_events must be valid event IDs")
})
.collect::<HashSet<OwnedEventId>>();
dag.insert(event_id.clone(), prev_events);
}
lexicographical_topological_sort(&dag, &|_| async {
// Note: we don't bother fetching power levels because that would massively slow
// this function down. This is a best-effort attempt to order events correctly
// for processing, however ultimately that should be the sender's job.
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.map_err(|e| err!("failed to resolve local graph: {e}"))
}
async fn handle_room( async fn handle_room(
services: &Services, services: &Services,
_client: &IpAddr, _client: &IpAddr,
origin: &ServerName, origin: &ServerName,
txn_start_time: Instant,
room_id: OwnedRoomId, room_id: OwnedRoomId,
pdus: impl Iterator<Item = Pdu> + Send, pdus: impl Iterator<Item = Pdu> + Send,
) -> std::result::Result<Vec<(OwnedEventId, Result)>, TransactionError> { ) -> Result<Vec<(OwnedEventId, Result)>> {
let _room_lock = services let _room_lock = services
.rooms .rooms
.event_handler .event_handler
@@ -322,40 +185,27 @@ async fn handle_room(
.await; .await;
let room_id = &room_id; let room_id = &room_id;
let pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus pdus.try_stream()
.into_iter() .and_then(|(_, event_id, value)| async move {
.map(|(_, event_id, value)| (event_id, value)) services.server.check_running()?;
.collect(); let pdu_start_time = Instant::now();
// Try to sort PDUs by their dependencies, but fall back to arbitrary order on
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
// responsibility.
let sorted_event_ids = if pdu_map.len() >= 2 {
build_local_dag(&pdu_map).await.unwrap_or_else(|e| {
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
pdu_map.keys().cloned().collect()
})
} else {
pdu_map.keys().cloned().collect()
};
let mut results = Vec::with_capacity(sorted_event_ids.len());
for event_id in sorted_event_ids {
let value = pdu_map
.get(&event_id)
.expect("sorted event IDs must be from the original map")
.clone();
services
.server
.check_running()
.map_err(|_| TransactionError::ShuttingDown)?;
let result = services let result = services
.rooms .rooms
.event_handler .event_handler
.handle_incoming_pdu(origin, room_id, &event_id, value, true) .handle_incoming_pdu(origin, room_id, &event_id, value, true)
.await .await
.map(|_| ()); .map(|_| ());
results.push((event_id, result));
} debug!(
Ok(results) pdu_elapsed = ?pdu_start_time.elapsed(),
txn_elapsed = ?txn_start_time.elapsed(),
"Finished PDU {event_id}",
);
Ok((event_id, result))
})
.try_collect()
.await
} }
async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) { async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) {
@@ -628,8 +478,8 @@ async fn handle_edu_direct_to_device(
// Check if this is a new transaction id // Check if this is a new transaction id
if services if services
.transactions .transaction_ids
.get_client_txn(sender, None, message_id) .existing_txnid(sender, None, message_id)
.await .await
.is_ok() .is_ok()
{ {
@@ -648,8 +498,8 @@ async fn handle_edu_direct_to_device(
// Save transaction id with empty data // Save transaction id with empty data
services services
.transactions .transaction_ids
.add_client_txnid(sender, None, message_id, &[]); .add_txnid(sender, None, message_id, &[]);
} }
async fn handle_edu_direct_to_device_user<Event: Send + Sync>( async fn handle_edu_direct_to_device_user<Event: Send + Sync>(
-4
View File
@@ -24,9 +24,6 @@ conduwuit_mods = [
gzip_compression = [ gzip_compression = [
"reqwest/gzip", "reqwest/gzip",
] ]
http3 = [
"reqwest/http3",
]
hardened_malloc = [ hardened_malloc = [
"dep:hardened_malloc-rs" "dep:hardened_malloc-rs"
] ]
@@ -86,7 +83,6 @@ libloading.optional = true
log.workspace = true log.workspace = true
num-traits.workspace = true num-traits.workspace = true
rand.workspace = true rand.workspace = true
rand_core = { version = "0.6.4", features = ["getrandom"] }
regex.workspace = true regex.workspace = true
reqwest.workspace = true reqwest.workspace = true
ring.workspace = true ring.workspace = true
+26 -131
View File
@@ -19,7 +19,7 @@ pub use figment::{Figment, value::Value as FigmentValue};
use regex::RegexSet; use regex::RegexSet;
use ruma::{ use ruma::{
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::discovery::{discover_homeserver::RtcFocusInfo, discover_support::ContactRole}, api::client::discovery::discover_support::ContactRole,
}; };
use serde::{Deserialize, de::IgnoredAny}; use serde::{Deserialize, de::IgnoredAny};
use url::Url; use url::Url;
@@ -368,31 +368,6 @@ pub struct Config {
#[serde(default = "default_max_fetch_prev_events")] #[serde(default = "default_max_fetch_prev_events")]
pub max_fetch_prev_events: u16, pub max_fetch_prev_events: u16,
/// How many incoming federation transactions the server is willing to be
/// processing at any given time before it becomes overloaded and starts
/// rejecting further transactions until some slots become available.
///
/// Setting this value too low or too high may result in unstable
/// federation, and setting it too high may cause runaway resource usage.
///
/// default: 150
#[serde(default = "default_max_concurrent_inbound_transactions")]
pub max_concurrent_inbound_transactions: usize,
/// Maximum age (in seconds) for cached federation transaction responses.
/// Entries older than this will be removed during cleanup.
///
/// default: 7200 (2 hours)
#[serde(default = "default_transaction_id_cache_max_age_secs")]
pub transaction_id_cache_max_age_secs: u64,
/// Maximum number of cached federation transaction responses.
/// When the cache exceeds this limit, older entries will be removed.
///
/// default: 8192
#[serde(default = "default_transaction_id_cache_max_entries")]
pub transaction_id_cache_max_entries: usize,
/// Default/base connection timeout (seconds). This is used only by URL /// Default/base connection timeout (seconds). This is used only by URL
/// previews and update/news endpoint checks. /// previews and update/news endpoint checks.
/// ///
@@ -584,7 +559,7 @@ pub struct Config {
/// ///
/// If you would like registration only via token reg, please configure /// If you would like registration only via token reg, please configure
/// `registration_token`. /// `registration_token`.
#[serde(default = "true_fn")] #[serde(default)]
pub allow_registration: bool, pub allow_registration: bool,
/// If registration is enabled, and this setting is true, new users /// If registration is enabled, and this setting is true, new users
@@ -678,6 +653,12 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub allow_public_room_directory_over_federation: bool, pub allow_public_room_directory_over_federation: bool,
/// Set this to true to allow your server's public room directory to be
/// queried without client authentication (access token) through the Client
/// APIs. Set this to false to protect against /publicRooms spiders.
#[serde(default)]
pub allow_public_room_directory_without_auth: bool,
/// Allow guests/unauthenticated users to access TURN credentials. /// Allow guests/unauthenticated users to access TURN credentials.
/// ///
/// This is the equivalent of Synapse's `turn_allow_guests` config option. /// This is the equivalent of Synapse's `turn_allow_guests` config option.
@@ -1263,6 +1244,12 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub rocksdb_repair: bool, pub rocksdb_repair: bool,
#[serde(default)]
pub rocksdb_read_only: bool,
#[serde(default)]
pub rocksdb_secondary: bool,
/// Enables idle CPU priority for compaction thread. This is not enabled by /// Enables idle CPU priority for compaction thread. This is not enabled by
/// default to prevent compaction from falling too far behind on busy /// default to prevent compaction from falling too far behind on busy
/// systems. /// systems.
@@ -1322,33 +1309,26 @@ pub struct Config {
/// Allow local (your server only) presence updates/requests. /// Allow local (your server only) presence updates/requests.
/// ///
/// Local presence must be enabled for outgoing presence to function. /// Note that presence on continuwuity is very fast unlike Synapse's. If
/// /// using outgoing presence, this MUST be enabled.
/// Note that local presence is not as heavy on the CPU as federated
/// presence, but will still become more expensive the more local users you
/// have.
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub allow_local_presence: bool, pub allow_local_presence: bool,
/// Allow incoming federated presence updates. /// Allow incoming federated presence updates/requests.
/// ///
/// This option enables processing inbound presence updates from other /// This option receives presence updates from other servers, but does not
/// servers. Without it, remote users will appear as if they are always /// send any unless `allow_outgoing_presence` is true. Note that presence on
/// offline to your local users. This does not affect typing indicators or /// continuwuity is very fast unlike Synapse's.
/// read receipts.
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub allow_incoming_presence: bool, pub allow_incoming_presence: bool,
/// Allow outgoing presence updates/requests. /// Allow outgoing presence updates/requests.
/// ///
/// This option sends presence updates to other servers, and requires that /// This option sends presence updates to other servers, but does not
/// `allow_local_presence` is also enabled. /// receive any unless `allow_incoming_presence` is true. Note that presence
/// /// on continuwuity is very fast unlike Synapse's. If using outgoing
/// Note that outgoing presence is very heavy on the CPU and network, and /// presence, you MUST enable `allow_local_presence` as well.
/// will typically cause extreme strain and slowdowns for no real benefit. #[serde(default = "true_fn")]
/// There are only a few clients that even implement presence, so you
/// probably don't want to enable this.
#[serde(default)]
pub allow_outgoing_presence: bool, pub allow_outgoing_presence: bool,
/// How many seconds without presence updates before you become idle. /// How many seconds without presence updates before you become idle.
@@ -1386,10 +1366,6 @@ pub struct Config {
pub allow_incoming_read_receipts: bool, pub allow_incoming_read_receipts: bool,
/// Allow sending read receipts to remote servers. /// Allow sending read receipts to remote servers.
///
/// Note that sending read receipts to remote servers in large rooms with
/// lots of other homeservers may cause additional strain on the CPU and
/// network.
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub allow_outgoing_read_receipts: bool, pub allow_outgoing_read_receipts: bool,
@@ -1401,10 +1377,6 @@ pub struct Config {
pub allow_local_typing: bool, pub allow_local_typing: bool,
/// Allow outgoing typing updates to federation. /// Allow outgoing typing updates to federation.
///
/// Note that sending typing indicators to remote servers in large rooms
/// with lots of other homeservers may cause additional strain on the CPU
/// and network.
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub allow_outgoing_typing: bool, pub allow_outgoing_typing: bool,
@@ -1544,7 +1516,7 @@ pub struct Config {
/// sender user's server name, inbound federation X-Matrix origin, and /// sender user's server name, inbound federation X-Matrix origin, and
/// outbound federation handler. /// outbound federation handler.
/// ///
/// You can set this to [".*"] to block all servers by default, and then /// You can set this to ["*"] to block all servers by default, and then
/// use `allowed_remote_server_names` to allow only specific servers. /// use `allowed_remote_server_names` to allow only specific servers.
/// ///
/// example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"] /// example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"]
@@ -1724,11 +1696,6 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub url_preview_check_root_domain: bool, pub url_preview_check_root_domain: bool,
/// User agent that is used specifically when fetching url previews.
///
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
pub url_preview_user_agent: Option<String>,
/// List of forbidden room aliases and room IDs as strings of regex /// List of forbidden room aliases and room IDs as strings of regex
/// patterns. /// patterns.
/// ///
@@ -2068,16 +2035,6 @@ pub struct Config {
pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure: pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure:
bool, bool,
/// Forcibly disables first-run mode.
///
/// This is intended to be used for Complement testing to allow the test
/// suite to register users, because first-run mode interferes with open
/// registration.
///
/// display: hidden
#[serde(default)]
pub force_disable_first_run_mode: bool,
/// display: nested /// display: nested
#[serde(default)] #[serde(default)]
pub ldap: LdapConfig, pub ldap: LdapConfig,
@@ -2090,12 +2047,6 @@ pub struct Config {
/// display: nested /// display: nested
#[serde(default)] #[serde(default)]
pub blurhashing: BlurhashConfig, pub blurhashing: BlurhashConfig,
/// Configuration for MatrixRTC (MSC4143) transport discovery.
/// display: nested
#[serde(default)]
pub matrix_rtc: MatrixRtcConfig,
#[serde(flatten)] #[serde(flatten)]
#[allow(clippy::zero_sized_map_values)] #[allow(clippy::zero_sized_map_values)]
// this is a catchall, the map shouldn't be zero at runtime // this is a catchall, the map shouldn't be zero at runtime
@@ -2160,18 +2111,6 @@ pub struct WellKnownConfig {
/// If no email or mxid is specified, all of the server's admins will be /// If no email or mxid is specified, all of the server's admins will be
/// listed. /// listed.
pub support_mxid: Option<OwnedUserId>, pub support_mxid: Option<OwnedUserId>,
/// **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
///
/// A list of MatrixRTC foci URLs which will be served as part of the
/// MSC4143 client endpoint at /.well-known/matrix/client.
///
/// This option is deprecated and will be removed in a future release.
/// Please migrate to the new `[global.matrix_rtc]` config section.
///
/// default: []
#[serde(default)]
pub rtc_focus_server_urls: Vec<RtcFocusInfo>,
} }
#[derive(Clone, Copy, Debug, Deserialize, Default)] #[derive(Clone, Copy, Debug, Deserialize, Default)]
@@ -2199,43 +2138,6 @@ pub struct BlurhashConfig {
pub blurhash_max_raw_size: u64, pub blurhash_max_raw_size: u64,
} }
#[derive(Clone, Debug, Deserialize, Default)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.matrix_rtc")]
pub struct MatrixRtcConfig {
/// A list of MatrixRTC foci (transports) which will be served via the
/// MSC4143 RTC transports endpoint at
/// `/_matrix/client/v1/rtc/transports`. If you're setting up livekit,
/// you'd want something like:
/// ```toml
/// [global.matrix_rtc]
/// foci = [
/// { type = "livekit", livekit_service_url = "https://livekit.example.com" },
/// ]
/// ```
///
/// To disable, set this to an empty list (`[]`).
///
/// default: []
#[serde(default)]
pub foci: Vec<RtcFocusInfo>,
}
impl MatrixRtcConfig {
/// Returns the effective foci, falling back to the deprecated
/// `rtc_focus_server_urls` if the new config is empty.
#[must_use]
pub fn effective_foci<'a>(
&'a self,
deprecated_foci: &'a [RtcFocusInfo],
) -> &'a [RtcFocusInfo] {
if !self.foci.is_empty() {
&self.foci
} else {
deprecated_foci
}
}
}
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")] #[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")]
pub struct LdapConfig { pub struct LdapConfig {
@@ -2429,7 +2331,6 @@ const DEPRECATED_KEYS: &[&str] = &[
"well_known_support_email", "well_known_support_email",
"well_known_support_mxid", "well_known_support_mxid",
"registration_token_file", "registration_token_file",
"well_known.rtc_focus_server_urls",
]; ];
impl Config { impl Config {
@@ -2612,12 +2513,6 @@ fn default_pusher_idle_timeout() -> u64 { 15 }
fn default_max_fetch_prev_events() -> u16 { 192_u16 } fn default_max_fetch_prev_events() -> u16 { 192_u16 }
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
fn default_transaction_id_cache_max_age_secs() -> u64 { 60 * 60 * 2 }
fn default_transaction_id_cache_max_entries() -> usize { 8192 }
fn default_tracing_flame_filter() -> String { fn default_tracing_flame_filter() -> String {
cfg!(debug_assertions) cfg!(debug_assertions)
.then_some("trace,h2=off") .then_some("trace,h2=off")
+1 -2
View File
@@ -85,8 +85,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED, | Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
// 404 // 404
| NotFound | NotImplemented | FeatureDisabled | SenderIgnored { .. } => | NotFound | NotImplemented | FeatureDisabled => StatusCode::NOT_FOUND,
StatusCode::NOT_FOUND,
// 403 // 403
| GuestAccessForbidden | GuestAccessForbidden
+1 -19
View File
@@ -8,13 +8,10 @@
use std::sync::OnceLock; use std::sync::OnceLock;
static BRANDING: &str = "continuwuity"; static BRANDING: &str = "continuwuity";
static WEBSITE: &str = "https://continuwuity.org";
static SEMANTIC: &str = env!("CARGO_PKG_VERSION"); static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
static VERSION: OnceLock<String> = OnceLock::new(); static VERSION: OnceLock<String> = OnceLock::new();
static VERSION_UA: OnceLock<String> = OnceLock::new();
static USER_AGENT: OnceLock<String> = OnceLock::new(); static USER_AGENT: OnceLock<String> = OnceLock::new();
static USER_AGENT_MEDIA: OnceLock<String> = OnceLock::new();
#[inline] #[inline]
#[must_use] #[must_use]
@@ -23,25 +20,10 @@ pub fn name() -> &'static str { BRANDING }
#[inline] #[inline]
pub fn version() -> &'static str { VERSION.get_or_init(init_version) } pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
#[inline]
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
#[inline] #[inline]
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) } pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
#[inline] fn init_user_agent() -> String { format!("{}/{}", name(), version()) }
pub fn user_agent_media() -> &'static str { USER_AGENT_MEDIA.get_or_init(init_user_agent_media) }
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
fn init_user_agent_media() -> String {
format!("{}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", name(), version_ua())
}
fn init_version_ua() -> String {
conduwuit_build_metadata::version_tag()
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
}
fn init_version() -> String { fn init_version() -> String {
conduwuit_build_metadata::version_tag() conduwuit_build_metadata::version_tag()
+1 -1
View File
@@ -1046,7 +1046,7 @@ mod tests {
// don't remove any events so we know it sorts them all correctly // don't remove any events so we know it sorts them all correctly
let mut events_to_sort = events.keys().cloned().collect::<Vec<_>>(); let mut events_to_sort = events.keys().cloned().collect::<Vec<_>>();
events_to_sort.shuffle(&mut rand::rng()); events_to_sort.shuffle(&mut rand::thread_rng());
let power_level = resolved_power let power_level = resolved_power
.get(&(StateEventType::RoomPowerLevels, "".into())) .get(&(StateEventType::RoomPowerLevels, "".into()))
+1 -1
View File
@@ -28,7 +28,7 @@ fn init_argon() -> Argon2<'static> {
} }
pub(super) fn password(password: &str) -> Result<String> { pub(super) fn password(password: &str) -> Result<String> {
let salt = SaltString::generate(rand_core::OsRng); let salt = SaltString::generate(rand::thread_rng());
ARGON ARGON
.get_or_init(init_argon) .get_or_init(init_argon)
.hash_password(password.as_bytes(), &salt) .hash_password(password.as_bytes(), &salt)
+10 -7
View File
@@ -4,16 +4,16 @@ use std::{
}; };
use arrayvec::ArrayString; use arrayvec::ArrayString;
use rand::{RngExt, seq::SliceRandom}; use rand::{Rng, seq::SliceRandom, thread_rng};
pub fn shuffle<T>(vec: &mut [T]) { pub fn shuffle<T>(vec: &mut [T]) {
let mut rng = rand::rng(); let mut rng = thread_rng();
vec.shuffle(&mut rng); vec.shuffle(&mut rng);
} }
pub fn string(length: usize) -> String { pub fn string(length: usize) -> String {
rand::rng() thread_rng()
.sample_iter(&rand::distr::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
.take(length) .take(length)
.map(char::from) .map(char::from)
.collect() .collect()
@@ -22,8 +22,8 @@ pub fn string(length: usize) -> String {
#[inline] #[inline]
pub fn string_array<const LENGTH: usize>() -> ArrayString<LENGTH> { pub fn string_array<const LENGTH: usize>() -> ArrayString<LENGTH> {
let mut ret = ArrayString::<LENGTH>::new(); let mut ret = ArrayString::<LENGTH>::new();
rand::rng() thread_rng()
.sample_iter(&rand::distr::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
.take(LENGTH) .take(LENGTH)
.map(char::from) .map(char::from)
.for_each(|c| ret.push(c)); .for_each(|c| ret.push(c));
@@ -40,4 +40,7 @@ pub fn time_from_now_secs(range: Range<u64>) -> SystemTime {
} }
#[must_use] #[must_use]
pub fn secs(range: Range<u64>) -> Duration { Duration::from_secs(rand::random_range(range)) } pub fn secs(range: Range<u64>) -> Duration {
let mut rng = thread_rng();
Duration::from_secs(rng.gen_range(range))
}
+9 -7
View File
@@ -3,17 +3,19 @@ use futures::{
stream::{Stream, TryStream}, stream::{Stream, TryStream},
}; };
use crate::{Error, Result};
pub trait IterStream<I: IntoIterator + Send> { pub trait IterStream<I: IntoIterator + Send> {
/// Convert an Iterator into a Stream /// Convert an Iterator into a Stream
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send; fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send;
/// Convert an Iterator into a TryStream with a generic error type /// Convert an Iterator into a TryStream
fn try_stream<E>( fn try_stream(
self, self,
) -> impl TryStream< ) -> impl TryStream<
Ok = <I as IntoIterator>::Item, Ok = <I as IntoIterator>::Item,
Error = E, Error = Error,
Item = Result<<I as IntoIterator>::Item, E>, Item = Result<<I as IntoIterator>::Item, Error>,
> + Send; > + Send;
} }
@@ -26,12 +28,12 @@ where
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send { stream::iter(self) } fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send { stream::iter(self) }
#[inline] #[inline]
fn try_stream<E>( fn try_stream(
self, self,
) -> impl TryStream< ) -> impl TryStream<
Ok = <I as IntoIterator>::Item, Ok = <I as IntoIterator>::Item,
Error = E, Error = Error,
Item = Result<<I as IntoIterator>::Item, E>, Item = Result<<I as IntoIterator>::Item, Error>,
> + Send { > + Send {
self.stream().map(Ok) self.stream().map(Ok)
} }
+1 -2
View File
@@ -1,10 +1,9 @@
//! Synchronous combinator extensions to futures::TryStream //! Synchronous combinator extensions to futures::TryStream
use std::result::Result;
use futures::{TryFuture, TryStream, TryStreamExt}; use futures::{TryFuture, TryStream, TryStreamExt};
use super::automatic_width; use super::automatic_width;
use crate::Result;
/// Concurrency extensions to augment futures::TryStreamExt. broad_ combinators /// Concurrency extensions to augment futures::TryStreamExt. broad_ combinators
/// produce out-of-order /// produce out-of-order
+10
View File
@@ -33,6 +33,8 @@ pub struct Engine {
pub(crate) db: Db, pub(crate) db: Db,
pub(crate) pool: Arc<Pool>, pub(crate) pool: Arc<Pool>,
pub(crate) ctx: Arc<Context>, pub(crate) ctx: Arc<Context>,
pub(super) read_only: bool,
pub(super) secondary: bool,
pub(crate) checksums: bool, pub(crate) checksums: bool,
corks: AtomicU32, corks: AtomicU32,
} }
@@ -127,6 +129,14 @@ impl Engine {
sequence sequence
} }
#[inline]
#[must_use]
pub fn is_read_only(&self) -> bool { self.secondary || self.read_only }
#[inline]
#[must_use]
pub fn is_secondary(&self) -> bool { self.secondary }
} }
impl Drop for Engine { impl Drop for Engine {
+2 -1
View File
@@ -12,8 +12,9 @@ pub fn backup(&self) -> Result {
let mut engine = self.backup_engine()?; let mut engine = self.backup_engine()?;
let config = &self.ctx.server.config; let config = &self.ctx.server.config;
if config.database_backups_to_keep > 0 { if config.database_backups_to_keep > 0 {
let flush = !self.is_read_only();
engine engine
.create_new_backup_flush(&self.db, true) .create_new_backup_flush(&self.db, flush)
.map_err(map_err)?; .map_err(map_err)?;
let engine_info = engine.get_backup_info(); let engine_info = engine.get_backup_info();
+10 -1
View File
@@ -35,7 +35,14 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
} }
debug!("Opening database..."); debug!("Opening database...");
let db = Db::open_cf_descriptors(&db_opts, path, cfds).or_else(or_else)?; let db = if config.rocksdb_read_only {
Db::open_cf_descriptors_read_only(&db_opts, path, cfds, false)
} else if config.rocksdb_secondary {
Db::open_cf_descriptors_as_secondary(&db_opts, path, path, cfds)
} else {
Db::open_cf_descriptors(&db_opts, path, cfds)
}
.or_else(or_else)?;
info!( info!(
columns = num_cfds, columns = num_cfds,
@@ -48,6 +55,8 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
db, db,
pool: ctx.pool.clone(), pool: ctx.pool.clone(),
ctx: ctx.clone(), ctx: ctx.clone(),
read_only: config.rocksdb_read_only,
secondary: config.rocksdb_secondary,
checksums: config.rocksdb_checksums, checksums: config.rocksdb_checksums,
corks: AtomicU32::new(0), corks: AtomicU32::new(0),
})) }))
+8
View File
@@ -74,6 +74,14 @@ impl Database {
#[inline] #[inline]
pub fn keys(&self) -> impl Iterator<Item = &MapsKey> + Send + '_ { self.maps.keys() } pub fn keys(&self) -> impl Iterator<Item = &MapsKey> + Send + '_ { self.maps.keys() }
#[inline]
#[must_use]
pub fn is_read_only(&self) -> bool { self.db.is_read_only() }
#[inline]
#[must_use]
pub fn is_secondary(&self) -> bool { self.db.is_secondary() }
} }
impl Index<&str> for Database { impl Index<&str> for Database {
-6
View File
@@ -99,11 +99,6 @@ gzip_compression = [
hardened_malloc = [ hardened_malloc = [
"conduwuit-core/hardened_malloc", "conduwuit-core/hardened_malloc",
] ]
http3 = [
"conduwuit-api/http3",
"conduwuit-core/http3",
"conduwuit-service/http3",
]
io_uring = [ io_uring = [
"conduwuit-database/io_uring", "conduwuit-database/io_uring",
] ]
@@ -235,7 +230,6 @@ tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-journald = { workspace = true, optional = true } tracing-journald = { workspace = true, optional = true }
parking_lot.workspace = true
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies] [target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
+9 -1
View File
@@ -27,6 +27,10 @@ pub struct Args {
#[arg(long, short('O'))] #[arg(long, short('O'))]
pub option: Vec<String>, pub option: Vec<String>,
/// Run in a stricter read-only --maintenance mode.
#[arg(long)]
pub read_only: bool,
/// Run in maintenance mode while refusing connections. /// Run in maintenance mode while refusing connections.
#[arg(long)] #[arg(long)]
pub maintenance: bool, pub maintenance: bool,
@@ -139,7 +143,11 @@ pub(crate) fn parse() -> Args { Args::parse() }
/// Synthesize any command line options with configuration file options. /// Synthesize any command line options with configuration file options.
pub(crate) fn update(mut config: Figment, args: &Args) -> Result<Figment> { pub(crate) fn update(mut config: Figment, args: &Args) -> Result<Figment> {
if args.maintenance { if args.read_only {
config = config.join(("rocksdb_read_only", true));
}
if args.maintenance || args.read_only {
config = config.join(("startup_netburst", false)); config = config.join(("startup_netburst", false));
config = config.join(("listening", false)); config = config.join(("listening", false));
} }
-36
View File
@@ -1,36 +0,0 @@
use std::{thread, time::Duration};
/// Runs a loop that checks for deadlocks every 10 seconds.
///
/// Note that this requires the `deadlock_detection` parking_lot feature to be
/// enabled.
pub(crate) fn deadlock_detection_thread() {
loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = parking_lot::deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
eprintln!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
eprintln!("Deadlock #{i}");
for t in threads {
eprintln!("Thread Id {:#?}", t.thread_id());
eprintln!("{:#?}", t.backtrace());
}
}
}
}
/// Spawns the deadlock detection thread.
///
/// This thread will run in the background and check for deadlocks every 10
/// seconds. When a deadlock is detected, it will print detailed information to
/// stderr.
pub(crate) fn spawn() {
thread::Builder::new()
.name("deadlock_detector".to_owned())
.spawn(deadlock_detection_thread)
.expect("failed to spawn deadlock detection thread");
}
-4
View File
@@ -5,7 +5,6 @@ use std::sync::{Arc, atomic::Ordering};
use conduwuit_core::{debug_info, error}; use conduwuit_core::{debug_info, error};
mod clap; mod clap;
mod deadlock;
mod logging; mod logging;
mod mods; mod mods;
mod panic; mod panic;
@@ -28,9 +27,6 @@ pub fn run() -> Result<()> {
} }
pub fn run_with_args(args: &Args) -> Result<()> { pub fn run_with_args(args: &Args) -> Result<()> {
// Spawn deadlock detection thread
deadlock::spawn();
let runtime = runtime::new(args)?; let runtime = runtime::new(args)?;
let server = Server::new(args, Some(runtime.handle()))?; let server = Server::new(args, Some(runtime.handle()))?;

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