Compare commits

...

86 Commits

Author SHA1 Message Date
Jade Ellis
b8e476626f docs: Add links to matrix guides 2026-02-11 18:25:11 +00:00
Jade Ellis
4e55e1ea90 docs: Add note about checking the contents of configuration 2026-02-11 16:56:07 +00:00
ginger
f5f3108d5f chore: Formatting 2026-02-10 22:56:11 +00:00
chri-k
d1e1ee6156 fix: always treat server_user as an admin 2026-02-10 22:56:11 +00:00
Omar Pakker
ae16a45515 chore: Add towncrier news fragment 2026-02-10 23:07:38 +01:00
Omar Pakker
077bda23a6 feat(admin): Add resolver cache flush command
This command allows an admin to flush a specific server
from the resolver caches or flush the whole cache.
2026-02-10 23:07:32 +01:00
Renovate Bot
a2bf0c1223 chore(deps): update pre-commit hook crate-ci/typos to v1.43.4 2026-02-10 05:02:40 +00:00
Ginger
b9b1ff87f2 chore: Formatting fixes 2026-02-10 02:29:11 +00:00
Ginger
3c0146d437 feat: Implement a migration to fix busted local invites 2026-02-10 02:29:11 +00:00
Ginger
7485d4aa91 fix: Properly set stripped state for local invites 2026-02-10 02:29:11 +00:00
Jade Ellis
39bdb4c5a2 chore: Announcement for v0.5.4 2026-02-09 20:48:47 +00:00
Renovate Bot
55fb3b8848 chore(deps): update pre-commit hook crate-ci/typos to v1.43.3 2026-02-09 15:26:52 +00:00
timedout
19146166c0 chore: Linkify pull requests in CHANGELOG.md 2026-02-08 17:49:53 +00:00
timedout
f47027006f chore: Bump cargo lock 2026-02-08 17:45:51 +00:00
timedout
b7a8f71e14 chore: Bump version 2026-02-08 17:41:53 +00:00
timedout
c7378d15ab chore: Update changelog 2026-02-08 17:41:30 +00:00
timedout
7beeab270e fix: Add failing spell check string to typos
This isn't the proper fix but whatever it makes CI pass
2026-02-08 17:25:09 +00:00
Julian Anderson
6a812b7776 chore: Add news fragment 2026-02-08 17:25:09 +00:00
Julian Anderson
b1f4bbe89e docs(deploying/fedora): Remove seemingly nonexistent/impossible Fedora install method 2026-02-08 17:25:09 +00:00
Julian Anderson
6701f88bf9 docs(deploying/fedora): Fix URLs for known working install methods, add EL caveat, correct GPG key info 2026-02-08 17:25:09 +00:00
Jade Ellis
62b9e8227b docs: Explain enabling backtraces at runtime 2026-02-08 17:23:09 +00:00
Jade Ellis
7369b58d91 feat: Try log original server error 2026-02-08 17:23:09 +00:00
Jade Ellis
f6df44b13f feat: Try log panics before unwinds to catch backtraces 2026-02-08 17:23:09 +00:00
timedout
f243b383cb style: Fix typo in validate_remote_member_event_stub 2026-02-08 15:37:40 +00:00
timedout
e0b7d03018 fix: Perform additional membership validation on remote knocks too 2026-02-08 15:34:07 +00:00
timedout
184ae2ebb9 fix: Apply validation to make_join process 2026-02-06 18:15:39 +00:00
timedout
0ea0d09b97 fix: Don't fail open when a PDU doesn't have a short state hash 2026-02-06 18:09:09 +00:00
timedout
6763952ce4 chore: Bump ruwuma 2026-02-06 17:52:48 +00:00
Renovate Bot
e2da8301df chore(deps): update pre-commit hook crate-ci/typos to v1.43.2 2026-02-06 16:49:57 +00:00
April Grimoire
296a4b92d6 fix: Resolve unnecessary serialization issue
Fixes #1335
2026-02-06 07:52:19 +00:00
timedout
00c054d356 fix: Get_missing_events returns the same event N times 2026-02-05 21:28:21 +00:00
Renovate Bot
2558ec0c2a chore(deps): update rust-patch-updates 2026-02-05 14:06:42 +00:00
timedout
56bc3c184e feat: Enable running complement manually 2026-02-04 18:06:53 +00:00
Renovate Bot
5c1b90b463 chore(deps): update dependency cargo-bins/cargo-binstall to v1.17.4 2026-02-04 16:05:32 +00:00
Renovate Bot
0dbb774559 chore(deps): update dependency @rspress/plugin-sitemap to v2.0.2 2026-02-04 16:04:56 +00:00
Renovate Bot
16e0566c84 chore(deps): update dependency @rspress/plugin-client-redirects to v2.0.2 2026-02-04 16:02:09 +00:00
Renovate Bot
489b6e4ecb chore(deps): update pre-commit hook crate-ci/typos to v1.43.1 2026-02-04 15:58:34 +00:00
Renovate Bot
e71f75a58c chore(deps): update dependency @rspress/core to v2.0.2 2026-02-04 05:04:11 +00:00
timedout
082ed5b70c feat: Use info level logs for residency check failures 2026-02-03 20:09:41 +00:00
timedout
76fe8c4cdc chore: Add news fragment 2026-02-03 20:09:41 +00:00
timedout
c4a9f7a6d1 perf: Don't handle expensive requests for rooms we aren't in
Mostly borrowed from dendrite:

https://github.com/element-hq/dendrite/blob/a042861/federationapi/routing/routing.go#L601
2026-02-03 20:09:41 +00:00
timedout
a047199fb4 perf: Don't handle PDUs for rooms we aren't in 2026-02-03 20:09:41 +00:00
Renovate Bot
411c9da743 chore(deps): update rust-patch-updates 2026-02-02 01:34:58 +00:00
Renovate Bot
fb54f2058c chore(deps): update dependency @rspress/plugin-client-redirects to v2.0.1 2026-02-01 05:03:41 +00:00
ginger
358273226c chore: Update FUNDING.yml 2026-01-31 01:13:15 +00:00
timedout
fd9bbb08ed fix: Restore admin room announcement for deactivations 2026-01-30 05:11:30 +00:00
timedout
53184cd2fc chore: Add news fragment 2026-01-30 05:11:30 +00:00
timedout
25f7d80a8c fix: Clippy lint 2026-01-30 05:11:30 +00:00
timedout
02fa0ba0b8 perf: Optimise account deactivation process 2026-01-30 05:11:30 +00:00
ginger
572b228f40 Update homeserver list 2026-01-29 23:35:07 +00:00
Renovate Bot
b0a61e38da chore(deps): update pre-commit hook crate-ci/typos to v1.42.3 2026-01-29 15:49:54 +00:00
Renovate Bot
401dff20eb chore(deps): update dependency cargo-bins/cargo-binstall to v1.17.3 2026-01-29 15:49:32 +00:00
Ginger
f2a50e8f62 fix(docs): Remove rspress-plugin-preview 2026-01-29 10:41:46 -05:00
Ginger
36e80b0af4 fix(docs): Add stub type definition for docs CSS 2026-01-29 10:36:44 -05:00
Ginger
c9a4c546e2 chore(deps): Update to rspress 2.0.0 2026-01-29 10:35:24 -05:00
Ginger
da8b60b4ce fix(docs): Add redirect from old community page 2026-01-26 21:42:50 -05:00
Ginger
89afaa94ac feat(docs): Move community pages into subdir, add partnered homeservers page 2026-01-26 21:32:05 -05:00
Ginger
2b5563cee3 fix(docs): Remove busted link in nav 2026-01-26 20:55:12 -05:00
Ginger
6cb9d50383 chore: News fragment 2026-01-21 12:27:13 -05:00
Ginger
77c0f6e0c6 fix: Add a code path for clients trying to use fallback auth 2026-01-21 12:27:13 -05:00
Jade Ellis
c85e710760 fix: Add option to mark certain config sections as optional
Fixes #1290
2026-01-20 17:36:22 +00:00
Renovate Bot
59346fc766 chore(deps): update pre-commit hook crate-ci/committed to v1.1.10 2026-01-20 16:25:19 +00:00
Renovate Bot
9c5e735888 chore(deps): update dependency cargo-bins/cargo-binstall to v1.16.7 2026-01-20 16:24:46 +00:00
Ginger
fe74e82318 chore: Formatting 2026-01-20 10:00:26 -05:00
K900
cb79a3b9d7 refactor(treewide): get rid of compile time build environment introspection
It's cursed and not very useful. Still a few uses of ctor left, but oh well.
2026-01-19 19:44:28 +00:00
timedout
ebc8df1c4d feat: Add endpoints required for API-based takedowns and room bans 2026-01-18 18:47:15 +00:00
nex
b667a963cf chore: Fixup typos 2026-01-18 15:22:14 +00:00
timedout
5a6b909b37 fix: Remove homebrewed error mangling for correctness 2026-01-18 15:22:14 +00:00
timedout
dba9cf0ad2 chore: Add news fragment 2026-01-18 15:22:14 +00:00
timedout
287ddd9bc5 fix: Only fall back to legacy media when response is M_UNRECOGNIZED
https://spec.matrix.org/v1.17/server-server-api/#content-repository
Previously we would fall back for ALL
auth media errors.
2026-01-18 15:22:14 +00:00
Jason Volk
79a278b9e8 Fix verification loss; workaround Nheko-Reborn/nheko#1908 (closes #146)
Signed-off-by: Jason Volk <jason@zemos.net>
2026-01-18 14:41:01 +00:00
Ginger
6c5d658ef2 fix: Fix explosions with new tracing 2026-01-15 09:28:26 -05:00
Renovate Bot
70c43abca8 chore(deps): update rust-patch-updates 2026-01-15 09:28:26 -05:00
Renovate Bot
6a9b47c52e chore(deps): update rust-patch-updates 2026-01-15 05:03:40 +00:00
Ginger
c042de96f8 chore(deps): Update rspress to 2.0.0-rc.5 2026-01-14 09:35:20 -05:00
Jade Ellis
7a6acd1c82 chore: Changelog 2026-01-13 20:29:30 +00:00
Jade Ellis
d260c4fcc2 style: Fix yo unused variables 2026-01-13 20:29:30 +00:00
Jade Ellis
fa15de9764 feat: Admin announce improvements
- Check announcements on first start
- Print out any fetch errors on first start in the admin room
- Randomly jitter the next check
2026-01-13 20:29:30 +00:00
Jade Ellis
e6c7a4ae60 docs: Changelog 2026-01-13 00:05:20 +00:00
Jade Ellis
5bed4ad81d chore: Admin announcement 2026-01-13 00:01:28 +00:00
Jade Ellis
587abe9d14 chore: Release 2026-01-12 23:47:37 +00:00
Jade Ellis
c499042a76 docs: Changelog 2026-01-12 23:45:42 +00:00
timedout
86e450a835 fix: M_BAD_JSON in send_join and send_knock 2026-01-12 17:53:37 +00:00
Jade Ellis
4c796029bb chore: Add correct configuration for cargo release 2026-01-12 16:20:38 +00:00
Jade Ellis
fc3615c46b docs: Changelog 2026-01-12 16:20:38 +00:00
Jade Ellis
7375f7a68e feat: Improve the display of the configuration in the admin room 2026-01-12 16:20:38 +00:00
104 changed files with 2358 additions and 2800 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,4 @@
github: [JadedBlueEyes, nexy7574]
github: [JadedBlueEyes, nexy7574, gingershaped]
custom:
- https://ko-fi.com/nexy7574
- https://ko-fi.com/JadedBlueEyes

View File

@@ -23,7 +23,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.41.0
rev: v1.43.4
hooks:
- id: typos
- id: typos
@@ -31,7 +31,7 @@ repos:
stages: [commit-msg]
- repo: https://github.com/crate-ci/committed
rev: v1.1.9
rev: v1.1.10
hooks:
- id: committed

View File

@@ -6,14 +6,13 @@ extend-exclude = ["*.csr", "*.lock", "pnpm-lock.yaml"]
extend-ignore-re = [
"(?Rm)^.*(#|//|<!--)\\s*spellchecker:disable-line(\\s*-->)$", # Ignore a line by making it trail with a `spellchecker:disable-line` comment
"^[0-9a-f]{7,}$", # Commit hashes
"4BA7",
# some heuristics for base64 strings
"[A-Za-z0-9+=]{72,}",
"([A-Za-z0-9+=]|\\\\\\s\\*){72,}",
"[0-9+][A-Za-z0-9+]{30,}[a-z0-9+]",
"\\$[A-Z0-9+][A-Za-z0-9+]{6,}[a-z0-9+]",
"\\b[a-z0-9+/=][A-Za-z0-9+/=]{7,}[a-z0-9+/=][A-Z]\\b",
# In the renovate config
".ontainer"
]

View File

@@ -1,37 +1,98 @@
# Continuwuity v0.5.4 (2026-02-08)
## Features
- The announcement checker will now announce errors it encounters in the first run to the admin room, plus a few other
misc improvements. Contributed by @Jade ([#1288](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1288))
- Drastically improved the performance and reliability of account deactivations. Contributed by @nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
- Refuse to process requests for and events in rooms that we no longer have any local users in (reduces state resets
and improves performance). Contributed by @nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
- Added server-specific admin API routes to ban and unban rooms, for use with moderation bots. Contributed by @nex
([#1301](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1301))
## Bugfixes
- Fix the generated configuration containing uncommented optional sections. Contributed by @Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
- Fixed specification non-compliance when handling remote media errors. Contributed by @nex ([#1298](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1298))
- UIAA requests which check for out-of-band success (sent by matrix-js-sdk) will no longer create unhelpful errors in
the logs. Contributed by @ginger ([#1305](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1305))
- Use exists instead of contains to save writing to a buffer in `src/service/users/mod.rs`: `is_login_disabled`.
Contributed
by @aprilgrimoire. ([#1340](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1340))
- Fixed backtraces being swallowed during panics. Contributed by @jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
- Fixed a potential vulnerability that could allow an evil remote server to return malicious events during the room join
and knock process. Contributed by @nex, reported by violet & [mat](https://matdoes.dev).
- Fixed a race condition that could result in outlier PDUs being incorrectly marked as visible to a remote server.
Contributed by @nex, reported by violet & [mat](https://matdoes.dev).
- ACLs are no longer case-sensitive. Contributed by @nex, reported by [vel](matrix:u/vel:nhjkl.com?action=chat).
## Docs
- Fixed Fedora install instructions. Contributed by @julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
# Continuwuity 0.5.3 (2026-01-12)
## Features
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by @Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
## Bugfixes
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by @nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
## Docs
- Improve admin command documentation generation. Contributed by @ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
## Misc
- Improve timeout-related code for federation and URL previews. Contributed by @Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
# Continuwuity 0.5.2 (2026-01-09)
## Features
- Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration tokens. Contributed by @ginger (#783).
- Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. (#1253)
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam. Contributed by @nex. (#1263)
- Implemented account locking functionality, to complement user suspension. Contributed by @nex. (#1266)
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex. (#1271)
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (#1272)
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs. Contributed by @nex. (#1277)
- Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server console. Contributed by @ginger.
- Added support for issuing additional registration tokens, stored in the database, which supplement the existing
registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or
after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is
superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration
tokens. Contributed by @ginger (#783).
- Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
Contributed by @nex. ([#1263](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1263))
- Implemented account locking functionality, to complement user suspension. Contributed by @nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex. ([#1271](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1271))
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (
[#1272](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1272))
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs.
Contributed by @nex. ([#1277](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1277))
- Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server
console. Contributed by @ginger.
## Bugfixes
- Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. (#1257)
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now also concurrent. Contributed by @nex. (#1261)
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by @nex. (#1276)
- Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now
also concurrent. Contributed by @nex. ([#1261](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1261))
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by
@nex. ([#1276](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1276))
## Misc
- The `console` feature is now enabled by default, allowing the server console to be used for running admin commands directly. To automatically open the console on startup, set the `admin_console_automatic` config option to `true`. Contributed by @ginger.
- The `console` feature is now enabled by default, allowing the server console to be used for running admin commands
directly. To automatically open the console on startup, set the `admin_console_automatic` config option to `true`.
Contributed by @ginger.
- We now (finally) document our container image mirrors. Contributed by @Jade
# Continuwuity 0.5.0 (2025-12-30)
**This release contains a CRITICAL vulnerability patch, and you must update as soon as possible**
## Features
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). (#1251)
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
## Bug Fixes
- Don't allow admin room upgrades, as this can break the admin room (@timedout) (#1245)
- Fix invalid creators in power levels during upgrade to v12 (@timedout) (#1245)
- Don't allow admin room upgrades, as this can break the admin room (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Fix invalid creators in power levels during upgrade to v12 (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))

1357
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.2"
version = "0.5.4"
[workspace.metadata.crane]
name = "conduwuit"
@@ -158,7 +158,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.10"
version = "0.0.17"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -342,7 +342,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
rev = "f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
rev = "458d52bdc7f9a07c497be94a1420ebd3d87d7b2b"
features = [
"compat",
"rand",

View File

@@ -57,9 +57,10 @@ ### What are the project's goals?
### 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!
There are currently no open registration Continuwuity instances available.
- https://continuwuity.rocks -- A public demo server operated by the Continuwuity Team.
- https://federated.nexus -- Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo.
### What are we working on?

View File

@@ -2,11 +2,7 @@
set -euo pipefail
# Path to Complement's source code
#
# The `COMPLEMENT_SRC` environment variable is set in the Nix dev shell, which
# points to a store path containing the Complement source code. It's likely you
# want to just pass that as the first argument to use it here.
# The root path where complement is available.
COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}"
# A `.jsonl` file to write test logs to
@@ -15,7 +11,10 @@ LOG_FILE="${2:-complement_test_logs.jsonl}"
# A `.jsonl` file to write test results to
RESULTS_FILE="${3:-complement_test_results.jsonl}"
COMPLEMENT_BASE_IMAGE="${COMPLEMENT_BASE_IMAGE:-complement-conduwuit:main}"
# The base docker image to use for complement tests
# You can build the default with `docker build -t continuwuity:complement -f ./docker/complement.Dockerfile .`
# after running `cargo build`. Only the debug binary is used.
COMPLEMENT_BASE_IMAGE="${COMPLEMENT_BASE_IMAGE:-continuwuity:complement}"
# Complement tests that are skipped due to flakiness/reliability issues or we don't implement such features and won't for a long time
SKIPPED_COMPLEMENT_TESTS='TestPartialStateJoin.*|TestRoomDeleteAlias/Parallel/Regular_users_can_add_and_delete_aliases_when_m.*|TestRoomDeleteAlias/Parallel/Can_delete_canonical_alias|TestUnbanViaInvite.*|TestRoomState/Parallel/GET_/publicRooms_lists.*"|TestRoomDeleteAlias/Parallel/Users_with_sufficient_power-level_can_delete_other.*'
@@ -34,25 +33,6 @@ toplevel="$(git rev-parse --show-toplevel)"
pushd "$toplevel" > /dev/null
if [ ! -f "complement_oci_image.tar.gz" ]; then
echo "building complement conduwuit image"
# if using macOS, use linux-complement
#bin/nix-build-and-cache just .#linux-complement
bin/nix-build-and-cache just .#complement
#nix build -L .#complement
echo "complement conduwuit image tar.gz built at \"result\""
echo "loading into docker"
docker load < result
popd > /dev/null
else
echo "skipping building a complement conduwuit image as complement_oci_image.tar.gz was already found, loading this"
docker load < complement_oci_image.tar.gz
popd > /dev/null
fi
echo ""
echo "running go test with:"
@@ -72,24 +52,16 @@ env \
set -o pipefail
# Post-process the results into an easy-to-compare format, sorted by Test name for reproducible results
cat "$LOG_FILE" | jq -s -c 'sort_by(.Test)[]' | jq -c '
jq -s -c 'sort_by(.Test)[]' < "$LOG_FILE" | jq -c '
select(
(.Action == "pass" or .Action == "fail" or .Action == "skip")
and .Test != null
) | {Action: .Action, Test: .Test}
' > "$RESULTS_FILE"
#if command -v gotestfmt &> /dev/null; then
# echo "using gotestfmt on $LOG_FILE"
# grep '{"Time":' "$LOG_FILE" | gotestfmt > "complement_test_logs_gotestfmt.log"
#fi
echo ""
echo ""
echo "complement logs saved at $LOG_FILE"
echo "complement results saved at $RESULTS_FILE"
#if command -v gotestfmt &> /dev/null; then
# echo "complement logs in gotestfmt pretty format outputted at complement_test_logs_gotestfmt.log (use an editor/terminal/pager that interprets ANSI colours and UTF-8 emojis)"
#fi
echo ""
echo ""

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
changelog.d/1349.feature Normal file
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

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -xe
# If we have no $SERVER_NAME set, abort
if [ -z "$SERVER_NAME" ]; then
echo "SERVER_NAME is not set, aborting"
exit 1
fi
# If /complement/ca/ca.crt or /complement/ca/ca.key are missing, abort
if [ ! -f /complement/ca/ca.crt ] || [ ! -f /complement/ca/ca.key ]; then
echo "/complement/ca/ca.crt or /complement/ca/ca.key is missing, aborting"
exit 1
fi
# Add the root cert to the local trust store
echo 'Installing Complement CA certificate to local trust store'
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/complement-ca.crt
update-ca-certificates
# Sign a certificate for our $SERVER_NAME
echo "Generating and signing certificate for $SERVER_NAME"
openssl genrsa -out "/$SERVER_NAME.key" 2048
echo "Generating CSR for $SERVER_NAME"
openssl req -new -sha256 \
-key "/$SERVER_NAME.key" \
-out "/$SERVER_NAME.csr" \
-subj "/C=US/ST=CA/O=Continuwuity, Inc./CN=$SERVER_NAME"\
-addext "subjectAltName=DNS:$SERVER_NAME"
openssl req -in "$SERVER_NAME.csr" -noout -text
echo "Signing certificate for $SERVER_NAME with Complement CA"
cat <<EOF > ./cert.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment, dataEncipherment, nonRepudiation
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.docker.internal
DNS.2 = hs1
DNS.3 = hs2
DNS.4 = hs3
DNS.5 = hs4
DNS.6 = $SERVER_NAME
IP.1 = 127.0.0.1
EOF
openssl x509 \
-req \
-in "/$SERVER_NAME.csr" \
-CA /complement/ca/ca.crt \
-CAkey /complement/ca/ca.key \
-CAcreateserial \
-out "/$SERVER_NAME.crt" \
-days 1 \
-sha256 \
-extfile ./cert.ext
# Tell continuwuity where to find the certs
export CONTINUWUITY_TLS__KEY="/$SERVER_NAME.key"
export CONTINUWUITY_TLS__CERTS="/$SERVER_NAME.crt"
# And who it is
export CONTINUWUITY_SERVER_NAME="$SERVER_NAME"
echo "Starting Continuwuity with SERVER_NAME=$SERVER_NAME"
# Start continuwuity
/usr/local/bin/conduwuit --config /etc/continuwuity/config.toml

View File

@@ -0,0 +1,53 @@
# ============================================= #
# Complement pre-filled configuration file #
#
# DANGER: THIS FILE FORCES INSECURE VALUES. #
# DO NOT USE OUTSIDE THE TEST SUITE ENV! #
# ============================================= #
[global]
address = "0.0.0.0"
allow_device_name_federation = true
allow_guest_registration = true
allow_public_room_directory_over_federation = true
allow_public_room_directory_without_auth = true
allow_registration = true
database_path = "/database"
log = "trace,h2=debug,hyper=debug"
port = [8008, 8448]
trusted_servers = []
only_query_trusted_key_servers = false
query_trusted_key_servers_first = false
query_trusted_key_servers_first_on_join = false
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
ip_range_denylist = []
url_preview_domain_contains_allowlist = ["*"]
url_preview_domain_explicit_denylist = ["*"]
media_compat_file_link = false
media_startup_check = true
prune_missing_media = true
log_colors = true
admin_room_notices = false
allow_check_for_updates = false
intentionally_unknown_config_option_for_testing = true
rocksdb_log_level = "info"
rocksdb_max_log_files = 1
rocksdb_recovery_mode = 0
rocksdb_paranoid_file_checks = true
log_guest_registrations = false
allow_legacy_media = true
startup_netburst = true
startup_netburst_keep = -1
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure = true
dns_timeout = 60
dns_attempts = 20
request_conn_timeout = 60
request_timeout = 120
well_known_conn_timeout = 60
well_known_timeout = 60
federation_idle_timeout = 300
sender_timeout = 300
sender_idle_timeout = 300
sender_retry_backoff_limit = 300
[global.tls]
dual_protocol = true

View File

@@ -1759,10 +1759,6 @@
#
#config_reload_signal = true
# This item is undocumented. Please contribute documentation for it.
#
#ldap = false
[global.tls]
# Path to a valid TLS certificate file.
@@ -1930,7 +1926,9 @@
#
#admin_filter = ""
[global.antispam.meowlnir]
#[global.antispam]
#[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
#
@@ -1955,7 +1953,7 @@
#
#check_all_joins = false
[global.antispam.draupnir]
#[global.antispam.draupnir]
# The base URL on which to contact Draupnir (before /api/).
#

View File

@@ -48,7 +48,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.16.6
ENV BINSTALL_VERSION=1.17.4
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree

View File

@@ -0,0 +1,11 @@
FROM ubuntu:latest
EXPOSE 8008
EXPOSE 8448
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
COPY docker/complement-entrypoint.sh /usr/local/bin/complement-entrypoint.sh
COPY docker/complement.config.toml /etc/continuwuity/config.toml
COPY target/debug/conduwuit /usr/local/bin/conduwuit
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
ENTRYPOINT ["/usr/local/bin/complement-entrypoint.sh"]

View File

@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.16.6
ENV BINSTALL_VERSION=1.17.4
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree

View File

@@ -34,6 +34,14 @@
"name": "troubleshooting",
"label": "Troubleshooting"
},
"security",
{
"type": "dir-section-header",
"name": "community",
"label": "Community",
"collapsible": true,
"collapsed": false
},
{
"type": "divider"
},
@@ -63,7 +71,5 @@
},
{
"type": "divider"
},
"community",
"security"
}
]

View File

@@ -19,16 +19,21 @@
{
"text": "Admin Command Reference",
"link": "/reference/admin/"
},
{
"text": "Server Reference",
"link": "/reference/server"
}
]
},
{
"text": "Community",
"link": "/community"
"items": [
{
"text": "Community Guidelines",
"link": "/community/guidelines"
},
{
"text": "Become a Partnered Homeserver!",
"link": "/community/ops-guidelines"
}
]
},
{
"text": "Security",

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

@@ -0,0 +1,12 @@
[
{
"type": "file",
"name": "guidelines",
"label": "Community Guidelines"
},
{
"type": "file",
"name": "ops-guidelines",
"label": "Partnered Homeserver Guidelines"
}
]

View File

@@ -0,0 +1,32 @@
# Partnered Homeserver Operator Requirements
> _So you want to be an officially sanctioned public Continuwuity homeserver operator?_
Thank you for your interest in the project! There's a few things we need from you first to make sure your homeserver meets our quality standards and that you are prepared to handle the additional workload introduced by operating a public chat service.
## Stuff you must have
if you don't do these things we will tell you to go away
- Your homeserver must be running an up-to-date version of Continuwuity
- You must have a CAPTCHA, external registration system, or apply-to-join system that provides one-time-use invite codes (we do not accept fully open nor static token registration)
- Your homeserver must have support details listed in [`/.well-known/matrix/support`](https://spec.matrix.org/v1.17/client-server-api/#getwell-knownmatrixsupport)
- Your rules and guidelines must align with [the project's own code of conduct](guidelines).
- You must be reasonably responsive (i.e. don't leave us hanging for a week if we alert you to an issue on your server)
- Your homeserver's community rooms (if any) must be protected by a moderation bot subscribed to policy lists like the Community Moderation Effort (you can get one from https://asgard.chat if you don't want to run your own)
## Stuff we encourage you to have
not strictly required but we will consider your request more strongly if you have it
- You should have automated moderation tooling that can automatically suspend abusive users on your homeserver who are added to policy lists
- You should have multiple server administrators (increased bus factor)
- You should have a terms of service and privacy policy prominently available
## Stuff you get
- Prominent listing in our README!
- A gold star sticker
- Access to a low noise room for more direct communication with maintainers and collaboration with fellow operators
- Read-only access to the continuwuity internal ban list
- Early notice of upcoming releases
## Sound good?
To get started, ping a team member in [our main chatroom](https://matrix.to/#/#continuwuity:continuwuity.org) and ask to be added to the list.

View File

@@ -1,17 +1,18 @@
# RPM Installation Guide
Continuwuity is available as RPM packages for Fedora, RHEL, and compatible distributions.
Continuwuity is available as RPM packages for Fedora and compatible distributions.
We do not currently have infrastructure to build RPMs for RHEL and compatible distributions, but this is a work in progress.
The RPM packaging files are maintained in the `fedora/` directory:
- `continuwuity.spec.rpkg` - RPM spec file using rpkg macros for building from git
- `continuwuity.service` - Systemd service file for the server
- `RPM-GPG-KEY-continuwuity.asc` - GPG public key for verifying signed packages
RPM packages built by CI are signed with our GPG key (Ed25519, ID: `5E0FF73F411AAFCA`).
RPM packages built by CI are signed with our GPG key (RSA, ID: `6595 E8DB 9191 D39A 46D6 A514 4BA7 F590 DF0B AA1D`). # spellchecker:disable-line
```bash
# Import the signing key
sudo rpm --import https://forgejo.ellis.link/continuwuation/continuwuity/raw/branch/main/fedora/RPM-GPG-KEY-continuwuity.asc
sudo rpm --import https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
# Verify a downloaded package
rpm --checksig continuwuity-*.rpm
@@ -23,7 +24,7 @@ ## Installation methods
```bash
# Add the repository and install
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable/continuwuation.repo
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable.repo
sudo dnf install continuwuity
```
@@ -31,7 +32,7 @@ # Add the repository and install
```bash
# Add the dev repository and install
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev/continuwuation.repo
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev.repo
sudo dnf install continuwuity
```
@@ -39,23 +40,10 @@ # Add the dev repository and install
```bash
# Branch names are sanitized (slashes become hyphens, lowercase only)
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature/continuwuation.repo
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature.repo
sudo dnf install continuwuity
```
**Direct installation** without adding repository
```bash
# Latest stable release
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable/continuwuity
# Latest development build
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev/continuwuity
# Specific feature branch
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/branch-name/continuwuity
```
**Manual repository configuration** (alternative method)
```bash
@@ -65,7 +53,7 @@ # Specific feature branch
baseurl=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable
enabled=1
gpgcheck=1
gpgkey=https://forgejo.ellis.link/continuwuation/continuwuity/raw/branch/main/fedora/RPM-GPG-KEY-continuwuity.asc
gpgkey=https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
EOF
sudo dnf install continuwuity

View File

@@ -19,6 +19,16 @@
src: /assets/logo.svg
alt: continuwuity logo
beforeFeatures:
- title: Matrix for Discord users
details: New to Matrix? Learn how Matrix compares to Discord
link: https://joinmatrix.org/guide/matrix-vs-discord/
buttonText: Find Out the Difference
- title: How Matrix Works
details: Learn how Matrix works under the hood, and what that means
link: https://matrix.org/docs/matrix-concepts/elements-of-matrix/
buttonText: Read the Guide
features:
- title: 🚀 High Performance
details: Built with Rust for exceptional speed and efficiency. Designed to run smoothly even on modest hardware.

View File

@@ -6,10 +6,10 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 7,
"mention_room": true,
"date": "2025-12-30",
"message": "Continuwuity v0.5.1 has been released. **The release contains a fix for the critical vulnerability [GHSA-m5p2-vccg-8c9v](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-m5p2-vccg-8c9v) (embargoed) affecting all Conduit-derived servers. Update as soon as possible.**\n\nThis has been *actively exploited* to attempt account takeover and forge events bricking the Continuwuity rooms. The new space is accessible at [Continuwuity (room list)](https://matrix.to/#/!8cR4g-i9ucof69E4JHNg9LbPVkGprHb3SzcrGBDDJgk?via=continuwuity.org&via=starstruck.systems&via=gingershaped.computer)\n"
"id": 9,
"mention_room": false,
"date": "2026-02-09",
"message": "Yesterday we released [v0.5.4](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.4). Bugfixes, performance improvements and more moderation features! There's also a security fix, so please update as soon as possible. Don't forget to join [our announcements channel](https://matrix.to/#/!jIdNjSM5X-V5JVx2h2kAhUZIIQ08GyzPL55NFZAH1vM/%2489TY9CqRg4-ff1MGo3Ulc5r5X4pakfdzT-99RD8Docc?via=ellis.link&via=explodie.org&via=matrix.org) to get important information sooner <3 "
}
]
}

View File

@@ -118,10 +118,6 @@ ## `!admin debug time`
Print the current time
## `!admin debug list-dependencies`
List dependencies
## `!admin debug database-stats`
Get database statistics

View File

@@ -112,6 +112,19 @@ ### `!admin query resolver overrides-cache`
Query the overrides cache
### `!admin query resolver flush-cache`
Flush a given server from the resolver caches or flush them completely
* Examples:
* Flush a specific server:
`!admin query resolver flush-cache matrix.example.com`
* Flush all resolver caches completely:
`!admin query resolver flush-cache --all`
## `!admin query pusher`
pusher service

View File

@@ -16,10 +16,6 @@ ## `!admin server reload-config`
Reload configuration values
## `!admin server list-features`
List the features built into the server
## `!admin server memory-usage`
Print database memory usage statistics

View File

@@ -20,6 +20,16 @@ ### Lost access to admin room
## General potential issues
### Configuration not working as expected
Sometimes you can make a mistake in your configuration that
means things don't get passed to Continuwuity correctly.
This is particularly easy to do with environment variables.
To check what configuration Continuwuity actually sees, you can
use the `!admin server show-config` command in your admin room.
Beware that this prints out any secrets in your configuration,
so you might want to delete the result afterwards!
### Potential DNS issues when using Docker
Docker's DNS setup for containers in a non-default network intercepts queries to
@@ -139,7 +149,7 @@ ### Database corruption
## Debugging
Note that users should not really be debugging things. If you find yourself
Note that users should not really need to debug things. If you find yourself
debugging and find the issue, please let us know and/or how we can fix it.
Various debug commands can be found in `!admin debug`.
@@ -178,6 +188,31 @@ ### Pinging servers
and simply fetches a string on a static JSON endpoint. It is very low cost both
bandwidth and computationally.
### Enabling backtraces for errors
Continuwuity can capture backtraces (stack traces) for errors to help diagnose
issues. Backtraces show the exact sequence of function calls that led to an
error, which is invaluable for debugging.
To enable backtraces, set the `RUST_BACKTRACE` environment variable before starting Continuwuity:
```bash
# For both panics and errors
RUST_BACKTRACE=1 ./conduwuit
```
For systemd deployments, add this to your service file:
```ini
[Service]
Environment="RUST_BACKTRACE=1"
```
Backtrace capture has a performance cost. Avoid leaving it on.
You can also enable it only for panics by setting
`RUST_BACKTRACE=1` and `RUST_LIB_BACKTRACE=0`.
### Allocator memory stats
When using jemalloc with jemallocator's `stats` feature (`--enable-stats`), you

1854
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,10 +22,9 @@
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@rspress/core": "^2.0.0-rc.1",
"@rspress/plugin-client-redirects": "^2.0.0-alpha.12",
"@rspress/plugin-preview": "^2.0.0-beta.35",
"@rspress/plugin-sitemap": "^2.0.0-beta.23",
"@rspress/core": "^2.0.0",
"@rspress/plugin-client-redirects": "^2.0.0",
"@rspress/plugin-sitemap": "^2.0.0",
"typescript": "^5.9.3"
}
}

View File

@@ -1 +1,8 @@
tag-message = "chore: Release v{{version}}"
tag-prefix = ""
shared-version = true
publish = false
sign-commit = true
sign-tag = true

View File

@@ -1,5 +1,4 @@
import { defineConfig } from '@rspress/core';
import { pluginPreview } from '@rspress/plugin-preview';
import { pluginSitemap } from '@rspress/plugin-sitemap';
import { pluginClientRedirects } from '@rspress/plugin-client-redirects';
@@ -41,7 +40,7 @@ export default defineConfig({
},
},
plugins: [pluginPreview(), pluginSitemap({
plugins: [pluginSitemap({
siteUrl: 'https://continuwuity.org', // TODO: Set automatically in build pipeline
}),
pluginClientRedirects({
@@ -54,6 +53,9 @@ export default defineConfig({
}, {
from: '/server_reference',
to: '/reference/server'
}, {
from: '/community$',
to: '/community/guidelines'
}
]
})],

View File

@@ -87,7 +87,6 @@ serde-saphyr.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
ctor.workspace = true
[lints]
workspace = true

View File

@@ -819,32 +819,6 @@ pub(super) async fn time(&self) -> Result {
self.write_str(&now).await
}
#[admin_command]
pub(super) async fn list_dependencies(&self, names: bool) -> Result {
if names {
let out = info::cargo::dependencies_names().join(" ");
return self.write_str(&out).await;
}
let mut out = String::new();
let deps = info::cargo::dependencies();
writeln!(out, "| name | version | features |")?;
writeln!(out, "| ---- | ------- | -------- |")?;
for (name, dep) in deps {
let version = dep.try_req().unwrap_or("*");
let feats = dep.req_features();
let feats = if !feats.is_empty() {
feats.join(" ")
} else {
String::new()
};
writeln!(out, "| {name} | {version} | {feats} |")?;
}
self.write_str(&out).await
}
#[admin_command]
pub(super) async fn database_stats(
&self,

View File

@@ -206,12 +206,6 @@ pub enum DebugCommand {
/// Print the current time
Time,
/// List dependencies
ListDependencies {
#[arg(short, long)]
names: bool,
},
/// Get database statistics
DatabaseStats {
property: Option<String>,

View File

@@ -30,11 +30,8 @@
pub(crate) const PAGE_SIZE: usize = 100;
use ctor::{ctor, dtor};
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
pub use crate::admin::AdminCommand;

View File

@@ -1,5 +1,5 @@
use clap::Subcommand;
use conduwuit::{Result, utils::time};
use conduwuit::{Err, Result, utils::time};
use futures::StreamExt;
use ruma::OwnedServerName;
@@ -7,6 +7,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
#[allow(clippy::enum_variant_names)]
/// Resolver service and caches
pub enum ResolverCommand {
/// Query the destinations cache
@@ -18,6 +19,14 @@ pub enum ResolverCommand {
OverridesCache {
name: Option<String>,
},
/// Flush a specific server from the resolver caches or everything
FlushCache {
name: Option<OwnedServerName>,
#[arg(short, long)]
all: bool,
},
}
#[admin_command]
@@ -69,3 +78,18 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
Ok(())
}
#[admin_command]
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
if all {
self.services.resolver.cache.clear().await;
writeln!(self, "Resolver caches cleared!").await
} else if let Some(name) = name {
self.services.resolver.cache.del_destination(&name);
self.services.resolver.cache.del_override(&name);
self.write_str(&format!("Cleared {name} from resolver caches!"))
.await
} else {
Err!("Missing name. Supply a name or use --all to flush the whole cache.")
}
}

View File

@@ -1,7 +1,7 @@
use std::{fmt::Write, path::PathBuf, sync::Arc};
use std::{path::PathBuf, sync::Arc};
use conduwuit::{
Err, Result, info,
Err, Result,
utils::{stream::IterStream, time},
warn,
};
@@ -59,34 +59,6 @@ pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
.await
}
#[admin_command]
pub(super) async fn list_features(&self, available: bool, enabled: bool, comma: bool) -> Result {
let delim = if comma { "," } else { " " };
if enabled && !available {
let features = info::rustc::features().join(delim);
let out = format!("`\n{features}\n`");
return self.write_str(&out).await;
}
if available && !enabled {
let features = info::cargo::features().join(delim);
let out = format!("`\n{features}\n`");
return self.write_str(&out).await;
}
let mut features = String::new();
let enabled = info::rustc::features();
let available = info::cargo::features();
for feature in available {
let active = enabled.contains(&feature.as_str());
let emoji = if active { "" } else { "" };
let remark = if active { "[enabled]" } else { "" };
writeln!(features, "{emoji} {feature} {remark}")?;
}
self.write_str(&features).await
}
#[admin_command]
pub(super) async fn memory_usage(&self) -> Result {
let services_usage = self.services.memory_usage().await?;

View File

@@ -21,18 +21,6 @@ pub enum ServerCommand {
path: Option<PathBuf>,
},
/// List the features built into the server
ListFeatures {
#[arg(short, long)]
available: bool,
#[arg(short, long)]
enabled: bool,
#[arg(short, long)]
comma: bool,
},
/// Print database memory usage statistics
MemoryUsage,

View File

@@ -3,10 +3,7 @@
fmt::Write as _,
};
use api::client::{
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room,
update_avatar_url, update_displayname,
};
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
use conduwuit::{
Err, Result, debug, debug_warn, error, info, is_equal_to,
matrix::{Event, pdu::PduBuilder},
@@ -227,9 +224,6 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await;
leave_all_rooms(self.services, &user_id).await;
}
self.write_str(&format!("User {user_id} has been deactivated"))
@@ -406,10 +400,6 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
.await;
leave_all_rooms(self.services, &user_id).await;
}
},
}

View File

@@ -91,7 +91,6 @@ serde.workspace = true
sha1.workspace = true
tokio.workspace = true
tracing.workspace = true
ctor.workspace = true
[lints]
workspace = true

1
src/api/admin/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod rooms;

132
src/api/admin/rooms/ban.rs Normal file
View File

@@ -0,0 +1,132 @@
use axum::extract::State;
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomAliasId, continuwuity_admin_api::rooms,
events::room::message::RoomMessageEventContent,
};
use crate::{Ruma, client::leave_room};
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
///
/// Bans or unbans a room.
pub(crate) async fn ban_room(
State(services): State<crate::State>,
body: Ruma<rooms::ban::v1::Request>,
) -> Result<rooms::ban::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if body.banned {
// Don't ban again if already banned
if services.rooms.metadata.is_banned(&body.room_id).await {
return Err!(Request(InvalidParam("Room is already banned")));
}
info!(%sender_user, "Banning room {}", body.room_id);
services
.admin
.notice(&format!("{sender_user} banned {} (ban in progress)", body.room_id))
.await;
let mut users = services
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| services.globals.user_is_local(user))
.boxed();
let mut evicted = Vec::new();
let mut failed_evicted = Vec::new();
while let Some(ref user_id) = users.next().await {
info!("Evicting user {} from room {}", user_id, body.room_id);
match leave_room(&services, user_id, &body.room_id, None)
.boxed()
.await
{
| Ok(()) => {
services.rooms.state_cache.forget(&body.room_id, user_id);
evicted.push(user_id.clone());
},
| Err(e) => {
warn!("Failed to evict user {} from room {}: {}", user_id, body.room_id, e);
failed_evicted.push(user_id.clone());
},
}
}
let aliases: Vec<OwnedRoomAliasId> = services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await;
for alias in &aliases {
info!("Removing alias {} for banned room {}", alias, body.room_id);
services
.rooms
.alias
.remove_alias(alias, &services.globals.server_user)
.await?;
}
services.rooms.directory.set_not_public(&body.room_id); // remove from the room directory
services.rooms.metadata.ban_room(&body.room_id, true); // prevent further joins
services.rooms.metadata.disable_room(&body.room_id, true); // disable federation
services
.admin
.notice(&format!(
"Finished banning {}: Removed {} users ({} failed) and {} aliases",
body.room_id,
evicted.len(),
failed_evicted.len(),
aliases.len()
))
.await;
if !evicted.is_empty() || !failed_evicted.is_empty() || !aliases.is_empty() {
let msg = services
.admin
.text_or_file(RoomMessageEventContent::text_markdown(format!(
"Removed users:\n{}\n\nFailed to remove users:\n{}\n\nRemoved aliases: {}",
evicted
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join("\n"),
failed_evicted
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join("\n"),
aliases
.iter()
.map(|a| a.as_str())
.collect::<Vec<_>>()
.join(", "),
)))
.await;
services.admin.send_message(msg).await.ok();
}
Ok(rooms::ban::v1::Response::new(evicted, failed_evicted, aliases))
} else {
// Don't unban if not banned
if !services.rooms.metadata.is_banned(&body.room_id).await {
return Err!(Request(InvalidParam("Room is not banned")));
}
info!(%sender_user, "Unbanning room {}", body.room_id);
services.rooms.metadata.disable_room(&body.room_id, false);
services.rooms.metadata.ban_room(&body.room_id, false);
services
.admin
.notice(&format!("{sender_user} unbanned {}", body.room_id))
.await;
Ok(rooms::ban::v1::Response::new(Vec::new(), Vec::new(), Vec::new()))
}
}

View File

@@ -0,0 +1,35 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
use crate::Ruma;
/// # `GET /_continuwuity/admin/rooms/list`
///
/// Lists all rooms known to this server, excluding banned ones.
pub(crate) async fn list_rooms(
State(services): State<crate::State>,
body: Ruma<rooms::list::v1::Request>,
) -> Result<rooms::list::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let mut rooms: Vec<OwnedRoomId> = services
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
if !services.rooms.metadata.is_banned(room_id).await {
Some(room_id.to_owned())
} else {
None
}
})
.collect()
.await;
rooms.sort();
Ok(rooms::list::v1::Response::new(rooms))
}

View File

@@ -0,0 +1,2 @@
pub mod ban;
pub mod list;

View File

@@ -26,6 +26,7 @@
events::{
GlobalAccountDataEventType, StateEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
@@ -815,9 +816,6 @@ pub(crate) async fn deactivate_route(
.collect()
.await;
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await;
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
full_user_deactivate(&services, sender_user, &all_joined_rooms)
.boxed()
.await?;
@@ -907,9 +905,6 @@ pub async fn full_user_deactivate(
) -> Result<()> {
services.users.deactivate_account(user_id).await.ok();
super::update_displayname(services, user_id, None, all_joined_rooms).await;
super::update_avatar_url(services, user_id, None, None, all_joined_rooms).await;
services
.users
.all_profile_keys(user_id)
@@ -918,9 +913,11 @@ pub async fn full_user_deactivate(
})
.await;
for room_id in all_joined_rooms {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
// TODO: Rescind all user invites
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
for room_id in all_joined_rooms {
let room_power_levels = services
.rooms
.state_accessor
@@ -948,30 +945,33 @@ pub async fn full_user_deactivate(
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
power_levels_content.users.remove(user_id);
// ignore errors so deactivation doesn't fail
match services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
user_id,
Some(room_id),
&state_lock,
)
.await
{
| Err(e) => {
warn!(%room_id, %user_id, "Failed to demote user's own power level: {e}");
},
| _ => {
info!("Demoted {user_id} in {room_id} as part of account deactivation");
},
}
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
pdu_queue.push((pl_evt, room_id));
}
// Leave the room
pdu_queue.push((
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
avatar_url: None,
blurhash: None,
membership: MembershipState::Leave,
displayname: None,
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
redact_events: None,
}),
room_id,
));
// TODO: Redact all messages sent by the user in the room
}
super::leave_all_rooms(services, user_id).boxed().await;
super::update_all_rooms(services, pdu_queue, user_id).await;
for room_id in all_joined_rooms {
services.rooms.state_cache.forget(room_id, user_id);
}
Ok(())
}

View File

@@ -91,8 +91,11 @@ pub(crate) async fn upload_keys_route(
.users
.get_device_keys(sender_user, sender_device)
.await
.and_then(|keys| keys.deserialize().map_err(Into::into))
{
if existing_keys.json().get() == device_keys.json().get() {
// NOTE: also serves as a workaround for a nheko bug which omits cross-signing
// NOTE: signatures when re-uploading the same DeviceKeys.
if existing_keys.keys == deser_device_keys.keys {
debug!(
%sender_user,
%sender_device,

View File

@@ -15,6 +15,7 @@
utils::{
self, shuffle,
stream::{IterStream, ReadyExt},
to_canonical_object,
},
warn,
};
@@ -1012,40 +1013,55 @@ async fn make_join_request(
trace!("make_join response: {:?}", make_join_response);
make_join_counter = make_join_counter.saturating_add(1);
if let Err(ref e) = make_join_response {
if matches!(
e.kind(),
ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
) {
incompatible_room_version_count =
incompatible_room_version_count.saturating_add(1);
}
match make_join_response {
| Ok(response) => {
info!("Received make_join response from {remote_server}");
if let Err(e) = validate_remote_member_event_stub(
&MembershipState::Join,
sender_user,
room_id,
&to_canonical_object(&response.event)?,
) {
warn!("make_join response from {remote_server} failed validation: {e}");
continue;
}
make_join_response_and_server = Ok((response, remote_server.clone()));
break;
},
| Err(e) => {
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!(
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support the \
room version {room_id}: {e}"
);
make_join_response_and_server =
Err!(BadServerResponse("Room version is not supported by Conduwuit"));
return make_join_response_and_server;
}
if incompatible_room_version_count > 15 {
info!(
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support \
the room version {room_id}: {e}"
);
make_join_response_and_server =
Err!(BadServerResponse("Room version is not supported by Conduwuit"));
return make_join_response_and_server;
}
if make_join_counter > 40 {
warn!(
"40 servers failed to provide valid make_join response, assuming no server \
can assist in joining."
);
make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
if make_join_counter > 40 {
warn!(
"40 servers failed to provide valid make_join response, assuming no \
server can assist in joining."
);
make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
return make_join_response_and_server;
}
return make_join_response_and_server;
}
},
}
make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));
if make_join_response_and_server.is_ok() {
break;
}

View File

@@ -10,7 +10,7 @@
},
result::FlatOk,
trace,
utils::{self, shuffle, stream::IterStream},
utils::{self, shuffle, stream::IterStream, to_canonical_object},
warn,
};
use futures::{FutureExt, StreamExt};
@@ -741,6 +741,17 @@ async fn make_knock_request(
trace!("make_knock response: {make_knock_response:?}");
make_knock_counter = make_knock_counter.saturating_add(1);
if let Ok(r) = &make_knock_response {
if let Err(e) = validate_remote_member_event_stub(
&MembershipState::Knock,
sender_user,
room_id,
&to_canonical_object(&r.event)?,
) {
warn!("make_knock response from {remote_server} failed validation: {e}");
continue;
}
}
make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone()));

View File

@@ -231,7 +231,7 @@ pub(crate) fn validate_remote_member_event_stub(
};
if event_membership != &membership.as_str() {
return Err!(BadServerResponse(
"Remote server returned member event with incorrect room_id"
"Remote server returned member event with incorrect membership type"
));
}

View File

@@ -371,11 +371,3 @@ pub(crate) async fn is_ignored_invite(
.invite_filter_level(&sender_user, recipient_user)
.await == FilterLevel::Ignore
}
#[cfg_attr(debug_assertions, ctor::ctor)]
fn _is_sorted() {
debug_assert!(
IGNORED_MESSAGE_TYPES.is_sorted(),
"IGNORED_MESSAGE_TYPES must be sorted by the developer"
);
}

View File

@@ -1,12 +1,13 @@
#![type_length_limit = "16384"] //TODO: reduce me
#![allow(clippy::toplevel_ref_arg)]
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service;
pub mod client;
pub mod router;
pub mod server;
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service;
pub mod admin;
pub(crate) use self::router::{Ruma, RumaResponse, State};

View File

@@ -17,7 +17,7 @@
use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse};
use crate::{client, server};
use crate::{admin, client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config;
@@ -187,7 +187,9 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
.ruma_route(&client::room_initial_sync_route)
.route("/client/server.json", get(client::syncv3_client_server_json));
.route("/client/server.json", get(client::syncv3_client_server_json))
.ruma_route(&admin::rooms::ban::ban_room)
.ruma_route(&admin::rooms::list::list_rooms);
if config.allow_federation {
router = router

View File

@@ -2,7 +2,7 @@
use axum::extract::State;
use conduwuit::{
Event, PduCount, Result,
Err, Event, PduCount, Result, info,
result::LogErr,
utils::{IterStream, ReadyExt, stream::TryTools},
};
@@ -34,6 +34,18 @@ pub(crate) async fn get_backfill_route(
}
.check()
.await?;
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve backfill for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let limit = body
.limit

View File

@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Result, err};
use conduwuit::{Err, Result, err, info};
use ruma::{MilliSecondsSinceUnixEpoch, RoomId, api::federation::event::get_event};
use super::AccessCheck;
@@ -38,6 +38,19 @@ pub(crate) async fn get_event_route(
.check()
.await?;
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
Ok(get_event::v1::Response {
origin: services.globals.server_name().to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),

View File

@@ -1,7 +1,7 @@
use std::{borrow::Borrow, iter::once};
use axum::extract::State;
use conduwuit::{Error, Result, utils::stream::ReadyExt};
use conduwuit::{Err, Error, Result, info, utils::stream::ReadyExt};
use futures::StreamExt;
use ruma::{
RoomId,
@@ -29,6 +29,19 @@ pub(crate) async fn get_event_authorization_route(
.check()
.await?;
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let event = services
.rooms
.timeline

View File

@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Result, debug, debug_error, utils::to_canonical_object};
use conduwuit::{Err, Result, debug, debug_error, info, utils::to_canonical_object};
use ruma::api::federation::event::get_missing_events;
use super::AccessCheck;
@@ -26,6 +26,19 @@ pub(crate) async fn get_missing_events_route(
.check()
.await?;
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let limit = body
.limit
.try_into()
@@ -66,12 +79,12 @@ pub(crate) async fn get_missing_events_route(
continue;
}
i = i.saturating_add(1);
let Ok(event) = to_canonical_object(&pdu) else {
debug_error!(
body.origin = body.origin.as_ref().map(tracing::field::display),
"Failed to convert PDU in database to canonical JSON: {pdu:?}"
);
i = i.saturating_add(1);
continue;
};

View File

@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Err, Result,
Err, Result, info,
utils::stream::{BroadbandExt, IterStream},
};
use conduwuit_service::rooms::spaces::{
@@ -23,6 +23,19 @@ pub(crate) async fn get_hierarchy_route(
return Err!(Request(NotFound("Room does not exist.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let room_id = &body.room_id;
let suggested_only = body.suggested_only;
let ref identifier = Identifier::ServerName(body.origin());

View File

@@ -30,6 +30,18 @@ pub(crate) async fn create_join_event_template_route(
if !services.rooms.metadata.exists(&body.room_id).await {
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve make_join for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if body.user_id.server_name() != body.origin() {
return Err!(Request(BadJson("Not allowed to join on behalf of another server/user.")));

View File

@@ -1,6 +1,6 @@
use RoomVersionId::*;
use axum::extract::State;
use conduwuit::{Err, Error, Result, debug_warn, matrix::pdu::PduBuilder, warn};
use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, warn};
use ruma::{
RoomVersionId,
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
@@ -20,6 +20,18 @@ pub(crate) async fn create_knock_event_template_route(
if !services.rooms.metadata.exists(&body.room_id).await {
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve make_knock for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if body.user_id.server_name() != body.origin() {
return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user.")));

View File

@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use conduwuit::{Err, Result, info, matrix::pdu::PduBuilder};
use ruma::{
api::federation::membership::prepare_leave_event,
events::room::member::{MembershipState, RoomMemberEventContent},
@@ -20,6 +20,19 @@ pub(crate) async fn create_leave_event_template_route(
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve make_leave for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if body.user_id.server_name() != body.origin() {
return Err!(Request(Forbidden(
"Not allowed to leave on behalf of another server/user."

View File

@@ -13,8 +13,7 @@
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
ServerName,
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName,
api::federation::membership::create_join_event,
events::{
StateEventType,
@@ -37,6 +36,18 @@ async fn create_join_event(
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
{
info!(
origin = origin.as_str(),
"Refusing to serve send_join for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
// ACL check origin server
services
@@ -178,15 +189,6 @@ async fn create_join_event(
}
}
let origin: OwnedServerName = serde_json::from_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
trace!("Signing send_join event");
services
.server_keys
@@ -204,7 +206,7 @@ async fn create_join_event(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value.clone(), true)
.handle_incoming_pdu(sender.server_name(), room_id, &event_id, value.clone(), true)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;

View File

@@ -1,12 +1,12 @@
use axum::extract::State;
use conduwuit::{
Err, Result, err,
Err, Result, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduEvent},
warn,
};
use futures::FutureExt;
use ruma::{
OwnedServerName, OwnedUserId,
OwnedUserId,
RoomVersionId::*,
api::federation::knock::send_knock,
events::{
@@ -54,6 +54,19 @@ pub(crate) async fn create_knock_event_v1_route(
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve send_knock for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
// ACL check origin server
services
.rooms
@@ -136,15 +149,6 @@ pub(crate) async fn create_knock_event_v1_route(
return Err!(Request(InvalidParam("state_key does not match sender user of event.")));
}
let origin: OwnedServerName = serde_json::from_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
let mut event: JsonObject = serde_json::from_str(body.pdu.get())
.map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?;
@@ -163,7 +167,7 @@ pub(crate) async fn create_knock_event_v1_route(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, &body.room_id, &event_id, value.clone(), true)
.handle_incoming_pdu(sender.server_name(), &body.room_id, &event_id, value.clone(), true)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;

View File

@@ -1,7 +1,7 @@
#![allow(deprecated)]
use axum::extract::State;
use conduwuit::{Err, Result, err, matrix::event::gen_event_id_canonical_json};
use conduwuit::{Err, Result, err, info, matrix::event::gen_event_id_canonical_json};
use conduwuit_service::Services;
use futures::FutureExt;
use ruma::{
@@ -50,6 +50,19 @@ async fn create_leave_event(
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
{
info!(
origin = origin.as_str(),
"Refusing to serve backfill for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
// ACL check origin
services
.rooms

View File

@@ -1,7 +1,7 @@
use std::{borrow::Borrow, iter::once};
use axum::extract::State;
use conduwuit::{Result, at, err, utils::IterStream};
use conduwuit::{Err, Result, at, err, info, utils::IterStream};
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{OwnedEventId, api::federation::event::get_room_state};
@@ -24,6 +24,19 @@ pub(crate) async fn get_room_state_route(
.check()
.await?;
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let shortstatehash = services
.rooms
.state_accessor

View File

@@ -1,7 +1,7 @@
use std::{borrow::Borrow, iter::once};
use axum::extract::State;
use conduwuit::{Result, at, err};
use conduwuit::{Err, Result, at, err, info};
use futures::{StreamExt, TryStreamExt};
use ruma::{OwnedEventId, api::federation::event::get_room_state_ids};
@@ -25,6 +25,19 @@ pub(crate) async fn get_room_state_ids_route(
.check()
.await?;
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
info!(
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let shortstatehash = services
.rooms
.state_accessor

View File

@@ -1,6 +1,6 @@
use conduwuit::{Err, Result, implement, is_false};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, future::OptionFuture, join};
use futures::{FutureExt, future::OptionFuture, join};
use ruma::{EventId, RoomId, ServerName};
pub(super) struct AccessCheck<'a> {
@@ -31,15 +31,6 @@ pub(super) async fn check(&self) -> Result {
.state_cache
.server_in_room(self.origin, self.room_id);
// if any user on our homeserver is trying to knock this room, we'll need to
// acknowledge bans or leaves
let user_is_knocking = self
.services
.rooms
.state_cache
.room_members_knocked(self.room_id)
.count();
let server_can_see: OptionFuture<_> = self
.event_id
.map(|event_id| {
@@ -51,14 +42,14 @@ pub(super) async fn check(&self) -> Result {
})
.into();
let (world_readable, server_in_room, server_can_see, acl_check, user_is_knocking) =
join!(world_readable, server_in_room, server_can_see, acl_check, user_is_knocking);
let (world_readable, server_in_room, server_can_see, acl_check) =
join!(world_readable, server_in_room, server_can_see, acl_check);
if !acl_check {
return Err!(Request(Forbidden("Server access denied.")));
}
if !world_readable && !server_in_room && user_is_knocking == 0 {
if !world_readable && !server_in_room {
return Err!(Request(Forbidden("Server is not in room.")));
}

View File

@@ -47,7 +47,7 @@
const NAME_MAX: usize = 128;
const KEY_SEGS: usize = 8;
#[crate::ctor]
#[ctor::ctor]
fn _static_initialization() {
acq_epoch().expect("pre-initialization of jemalloc failed");
acq_epoch().expect("pre-initialization of jemalloc failed");

View File

@@ -53,8 +53,7 @@
### For more information, see:
### https://continuwuity.org/configuration.html
"#,
ignore = "config_paths catchall well_known tls blurhashing \
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
ignore = "config_paths catchall"
)]
pub struct Config {
// Paths to config file(s). Not supposed to be set manually in the config file,
@@ -105,7 +104,7 @@ pub struct Config {
#[serde(default = "default_port")]
port: ListeningPort,
// external structure; separate section
/// display: nested
#[serde(default)]
pub tls: TlsConfig,
@@ -724,7 +723,7 @@ pub struct Config {
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
// external structure; separate section
/// display: nested
#[serde(default)]
pub well_known: WellKnownConfig,
@@ -2030,19 +2029,22 @@ pub struct Config {
/// etc. This is a hidden argument that should NOT be used in production as
/// it is highly insecure and I will personally yell at you if I catch you
/// using this.
///
/// display: hidden
#[serde(default)]
pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure:
bool,
// external structure; separate section
/// display: nested
#[serde(default)]
pub ldap: LdapConfig,
/// Configuration for antispam support
/// display: nested
#[serde(default)]
pub antispam: Option<Antispam>,
// external structure; separate section
/// display: nested
#[serde(default)]
pub blurhashing: BlurhashConfig,
#[serde(flatten)]
@@ -2259,15 +2261,23 @@ struct ListeningAddr {
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam",
optional = "true"
)]
pub struct Antispam {
/// display: nested
pub meowlnir: Option<MeowlnirConfig>,
/// display: nested
pub draupnir: Option<DraupnirConfig>,
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.meowlnir"
section = "global.antispam.meowlnir",
optional = "true"
)]
pub struct MeowlnirConfig {
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
@@ -2296,7 +2306,8 @@ pub struct MeowlnirConfig {
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.draupnir"
section = "global.antispam.draupnir",
optional = "true"
)]
pub struct DraupnirConfig {
/// The base URL on which to contact Draupnir (before /api/).

View File

@@ -62,7 +62,7 @@ macro_rules! debug_info {
pub static DEBUGGER: LazyLock<bool> =
LazyLock::new(|| env::var("_").unwrap_or_default().ends_with("gdb"));
#[cfg_attr(debug_assertions, crate::ctor)]
#[cfg_attr(debug_assertions, ctor::ctor)]
#[cfg_attr(not(debug_assertions), allow(dead_code))]
fn set_panic_trap() {
if !*DEBUGGER {

View File

@@ -115,7 +115,7 @@ macro_rules! err {
macro_rules! err_log {
($out:ident, $level:ident, $($fields:tt)+) => {{
use $crate::tracing::{
callsite, callsite2, metadata, valueset, Callsite,
callsite, callsite2, metadata, valueset_all, Callsite,
Level,
};
@@ -133,7 +133,7 @@ macro_rules! err_log {
fields: $($fields)+,
};
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset!(__CALLSITE.metadata().fields(), $($fields)+));
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset_all!(__CALLSITE.metadata().fields(), $($fields)+));
($out).into()
}}
}

View File

@@ -14,6 +14,25 @@
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let status = self.status_code();
if status.is_server_error() {
error!(
error = %self,
error_debug = ?self,
kind = ?self.kind(),
status = %status,
"Server error"
);
} else if status.is_client_error() {
use crate::debug_error;
debug_error!(
error = %self,
kind = ?self.kind(),
status = %status,
"Client error"
);
}
let response: UiaaResponse = self.into();
response
.try_into_http_response::<BytesMut>()

View File

@@ -1,95 +0,0 @@
//! Information about the build related to Cargo. This is a frontend interface
//! informed by proc-macros that capture raw information at build time which is
//! further processed at runtime either during static initialization or as
//! necessary.
use std::sync::OnceLock;
use cargo_toml::{DepsSet, Manifest};
use conduwuit_macros::cargo_manifest;
use crate::Result;
// Raw captures of the cargo manifest for each crate. This is provided by a
// proc-macro at build time since the source directory and the cargo toml's may
// not be present during execution.
#[cargo_manifest]
const WORKSPACE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "macros")]
const MACROS_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "core")]
const CORE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "database")]
const DATABASE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "service")]
const SERVICE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "admin")]
const ADMIN_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "router")]
const ROUTER_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "main")]
const MAIN_MANIFEST: &'static str = ();
/// Processed list of features across all project crates. This is generated from
/// the data in the MANIFEST strings and contains all possible project features.
/// For *enabled* features see the info::rustc module instead.
static FEATURES: OnceLock<Vec<String>> = OnceLock::new();
/// Processed list of dependencies. This is generated from the data captured in
/// the MANIFEST.
static DEPENDENCIES: OnceLock<DepsSet> = OnceLock::new();
#[must_use]
pub fn dependencies_names() -> Vec<&'static str> {
dependencies().keys().map(String::as_str).collect()
}
pub fn dependencies() -> &'static DepsSet {
DEPENDENCIES.get_or_init(|| {
init_dependencies().unwrap_or_else(|e| panic!("Failed to initialize dependencies: {e}"))
})
}
/// List of all possible features for the project. For *enabled* features in
/// this build see the companion function in info::rustc.
pub fn features() -> &'static Vec<String> {
FEATURES.get_or_init(|| {
init_features().unwrap_or_else(|e| panic!("Failed initialize features: {e}"))
})
}
fn init_features() -> Result<Vec<String>> {
let mut features = Vec::new();
append_features(&mut features, WORKSPACE_MANIFEST)?;
append_features(&mut features, MACROS_MANIFEST)?;
append_features(&mut features, CORE_MANIFEST)?;
append_features(&mut features, DATABASE_MANIFEST)?;
append_features(&mut features, SERVICE_MANIFEST)?;
append_features(&mut features, ADMIN_MANIFEST)?;
append_features(&mut features, ROUTER_MANIFEST)?;
append_features(&mut features, MAIN_MANIFEST)?;
features.sort();
features.dedup();
Ok(features)
}
fn append_features(features: &mut Vec<String>, manifest: &str) -> Result<()> {
let manifest = Manifest::from_str(manifest)?;
features.extend(manifest.features.keys().cloned());
Ok(())
}
fn init_dependencies() -> Result<DepsSet> {
let manifest = Manifest::from_str(WORKSPACE_MANIFEST)?;
let deps_set = manifest
.workspace
.as_ref()
.expect("manifest has workspace section")
.dependencies
.clone();
Ok(deps_set)
}

View File

@@ -1,12 +1,5 @@
//! Information about the project. This module contains version, build, system,
//! etc information which can be queried by admins or used by developers.
pub mod cargo;
pub mod room_version;
pub mod rustc;
pub mod version;
pub use conduwuit_macros::rustc_flags_capture;
pub const MODULE_ROOT: &str = const_str::split!(std::module_path!(), "::")[0];
pub const CRATE_PREFIX: &str = const_str::split!(MODULE_ROOT, '_')[0];

View File

@@ -1,54 +0,0 @@
//! Information about the build related to rustc. This is a frontend interface
//! informed by proc-macros at build time. Since the project is split into
//! several crates, lower-level information is supplied from each crate during
//! static initialization.
use std::{collections::BTreeMap, sync::OnceLock};
use crate::utils::exchange;
/// Raw capture of rustc flags used to build each crate in the project. Informed
/// by rustc_flags_capture macro (one in each crate's mod.rs). This is
/// done during static initialization which is why it's mutex-protected and pub.
/// Should not be written to by anything other than our macro.
///
/// We specifically use a std mutex here because parking_lot cannot be used
/// after thread local storage is destroyed on MacOS.
pub static FLAGS: std::sync::Mutex<BTreeMap<&str, &[&str]>> =
std::sync::Mutex::new(BTreeMap::new());
/// Processed list of enabled features across all project crates. This is
/// generated from the data in FLAGS.
static FEATURES: OnceLock<Vec<&'static str>> = OnceLock::new();
/// List of features enabled for the project.
pub fn features() -> &'static Vec<&'static str> { FEATURES.get_or_init(init_features) }
fn init_features() -> Vec<&'static str> {
let mut features = Vec::new();
FLAGS
.lock()
.expect("locked")
.iter()
.for_each(|(_, flags)| append_features(&mut features, flags));
features.sort_unstable();
features.dedup();
features
}
fn append_features(features: &mut Vec<&'static str>, flags: &[&'static str]) {
let mut next_is_cfg = false;
for flag in flags {
let is_cfg = *flag == "--cfg";
let is_feature = flag.starts_with("feature=");
if exchange(&mut next_is_cfg, is_cfg) && is_feature {
if let Some(feature) = flag
.split_once('=')
.map(|(_, feature)| feature.trim_matches('"'))
{
features.push(feature);
}
}
}
}

View File

@@ -22,7 +22,7 @@
pub use config::Config;
pub use error::Error;
pub use info::{
rustc_flags_capture, version,
version,
version::{name, version},
};
pub use matrix::{
@@ -30,12 +30,10 @@
};
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
pub use server::Server;
pub use utils::{ctor, dtor, implement, result, result::Result};
pub use utils::{implement, result, result::Result};
pub use crate as conduwuit_core;
rustc_flags_capture! {}
#[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))]
pub mod mods {
#[macro_export]

View File

@@ -22,7 +22,6 @@
pub mod with_lock;
pub use ::conduwuit_macros::implement;
pub use ::ctor::{ctor, dtor};
pub use self::{
arrayvec::ArrayVecExt,

View File

@@ -64,7 +64,6 @@ serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
ctor.workspace = true
[lints]
workspace = true

View File

@@ -3,11 +3,8 @@
extern crate conduwuit_core as conduwuit;
extern crate rust_rocksdb as rocksdb;
use ctor::{ctor, dtor};
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
#[cfg(test)]
mod benches;

View File

@@ -1,47 +0,0 @@
use std::{fs::read_to_string, path::PathBuf};
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{Error, ItemConst, Meta};
use crate::{Result, utils};
pub(super) fn manifest(item: ItemConst, args: &[Meta]) -> Result<TokenStream> {
let member = utils::get_named_string(args, "crate");
let path = manifest_path(member.as_deref())?;
let manifest = read_to_string(&path).unwrap_or_default();
let val = manifest.as_str();
let name = item.ident;
let ret = quote! {
const #name: &'static str = #val;
};
Ok(ret.into())
}
#[allow(clippy::option_env_unwrap)]
fn manifest_path(member: Option<&str>) -> Result<PathBuf> {
let Some(path) = option_env!("CARGO_MANIFEST_DIR") else {
return Err(Error::new(
Span::call_site().into(),
"missing CARGO_MANIFEST_DIR in environment",
));
};
let mut path: PathBuf = path.into();
// conduwuit/src/macros/ -> conduwuit/src/
path.pop();
if let Some(member) = member {
// conduwuit/$member/Cargo.toml
path.push(member);
} else {
// conduwuit/src/ -> conduwuit/
path.pop();
}
path.push("Cargo.toml");
Ok(path)
}

View File

@@ -73,11 +73,19 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
.expect("written to config file");
}
file.write_fmt(format_args!("\n[{section}]\n"))
let optional = settings.get("optional").is_some_and(|v| v == "true");
let section_header = if optional {
format!("\n#[{section}]\n")
} else {
format!("\n[{section}]\n")
};
file.write_fmt(format_args!("{section_header}"))
.expect("written to config file");
}
let mut summary: Vec<TokenStream2> = Vec::new();
let mut nested_displays: Vec<TokenStream2> = Vec::new();
if let Fields::Named(FieldsNamed { named, .. }) = &input.fields {
for field in named {
let Some(ident) = &field.ident else {
@@ -92,35 +100,6 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
continue;
};
let doc = get_doc_comment(field)
.unwrap_or_else(|| undocumented.into())
.trim_end()
.to_owned();
let doc = if doc.ends_with('#') {
format!("{doc}\n")
} else {
format!("{doc}\n#\n")
};
let default = get_doc_comment_line(field, "default")
.or_else(|| get_default(field))
.unwrap_or_default();
let default = if !default.is_empty() {
format!(" {default}")
} else {
default
};
if let Some(file) = file.as_mut() {
file.write_fmt(format_args!("\n{doc}"))
.expect("written to config file");
file.write_fmt(format_args!("#{ident} ={default}\n"))
.expect("written to config file");
}
let display = get_doc_comment_line(field, "display");
let display_directive = |key| {
display
@@ -129,17 +108,77 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
.flat_map(|display| display.split(' '))
.any(|directive| directive == key)
};
let is_nested = display_directive("nested");
let is_hidden = display_directive("hidden");
if !display_directive("hidden") {
let value = if display_directive("sensitive") {
quote! { "***********" }
// Only generate config file entries for non-nested, visible types
if !is_nested && !is_hidden {
let doc = get_doc_comment(field)
.unwrap_or_else(|| undocumented.into())
.trim_end()
.to_owned();
let doc = if doc.ends_with('#') {
format!("{doc}\n")
} else {
quote! { format_args!("{:?}", self.#ident) }
format!("{doc}\n#\n")
};
let name = ident.to_string();
let default = get_doc_comment_line(field, "default")
.or_else(|| get_default(field))
.unwrap_or_default();
let default = if !default.is_empty() {
format!(" {default}")
} else {
default
};
if let Some(file) = file.as_mut() {
file.write_fmt(format_args!("\n{doc}"))
.expect("written to config file");
file.write_fmt(format_args!("#{ident} ={default}\n"))
.expect("written to config file");
}
}
// Generate Display implementation for all fields
let name = ident.to_string();
if display_directive("sensitive") {
summary.push(quote! {
writeln!(out, "| {} | {} |", #name, #value)?;
writeln!(out, "| {} | {} |", #name, "***********")?;
});
} else if is_nested {
let is_option = matches!(type_name.as_str(), "Option");
if is_option {
summary.push(quote! {
writeln!(out, "| {} | {} |", #name,
if self.#ident.is_some() { "[configured]" } else { "None" })?;
});
nested_displays.push(quote! {
if let Some(nested) = &self.#ident {
writeln!(out)?;
writeln!(out, "## {}", #name)?;
write!(out, "{}", nested)?;
}
});
} else {
summary.push(quote! {
writeln!(out, "| {} | [configured] |", #name)?;
});
nested_displays.push(quote! {
writeln!(out)?;
writeln!(out, "## {}", #name)?;
write!(out, "{}", &self.#ident)?;
});
}
} else {
summary.push(quote! {
writeln!(out, "| {} | {:?} |", #name, self.#ident)?;
});
}
}
@@ -159,6 +198,7 @@ fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(out, "| name | value |")?;
writeln!(out, "| :--- | :--- |")?;
#( #summary )*
#( #nested_displays )*
Ok(())
}
}

View File

@@ -1,15 +1,13 @@
mod admin;
mod cargo;
mod config;
mod debug;
mod implement;
mod refutable;
mod rustc;
mod utils;
use proc_macro::TokenStream;
use syn::{
Error, Item, ItemConst, ItemEnum, ItemFn, ItemStruct, Meta,
Error, Item, ItemEnum, ItemFn, ItemStruct, Meta,
parse::{Parse, Parser},
parse_macro_input,
};
@@ -26,19 +24,11 @@ pub fn admin_command_dispatch(args: TokenStream, input: TokenStream) -> TokenStr
attribute_macro::<ItemEnum, _>(args, input, admin::command_dispatch)
}
#[proc_macro_attribute]
pub fn cargo_manifest(args: TokenStream, input: TokenStream) -> TokenStream {
attribute_macro::<ItemConst, _>(args, input, cargo::manifest)
}
#[proc_macro_attribute]
pub fn recursion_depth(args: TokenStream, input: TokenStream) -> TokenStream {
attribute_macro::<Item, _>(args, input, debug::recursion_depth)
}
#[proc_macro]
pub fn rustc_flags_capture(args: TokenStream) -> TokenStream { rustc::flags_capture(args) }
#[proc_macro_attribute]
pub fn refutable(args: TokenStream, input: TokenStream) -> TokenStream {
attribute_macro::<ItemFn, _>(args, input, refutable::refutable)

View File

@@ -1,29 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
pub(super) fn flags_capture(args: TokenStream) -> TokenStream {
let cargo_crate_name = std::env::var("CARGO_CRATE_NAME");
let crate_name = match cargo_crate_name.as_ref() {
| Err(_) => return args,
| Ok(crate_name) => crate_name.trim_start_matches("conduwuit_"),
};
let flag = std::env::args().collect::<Vec<_>>();
let flag_len = flag.len();
let ret = quote! {
pub static RUSTC_FLAGS: [&str; #flag_len] = [#( #flag ),*];
#[ctor]
fn _set_rustc_flags() {
conduwuit_core::info::rustc::FLAGS.lock().expect("locked").insert(#crate_name, &RUSTC_FLAGS);
}
// static strings have to be yanked on module unload
#[dtor]
fn _unset_rustc_flags() {
conduwuit_core::info::rustc::FLAGS.lock().expect("locked").remove(#crate_name);
}
};
ret.into()
}

View File

@@ -207,7 +207,6 @@ clap.workspace = true
console-subscriber.optional = true
console-subscriber.workspace = true
const-str.workspace = true
ctor.workspace = true
log.workspace = true
opentelemetry.optional = true
opentelemetry.workspace = true

View File

@@ -2,27 +2,26 @@
use std::sync::{Arc, atomic::Ordering};
use conduwuit_core::{debug_info, error, rustc_flags_capture};
use conduwuit_core::{debug_info, error};
mod clap;
mod logging;
mod mods;
mod panic;
mod restart;
mod runtime;
mod sentry;
mod server;
mod signal;
use ctor::{ctor, dtor};
use server::Server;
rustc_flags_capture! {}
pub use conduwuit_core::{Error, Result};
use server::Server;
pub use crate::clap::Args;
pub fn run() -> Result<()> {
panic::init();
let args = clap::parse();
run_with_args(&args)
}

34
src/main/panic.rs Normal file
View File

@@ -0,0 +1,34 @@
use std::{backtrace::Backtrace, panic};
/// Initialize the panic hook to capture backtraces at the point of panic.
/// This is needed to capture the backtrace before the unwind destroys it.
pub(crate) fn init() {
let default_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
let backtrace = Backtrace::force_capture();
let location_str = info.location().map_or_else(String::new, |loc| {
format!(" at {}:{}:{}", loc.file(), loc.line(), loc.column())
});
let message = if let Some(s) = info.payload().downcast_ref::<&str>() {
(*s).to_owned()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Box<dyn Any>".to_owned()
};
let thread_name = std::thread::current()
.name()
.map_or_else(|| "<unnamed>".to_owned(), ToOwned::to_owned);
eprintln!(
"\nthread '{thread_name}' panicked{location_str}: \
{message}\n\nBacktrace:\n{backtrace}"
);
default_hook(info);
}));
}

View File

@@ -122,7 +122,6 @@ tokio.workspace = true
tower.workspace = true
tower-http.workspace = true
tracing.workspace = true
ctor.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true

View File

@@ -12,12 +12,10 @@
use conduwuit::{Error, Result, Server};
use conduwuit_service::Services;
use ctor::{ctor, dtor};
use futures::{Future, FutureExt, TryFutureExt};
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
#[unsafe(no_mangle)]
pub extern "Rust" fn start(

View File

@@ -118,7 +118,6 @@ webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false }
ctor.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true

View File

@@ -406,6 +406,10 @@ pub async fn get_admins(&self) -> Vec<OwnedUserId> {
/// Checks whether a given user is an admin of this server
pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
if self.services.globals.server_user == user_id {
return true;
}
if self
.services
.server

View File

@@ -18,8 +18,9 @@
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{Result, Server, debug, info, warn};
use conduwuit::{Result, Server, debug, error, info, warn};
use database::{Deserialized, Map};
use rand::Rng;
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
use serde::Deserialize;
use tokio::{
@@ -86,9 +87,27 @@ async fn worker(self: Arc<Self>) -> Result<()> {
return Ok(());
}
// Run the first check immediately and send errors to admin room
if let Err(e) = self.check().await {
error!(?e, "Failed to check for announcements on startup");
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to check for announcements on startup: {e}"
)))
.await
.ok();
}
let first_check_jitter = {
let mut rng = rand::thread_rng();
let jitter_percent = rng.gen_range(-50.0..=10.0);
self.interval.mul_f64(1.0 + jitter_percent / 100.0)
};
let mut i = interval(self.interval);
i.set_missed_tick_behavior(MissedTickBehavior::Delay);
i.reset_after(self.interval);
i.reset_after(first_check_jitter);
loop {
tokio::select! {
() = self.interrupt.notified() => break,

View File

@@ -56,9 +56,18 @@ pub async fn fetch_remote_content(
let result = self
.fetch_content_authenticated(mxc, user, server, timeout_ms)
.await;
.await
.inspect_err(|error| {
debug_warn!(
%mxc,
?user,
?server,
?error,
"Authenticated fetch of remote content failed"
);
});
if let Err(Error::Request(NotFound, ..)) = &result {
if let Err(Error::Request(Unrecognized, ..)) = &result {
return self
.fetch_content_unauthenticated(mxc, user, server, timeout_ms)
.await;
@@ -87,7 +96,7 @@ async fn fetch_thumbnail_authenticated(
timeout_ms,
};
let Response { content, .. } = self.federation_request(mxc, user, server, request).await?;
let Response { content, .. } = self.federation_request(mxc, server, request).await?;
match content {
| FileOrLocation::File(content) =>
@@ -111,7 +120,7 @@ async fn fetch_content_authenticated(
timeout_ms,
};
let Response { content, .. } = self.federation_request(mxc, user, server, request).await?;
let Response { content, .. } = self.federation_request(mxc, server, request).await?;
match content {
| FileOrLocation::File(content) => self.handle_content_file(mxc, user, content).await,
@@ -145,7 +154,7 @@ async fn fetch_thumbnail_unauthenticated(
let Response {
file, content_type, content_disposition, ..
} = self.federation_request(mxc, user, server, request).await?;
} = self.federation_request(mxc, server, request).await?;
let content = Content { file, content_type, content_disposition };
@@ -173,7 +182,7 @@ async fn fetch_content_unauthenticated(
let Response {
file, content_type, content_disposition, ..
} = self.federation_request(mxc, user, server, request).await?;
} = self.federation_request(mxc, server, request).await?;
let content = Content { file, content_type, content_disposition };
@@ -296,7 +305,6 @@ async fn location_request(&self, location: &str) -> Result<FileMeta> {
async fn federation_request<Request>(
&self,
mxc: &Mxc<'_>,
user: Option<&UserId>,
server: Option<&ServerName>,
request: Request,
) -> Result<Request::IncomingResponse>
@@ -307,40 +315,6 @@ async fn federation_request<Request>(
.sending
.send_federation_request(server.unwrap_or(mxc.server_name), request)
.await
.map_err(|error| handle_federation_error(mxc, user, server, error))
}
// Handles and adjusts the error for the caller to determine if they should
// request the fallback endpoint or give up.
fn handle_federation_error(
mxc: &Mxc<'_>,
user: Option<&UserId>,
server: Option<&ServerName>,
error: Error,
) -> Error {
let fallback = || {
err!(Request(NotFound(
debug_error!(%mxc, user = user.map(tracing::field::display), server = server.map(tracing::field::display), ?error, "Remote media not found")
)))
};
// Matrix server responses for fallback always taken.
if error.kind() == NotFound || error.kind() == Unrecognized {
return fallback();
}
// If we get these from any middleware we'll try the other endpoint rather than
// giving up too early.
if error.status_code().is_redirection()
|| error.status_code().is_client_error()
|| error.status_code().is_server_error()
{
return fallback();
}
// Reached for 5xx errors. This is where we don't fallback given the likelihood
// the other endpoint will also be a 5xx and we're wasting time.
error
}
#[implement(super::Service)]

View File

@@ -1,7 +1,7 @@
use std::{cmp, collections::HashMap};
use std::{cmp, collections::HashMap, future::ready};
use conduwuit::{
Err, Pdu, Result, debug, debug_info, debug_warn, error, info,
Err, Event, Pdu, Result, debug, debug_info, debug_warn, error, info,
result::NotFound,
utils::{
IterStream, ReadyExt,
@@ -15,8 +15,9 @@
use ruma::{
OwnedRoomId, OwnedUserId, RoomId, UserId,
events::{
GlobalAccountDataEventType, StateEventType, push_rules::PushRulesEvent,
room::member::MembershipState,
AnyStrippedStateEvent, GlobalAccountDataEventType, StateEventType,
push_rules::PushRulesEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
push::Ruleset,
serde::Raw,
@@ -162,6 +163,14 @@ async fn migrate(services: &Services) -> Result<()> {
populate_userroomid_leftstate_table(services).await?;
}
if db["global"]
.get(FIXED_LOCAL_INVITE_STATE_MARKER)
.await
.is_not_found()
{
fix_local_invite_state(services).await?;
}
assert_eq!(
services.globals.db.database_version().await,
DATABASE_VERSION,
@@ -721,3 +730,46 @@ async fn populate_userroomid_leftstate_table(services: &Services) -> Result {
db.db.sort()?;
Ok(())
}
const FIXED_LOCAL_INVITE_STATE_MARKER: &str = "fix_local_invite_state";
async fn fix_local_invite_state(services: &Services) -> Result {
// Clean up the effects of !1249 by caching stripped state for invites
type KeyVal<'a> = (Key<'a>, Raw<Vec<AnyStrippedStateEvent>>);
type Key<'a> = (&'a UserId, &'a RoomId);
let db = &services.db;
let cork = db.cork_and_sync();
let userroomid_invitestate = services.db["userroomid_invitestate"].clone();
// for each user invited to a room
let fixed = userroomid_invitestate.stream()
// if they're a local user on this homeserver
.try_filter(|((user_id, _), _): &KeyVal<'_>| ready(services.globals.user_is_local(user_id)))
.and_then(async |((user_id, room_id), stripped_state): KeyVal<'_>| Ok::<_, conduwuit::Error>((user_id.to_owned(), room_id.to_owned(), stripped_state.deserialize()?)))
.try_fold(0_usize, async |mut fixed, (user_id, room_id, stripped_state)| {
// and their invite state is None
if stripped_state.is_empty()
// and they are actually invited to the room
&& let Ok(membership_event) = services.rooms.state_accessor.room_state_get(&room_id, &StateEventType::RoomMember, user_id.as_str()).await
&& membership_event.get_content::<RoomMemberEventContent>().is_ok_and(|content| content.membership == MembershipState::Invite)
// and the invite was sent by a local user
&& services.globals.user_is_local(&membership_event.sender) {
// build and save stripped state for their invite in the database
let stripped_state = services.rooms.state.summary_stripped(&membership_event, &room_id).await;
userroomid_invitestate.put((&user_id, &room_id), Json(stripped_state));
fixed = fixed.saturating_add(1);
}
Ok(fixed)
})
.await?;
drop(cork);
info!(?fixed, "Fixed local invite state cache entries.");
db["global"].insert(FIXED_LOCAL_INVITE_STATE_MARKER, []);
db.db.sort()?;
Ok(())
}

View File

@@ -34,11 +34,9 @@
pub mod uiaa;
pub mod users;
use ctor::{ctor, dtor};
pub(crate) use service::{Args, Dep, Service};
pub use crate::services::Services;
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}

View File

@@ -4,8 +4,8 @@
};
use conduwuit::{
Err, Event, Result, debug::INFO_SPAN_LEVEL, defer, err, implement, utils::stream::IterStream,
warn,
Err, Event, Result, debug::INFO_SPAN_LEVEL, defer, err, implement, info,
utils::stream::IterStream, warn,
};
use futures::{
FutureExt, TryFutureExt, TryStreamExt,
@@ -70,7 +70,7 @@ pub async fn handle_incoming_pdu<'a>(
return Err!(Request(TooLarge("PDU is too large")));
}
// 1.1 Check the server is in the room
// 1.1 Check we even know about the room
let meta_exists = self.services.metadata.exists(room_id).map(Ok);
// 1.2 Check if the room is disabled
@@ -114,6 +114,19 @@ pub async fn handle_incoming_pdu<'a>(
return Err!(Request(Forbidden("Federation of this room is disabled by this server.")));
}
if !self
.services
.state_cache
.server_in_room(self.services.globals.server_name(), room_id)
.await
{
info!(
%origin,
"Dropping inbound PDU for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
let (incoming_pdu, val) = self
.handle_outlier_pdu(origin, create_event, event_id, room_id, value, false)
.await?;

View File

@@ -1,7 +1,7 @@
use std::borrow::Borrow;
use conduwuit::{
Result, err, implement,
Pdu, Result, err, implement,
matrix::{Event, StateKey},
};
use futures::{Stream, StreamExt, TryFutureExt};
@@ -84,7 +84,7 @@ pub async fn room_state_get(
room_id: &RoomId,
event_type: &StateEventType,
state_key: &str,
) -> Result<impl Event> {
) -> Result<Pdu> {
self.services
.state
.get_room_shortstatehash(room_id)

View File

@@ -1,4 +1,4 @@
use conduwuit::{implement, utils::stream::ReadyExt};
use conduwuit::{implement, utils::stream::ReadyExt, warn};
use futures::StreamExt;
use ruma::{
EventId, RoomId, ServerName,
@@ -19,7 +19,12 @@ pub async fn server_can_see_event(
event_id: &EventId,
) -> bool {
let Ok(shortstatehash) = self.pdu_shortstatehash(event_id).await else {
return true;
warn!(
"Unable to visibility check event {} in room {} for server {}: shortstatehash not \
found",
event_id, room_id, origin
);
return false;
};
let history_visibility = self

View File

@@ -30,6 +30,7 @@ struct Services {
config: Dep<config::Service>,
globals: Dep<globals::Service>,
metadata: Dep<rooms::metadata::Service>,
state: Dep<rooms::state::Service>,
state_accessor: Dep<rooms::state_accessor::Service>,
users: Dep<users::Service>,
}
@@ -64,6 +65,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
config: args.depend::<config::Service>("config"),
globals: args.depend::<globals::Service>("globals"),
metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"),
state: args.depend::<rooms::state::Service>("rooms::state"),
state_accessor: args
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
users: args.depend::<users::Service>("users"),

View File

@@ -118,10 +118,8 @@ pub async fn update_membership(
self.mark_as_joined(user_id, room_id);
},
| MembershipState::Invite => {
// TODO: make sure that passing None for `last_state` is correct behavior.
// the call from `append_pdu` used to use `services.state.summary_stripped`
// to fill that parameter.
self.mark_as_invited(user_id, room_id, pdu.sender(), None, None)
let last_state = self.services.state.summary_stripped(pdu, room_id).await;
self.mark_as_invited(user_id, room_id, pdu.sender(), Some(last_state), None)
.await?;
},
| MembershipState::Leave | MembershipState::Ban => {

View File

@@ -233,6 +233,16 @@ pub async fn try_auth(
| AuthData::Dummy(_) => {
uiaainfo.completed.push(AuthType::Dummy);
},
| AuthData::FallbackAcknowledgement(_) => {
// The client is checking if authentication has succeeded out-of-band. This is
// possible if the client is using "fallback auth" (see spec section
// 4.9.1.4), which we don't support (and probably never will, because it's a
// disgusting hack).
// Return early to tell the client that no, authentication did not succeed while
// it wasn't looking.
return Ok((false, uiaainfo));
},
| k => error!("type not supported: {:?}", k),
}

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