mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-04-25 06:42:23 +00:00
Compare commits
28 Commits
v0.5.3
...
nex/fix/tp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07d35c6112 | ||
|
|
2e04ae947f | ||
|
|
c4c1481d78 | ||
|
|
da8b60b4ce | ||
|
|
89afaa94ac | ||
|
|
2b5563cee3 | ||
|
|
6cb9d50383 | ||
|
|
77c0f6e0c6 | ||
|
|
c85e710760 | ||
|
|
59346fc766 | ||
|
|
9c5e735888 | ||
|
|
fe74e82318 | ||
|
|
cb79a3b9d7 | ||
|
|
ebc8df1c4d | ||
|
|
b667a963cf | ||
|
|
5a6b909b37 | ||
|
|
dba9cf0ad2 | ||
|
|
287ddd9bc5 | ||
|
|
79a278b9e8 | ||
|
|
6c5d658ef2 | ||
|
|
70c43abca8 | ||
|
|
6a9b47c52e | ||
|
|
c042de96f8 | ||
|
|
7a6acd1c82 | ||
|
|
d260c4fcc2 | ||
|
|
fa15de9764 | ||
|
|
e6c7a4ae60 | ||
|
|
5bed4ad81d |
@@ -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
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ ## Docs
|
||||
|
||||
## Misc
|
||||
|
||||
- Improve timeout-related code for federation and URL previews. Contributed by @Jade
|
||||
- Improve timeout-related code for federation and URL previews. Contributed by @Jade (#1278)
|
||||
|
||||
|
||||
# Continuwuity 0.5.2 (2026-01-09)
|
||||
|
||||
941
Cargo.lock
generated
941
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -158,7 +158,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.10"
|
||||
version = "0.0.14"
|
||||
|
||||
# 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 = "85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
|
||||
1
changelog.d/1288.feature.md
Normal file
1
changelog.d/1288.feature.md
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
1
changelog.d/1290.bugfix.md
Normal file
1
changelog.d/1290.bugfix.md
Normal file
@@ -0,0 +1 @@
|
||||
Fix the generated configuration containing uncommented optional sections. Contributed by @Jade
|
||||
1
changelog.d/1298.bugfix
Normal file
1
changelog.d/1298.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fixed specification non-compliance when handling remote media errors. Contributed by @nex.
|
||||
1
changelog.d/1302.bugfix.md
Normal file
1
changelog.d/1302.bugfix.md
Normal file
@@ -0,0 +1 @@
|
||||
UIAA requests which check for out-of-band success (sent by matrix-js-sdk) will no longer create unhelpful errors in the logs.
|
||||
@@ -1926,9 +1926,9 @@
|
||||
#
|
||||
#admin_filter = ""
|
||||
|
||||
[global.antispam]
|
||||
#[global.antispam]
|
||||
|
||||
[global.antispam.meowlnir]
|
||||
#[global.antispam.meowlnir]
|
||||
|
||||
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||
#
|
||||
@@ -1953,7 +1953,7 @@
|
||||
#
|
||||
#check_all_joins = false
|
||||
|
||||
[global.antispam.draupnir]
|
||||
#[global.antispam.draupnir]
|
||||
|
||||
# The base URL on which to contact Draupnir (before /api/).
|
||||
#
|
||||
|
||||
@@ -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.16.7
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -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.16.7
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
12
docs/community/_meta.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"type": "file",
|
||||
"name": "guidelines",
|
||||
"label": "Community Guidelines"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "ops-guidelines",
|
||||
"label": "Partnered Homeserver Guidelines"
|
||||
}
|
||||
]
|
||||
32
docs/community/ops-guidelines.mdx
Normal file
32
docs/community/ops-guidelines.mdx
Normal 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.
|
||||
@@ -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": 8,
|
||||
"mention_room": false,
|
||||
"date": "2026-01-12",
|
||||
"message": "Hey everyone!\n\nJust letting you know we've released [v0.5.3](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.3) - this one is a bit of a hotfix for an issue with inviting and allowing others to join rooms.\n\nIf you appreceate the round-the-clock work we've been doing to keep your servers secure over this holiday period, we'd really appreciate your support - you can sponsor individuals on our team using the 'sponsor' button at the top of [our GitHub repository](https://github.com/continuwuity/continuwuity). If you can't do that, even a star helps - spreading the word and advocating for our project helps keep it going.\n\nHave a lovely rest of your year \\\n[Jade \\(she/her\\)](https://matrix.to/#/%40jade%3Aellis.link) \n🩵"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -118,10 +118,6 @@ ## `!admin debug time`
|
||||
|
||||
Print the current time
|
||||
|
||||
## `!admin debug list-dependencies`
|
||||
|
||||
List dependencies
|
||||
|
||||
## `!admin debug database-stats`
|
||||
|
||||
Get database statistics
|
||||
|
||||
@@ -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
|
||||
|
||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -713,9 +713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
||||
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3244,9 +3244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||||
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4285,13 +4285,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
|
||||
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.1"
|
||||
"@remix-run/router": "1.23.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -4301,14 +4301,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.30.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
|
||||
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.1",
|
||||
"react-router": "6.30.2"
|
||||
"@remix-run/router": "1.23.2",
|
||||
"react-router": "6.30.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
@@ -54,6 +54,9 @@ export default defineConfig({
|
||||
}, {
|
||||
from: '/server_reference',
|
||||
to: '/reference/server'
|
||||
}, {
|
||||
from: '/community$',
|
||||
to: '/community/guidelines'
|
||||
}
|
||||
]
|
||||
})],
|
||||
|
||||
@@ -87,7 +87,6 @@ serde-saphyr.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
ctor.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
1
src/api/admin/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod rooms;
|
||||
132
src/api/admin/rooms/ban.rs
Normal file
132
src/api/admin/rooms/ban.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
35
src/api/admin/rooms/list.rs
Normal file
35
src/api/admin/rooms/list.rs
Normal 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))
|
||||
}
|
||||
2
src/api/admin/rooms/mod.rs
Normal file
2
src/api/admin/rooms/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod ban;
|
||||
pub mod list;
|
||||
@@ -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,
|
||||
|
||||
@@ -657,7 +657,6 @@ async fn join_room_by_id_helper_remote(
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&state_res::RoomVersion::new(&room_version_id)?,
|
||||
&parsed_join_pdu,
|
||||
None, // TODO: third party invite
|
||||
|k, s| state_fetch(k.clone(), s.into()),
|
||||
&state_fetch(StateEventType::RoomCreate, "".into())
|
||||
.await
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
OwnedServerName, OwnedUserId,
|
||||
OwnedUserId,
|
||||
RoomVersionId::*,
|
||||
api::federation::knock::send_knock,
|
||||
events::{
|
||||
|
||||
@@ -10,31 +10,31 @@ version.workspace = true
|
||||
[lib]
|
||||
path = "mod.rs"
|
||||
crate-type = [
|
||||
"rlib",
|
||||
# "dylib",
|
||||
"rlib",
|
||||
# "dylib",
|
||||
]
|
||||
|
||||
[features]
|
||||
brotli_compression = [
|
||||
"reqwest/brotli",
|
||||
"reqwest/brotli",
|
||||
]
|
||||
conduwuit_mods = [
|
||||
"dep:libloading"
|
||||
]
|
||||
gzip_compression = [
|
||||
"reqwest/gzip",
|
||||
"reqwest/gzip",
|
||||
]
|
||||
hardened_malloc = [
|
||||
"dep:hardened_malloc-rs"
|
||||
"dep:hardened_malloc-rs"
|
||||
]
|
||||
jemalloc = [
|
||||
"dep:tikv-jemalloc-sys",
|
||||
"dep:tikv-jemalloc-ctl",
|
||||
"dep:tikv-jemallocator",
|
||||
"dep:tikv-jemalloc-sys",
|
||||
"dep:tikv-jemalloc-ctl",
|
||||
"dep:tikv-jemallocator",
|
||||
]
|
||||
jemalloc_conf = []
|
||||
jemalloc_prof = [
|
||||
"tikv-jemalloc-sys/profiling",
|
||||
"tikv-jemalloc-sys/profiling",
|
||||
]
|
||||
jemalloc_stats = [
|
||||
"tikv-jemalloc-sys/stats",
|
||||
@@ -43,10 +43,10 @@ jemalloc_stats = [
|
||||
]
|
||||
perf_measurements = []
|
||||
release_max_log_level = [
|
||||
"tracing/max_level_trace",
|
||||
"tracing/release_max_level_info",
|
||||
"log/max_level_trace",
|
||||
"log/release_max_level_info",
|
||||
"tracing/max_level_trace",
|
||||
"tracing/release_max_level_info",
|
||||
"log/max_level_trace",
|
||||
"log/release_max_level_info",
|
||||
]
|
||||
sentry_telemetry = []
|
||||
zstd_compression = [
|
||||
@@ -110,6 +110,7 @@ tracing.workspace = true
|
||||
url.workspace = true
|
||||
parking_lot.workspace = true
|
||||
lock_api.workspace = true
|
||||
ed25519-dalek = "~2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -2261,7 +2261,11 @@ struct ListeningAddr {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.antispam")]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.antispam",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct Antispam {
|
||||
/// display: nested
|
||||
pub meowlnir: Option<MeowlnirConfig>,
|
||||
@@ -2272,7 +2276,8 @@ pub struct Antispam {
|
||||
#[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).
|
||||
@@ -2301,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/).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,36 @@
|
||||
use std::{borrow::Borrow, collections::BTreeSet};
|
||||
|
||||
use ed25519_dalek::{Verifier, VerifyingKey};
|
||||
use futures::{
|
||||
Future,
|
||||
future::{OptionFuture, join, join3},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ruma::{
|
||||
Int, OwnedUserId, RoomVersionId, UserId,
|
||||
CanonicalJsonObject, Int, OwnedUserId, RoomVersionId, UserId,
|
||||
canonical_json::to_canonical_value,
|
||||
events::room::{
|
||||
create::RoomCreateEventContent,
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, ThirdPartyInvite},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
third_party_invite::RoomThirdPartyInviteEventContent,
|
||||
third_party_invite::{PublicKey, RoomThirdPartyInviteEventContent},
|
||||
},
|
||||
int,
|
||||
serde::{Base64, Raw},
|
||||
serde::{
|
||||
Base64, Base64DecodeError, Raw,
|
||||
base64::{Standard, UrlSafe},
|
||||
},
|
||||
signatures::{ParseError, VerificationError},
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
de::{Error as _, IgnoredAny},
|
||||
};
|
||||
use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue};
|
||||
use serde_json::{
|
||||
from_str as from_json_str, to_value,
|
||||
value::{RawValue as RawJsonValue, to_raw_value},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Error, Event, Result, StateEventType, StateKey, TimelineEventType,
|
||||
@@ -30,7 +40,7 @@
|
||||
},
|
||||
room_version::RoomVersion,
|
||||
};
|
||||
use crate::{debug, error, trace, warn};
|
||||
use crate::{debug, error, trace, utils::to_canonical_object, warn};
|
||||
|
||||
// FIXME: field extracting could be bundled for `content`
|
||||
#[derive(Deserialize)]
|
||||
@@ -157,15 +167,14 @@ struct RoomMemberContentFields {
|
||||
pub async fn auth_check<E, F, Fut>(
|
||||
room_version: &RoomVersion,
|
||||
incoming_event: &E,
|
||||
current_third_party_invite: Option<&E>,
|
||||
fetch_state: F,
|
||||
create_event: &E,
|
||||
) -> Result<bool, Error>
|
||||
where
|
||||
F: Fn(&StateEventType, &str) -> Fut + Send,
|
||||
F: Fn(&StateEventType, &str) -> Fut + Send + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
for<'a> &'a E: Event + Send,
|
||||
for<'a> &'a E: Event + Send + Sync,
|
||||
{
|
||||
debug!(
|
||||
event_id = %incoming_event.event_id(),
|
||||
@@ -415,13 +424,15 @@ pub async fn auth_check<E, F, Fut>(
|
||||
sender,
|
||||
sender_member_event.as_ref(),
|
||||
incoming_event,
|
||||
current_third_party_invite,
|
||||
power_levels_event.as_ref(),
|
||||
join_rules_event.as_ref(),
|
||||
user_for_join_auth.as_deref(),
|
||||
&user_for_join_auth_membership,
|
||||
&room_create_event,
|
||||
)? {
|
||||
&fetch_state,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -658,23 +669,25 @@ fn is_creator<EV>(
|
||||
/// event and the current State.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn valid_membership_change<E>(
|
||||
async fn valid_membership_change<F, Fut, E>(
|
||||
room_version: &RoomVersion,
|
||||
target_user: &UserId,
|
||||
target_user_membership_event: Option<&E>,
|
||||
sender: &UserId,
|
||||
sender_membership_event: Option<&E>,
|
||||
current_event: &E,
|
||||
current_third_party_invite: Option<&E>,
|
||||
power_levels_event: Option<&E>,
|
||||
join_rules_event: Option<&E>,
|
||||
user_for_join_auth: Option<&UserId>,
|
||||
user_for_join_auth_membership: &MembershipState,
|
||||
create_room: &E,
|
||||
fetch_state: &F,
|
||||
) -> Result<bool>
|
||||
where
|
||||
F: Fn(&StateEventType, &str) -> Fut + Send + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
for<'a> &'a E: Event + Send,
|
||||
for<'a> &'a E: Event + Send + Sync,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct GetThirdPartyInvite {
|
||||
@@ -950,68 +963,62 @@ struct GetThirdPartyInvite {
|
||||
| MembershipState::Invite => {
|
||||
// If content has third_party_invite key
|
||||
trace!("starting target_membership=invite check");
|
||||
match third_party_invite.and_then(|i| i.deserialize().ok()) {
|
||||
| Some(tp_id) =>
|
||||
if target_user_current_membership == MembershipState::Ban {
|
||||
warn!(?target_user_membership_event_id, "Can't invite banned user");
|
||||
false
|
||||
} else {
|
||||
let allow = verify_third_party_invite(
|
||||
Some(target_user),
|
||||
sender,
|
||||
&tp_id,
|
||||
current_third_party_invite,
|
||||
);
|
||||
if !allow {
|
||||
warn!("Third party invite invalid");
|
||||
}
|
||||
allow
|
||||
},
|
||||
| _ =>
|
||||
if !sender_is_joined {
|
||||
warn!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
?sender_membership,
|
||||
"sender cannot produce an invite without being joined to the room",
|
||||
);
|
||||
false
|
||||
} else if matches!(
|
||||
target_user_current_membership,
|
||||
MembershipState::Join | MembershipState::Ban
|
||||
) {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
"cannot invite a user who is banned or already joined",
|
||||
);
|
||||
false
|
||||
} else {
|
||||
let allow = sender_creator
|
||||
|| sender_power
|
||||
.filter(|&p| p >= &power_levels.invite)
|
||||
.is_some();
|
||||
if !allow {
|
||||
warn!(
|
||||
%sender,
|
||||
has=?sender_power,
|
||||
required=?power_levels.invite,
|
||||
"sender does not have enough power to produce invites",
|
||||
);
|
||||
}
|
||||
trace!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
?sender_membership,
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
sender_pl=?sender_power,
|
||||
required_pl=?power_levels.invite,
|
||||
"allowing invite"
|
||||
);
|
||||
allow
|
||||
},
|
||||
if let Some(third_party_invite) = third_party_invite {
|
||||
let allow = verify_third_party_invite(
|
||||
target_user_current_membership,
|
||||
&serde_json::to_value(third_party_invite)?,
|
||||
target_user,
|
||||
current_event,
|
||||
fetch_state,
|
||||
)
|
||||
.await;
|
||||
if !allow {
|
||||
warn!("Third party invite invalid");
|
||||
}
|
||||
return Ok(allow);
|
||||
}
|
||||
if !sender_is_joined {
|
||||
warn!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
?sender_membership,
|
||||
"sender cannot produce an invite without being joined to the room",
|
||||
);
|
||||
return Ok(false);
|
||||
} else if matches!(
|
||||
target_user_current_membership,
|
||||
MembershipState::Join | MembershipState::Ban
|
||||
) {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
"cannot invite a user who is banned or already joined",
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
let allow = sender_creator
|
||||
|| sender_power
|
||||
.filter(|&p| p >= &power_levels.invite)
|
||||
.is_some();
|
||||
if !allow {
|
||||
warn!(
|
||||
%sender,
|
||||
has=?sender_power,
|
||||
required=?power_levels.invite,
|
||||
"sender does not have enough power to produce invites",
|
||||
);
|
||||
}
|
||||
trace!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
?sender_membership,
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
sender_pl=?sender_power,
|
||||
required_pl=?power_levels.invite,
|
||||
"allowing invite"
|
||||
);
|
||||
return Ok(allow);
|
||||
},
|
||||
| MembershipState::Leave => {
|
||||
let can_unban = if target_user_current_membership == MembershipState::Ban {
|
||||
@@ -1499,399 +1506,187 @@ fn get_send_level(
|
||||
.unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) })
|
||||
}
|
||||
|
||||
fn verify_third_party_invite(
|
||||
target_user: Option<&UserId>,
|
||||
sender: &UserId,
|
||||
tp_id: &ThirdPartyInvite,
|
||||
current_third_party_invite: Option<&impl Event>,
|
||||
) -> bool {
|
||||
// 1. Check for user being banned happens before this is called
|
||||
// checking for mxid and token keys is done by ruma when deserializing
|
||||
fn verify_payload(pk: &[u8], sig: &[u8], c: &[u8]) -> Result<(), ruma::signatures::Error> {
|
||||
VerifyingKey::from_bytes(
|
||||
pk.try_into()
|
||||
.map_err(|_| ParseError::PublicKey(ed25519_dalek::SignatureError::new()))?,
|
||||
)
|
||||
.map_err(ParseError::PublicKey)?
|
||||
.verify(c, &sig.try_into().map_err(ParseError::Signature)?)
|
||||
.map_err(VerificationError::Signature)
|
||||
.map_err(ruma::signatures::Error::from)
|
||||
}
|
||||
|
||||
// The state key must match the invitee
|
||||
if target_user != Some(&tp_id.signed.mxid) {
|
||||
/// Decodes a base64 string as either URL-safe or standard base64, as per the
|
||||
/// spec. It attempts to decode urlsafe first.
|
||||
fn decode_base64(content: &str) -> Result<Vec<u8>, Base64DecodeError> {
|
||||
if let Ok(decoded) = Base64::<UrlSafe>::parse(content) {
|
||||
Ok(decoded.as_bytes().to_vec())
|
||||
} else {
|
||||
Base64::<Standard>::parse(content).map(|v| v.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_public_keys(event: &CanonicalJsonObject) -> Vec<Vec<u8>> {
|
||||
let mut public_keys = Vec::new();
|
||||
if let Some(public_key) = event.get("public_key").and_then(|v| v.as_str()) {
|
||||
if let Ok(v) = decode_base64(public_key) {
|
||||
trace!(
|
||||
encoded = public_key,
|
||||
decoded = ?v,
|
||||
"found public key in public_key property of m.room.third_party_invite event",
|
||||
);
|
||||
public_keys.push(v);
|
||||
} else {
|
||||
warn!("m.room.third_party_invite event has invalid public_key");
|
||||
}
|
||||
}
|
||||
if let Some(keys) = event.get("public_keys").and_then(|v| v.as_array()) {
|
||||
for key in keys {
|
||||
if let Some(key_obj) = key.as_object() {
|
||||
if let Some(public_key) = key_obj.get("public_key").and_then(|v| v.as_str()) {
|
||||
if let Ok(v) = decode_base64(public_key) {
|
||||
trace!(
|
||||
encoded = public_key,
|
||||
decoded = ?v,
|
||||
"found public key in public_keys list of m.room.third_party_invite \
|
||||
event",
|
||||
);
|
||||
public_keys.push(v);
|
||||
} else {
|
||||
warn!(
|
||||
"m.room.third_party_invite event has invalid public_key in \
|
||||
public_keys list"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"m.room.third_party_invite event has entry in public_keys list missing \
|
||||
public_key property"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"m.room.third_party_invite event has invalid entry in public_keys list, \
|
||||
expected object"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
public_keys
|
||||
}
|
||||
|
||||
/// Checks a third-party invite is valid.
|
||||
async fn verify_third_party_invite<F, Fut, E>(
|
||||
target_current_membership: MembershipState,
|
||||
raw_third_party_invite: &serde_json::Value,
|
||||
target: &UserId,
|
||||
event: &E,
|
||||
fetch_state: &F,
|
||||
) -> bool
|
||||
where
|
||||
F: Fn(&StateEventType, &str) -> Fut + Send + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
for<'a> &'a E: Event + Send + Sync,
|
||||
{
|
||||
// 4.1.1: If target user is banned, reject.
|
||||
if target_current_membership == MembershipState::Ban {
|
||||
warn!("invite target is banned");
|
||||
return false;
|
||||
}
|
||||
// 4.1.2: If content.third_party_invite does not have a signed property, reject.
|
||||
let Some(signed) = raw_third_party_invite.get("signed") else {
|
||||
warn!("invite event third_party_invite missing signed property");
|
||||
return false;
|
||||
};
|
||||
// 4.2.3: If signed does not have mxid and token properties, reject.
|
||||
let Some(mxid) = signed.get("mxid").and_then(|v| v.as_str()) else {
|
||||
warn!("invite event third_party_invite signed missing/invalid mxid property");
|
||||
return false;
|
||||
};
|
||||
let Some(token) = signed.get("token").and_then(|v| v.as_str()) else {
|
||||
warn!("invite event third_party_invite signed missing token property");
|
||||
return false;
|
||||
};
|
||||
// 4.2.4: If mxid does not match state_key, reject.
|
||||
if mxid != target.as_str() {
|
||||
warn!("invite event third_party_invite signed mxid does not match state_key");
|
||||
return false;
|
||||
}
|
||||
// 4.2.5: If there is no m.room.third_party_invite event in the room
|
||||
// state matching the token, reject.
|
||||
let Some(third_party_invite_event) =
|
||||
fetch_state(&StateEventType::RoomThirdPartyInvite, token).await
|
||||
else {
|
||||
warn!("invite event third_party_invite token has no matching m.room.third_party_invite");
|
||||
return false;
|
||||
};
|
||||
// 4.2.6: If sender does not match sender of the m.room.third_party_invite,
|
||||
// reject.
|
||||
if third_party_invite_event.sender() != event.sender() {
|
||||
warn!("invite event sender does not match m.room.third_party_invite sender");
|
||||
return false;
|
||||
}
|
||||
// 4.2.7: If any signature in signed matches any public key in the
|
||||
// m.room.third_party_invite event, allow. The public keys are in
|
||||
// content of m.room.third_party_invite as:
|
||||
// 1. A single public key in the public_key property.
|
||||
// 2. A list of public keys in the public_keys property.
|
||||
debug!(
|
||||
"Fetching signatures in third-party-invite event {}",
|
||||
third_party_invite_event.event_id()
|
||||
);
|
||||
trace!("third-party-invite event content: {}", third_party_invite_event.content().get());
|
||||
|
||||
// If there is no m.room.third_party_invite event in the current room state with
|
||||
// state_key matching token, reject
|
||||
#[allow(clippy::manual_let_else)]
|
||||
let current_tpid = match current_third_party_invite {
|
||||
| Some(id) => id,
|
||||
| None => return false,
|
||||
let Some(signatures) = signed.get("signatures").and_then(|v| v.as_object()) else {
|
||||
warn!("invite event third_party_invite signed missing/invalid signatures");
|
||||
return false;
|
||||
};
|
||||
|
||||
if current_tpid.state_key() != Some(&tp_id.signed.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if sender != current_tpid.sender() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any signature in signed matches any public key in the
|
||||
// m.room.third_party_invite event, allow
|
||||
#[allow(clippy::manual_let_else)]
|
||||
let tpid_ev =
|
||||
match from_json_str::<RoomThirdPartyInviteEventContent>(current_tpid.content().get()) {
|
||||
| Ok(ev) => ev,
|
||||
| Err(_) => return false,
|
||||
};
|
||||
|
||||
#[allow(clippy::manual_let_else)]
|
||||
let decoded_invite_token = match Base64::parse(&tp_id.signed.token) {
|
||||
| Ok(tok) => tok,
|
||||
// FIXME: Log a warning?
|
||||
| Err(_) => return false,
|
||||
};
|
||||
|
||||
// A list of public keys in the public_keys field
|
||||
for key in tpid_ev.public_keys.unwrap_or_default() {
|
||||
if key.public_key == decoded_invite_token {
|
||||
return true;
|
||||
for pk in get_public_keys(
|
||||
&to_canonical_object(third_party_invite_event.content())
|
||||
.expect("m.room.third_party_invite event content is not a JSON object"),
|
||||
) {
|
||||
// signatures -> { server_name: { ed25519:N: signature } }
|
||||
for (server_name, server_sigs) in signatures {
|
||||
trace!("Searching for signatures from {}", server_name);
|
||||
if let Some(server_sigs) = server_sigs.as_object() {
|
||||
for (key_id, signature_value) in server_sigs {
|
||||
trace!("Checking signature with key id {}", key_id);
|
||||
if let Some(signature_str) = signature_value.as_str() {
|
||||
if let Ok(signature) = decode_base64(signature_str) {
|
||||
debug!(
|
||||
%server_name,
|
||||
%key_id,
|
||||
"verifying third-party invite signature",
|
||||
);
|
||||
match verify_payload(
|
||||
&pk,
|
||||
&signature,
|
||||
serde_json::to_string(&to_canonical_value(signed).unwrap())
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
) {
|
||||
| Ok(()) => {
|
||||
debug!("valid third-party invite signature found");
|
||||
return true;
|
||||
},
|
||||
| Err(e) => {
|
||||
warn!(
|
||||
%server_name,
|
||||
%key_id,
|
||||
"invalid third-party invite signature: {e}",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A single public key in the public_key field
|
||||
tpid_ev.public_key == decoded_invite_token
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
join_rules::{
|
||||
AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership,
|
||||
},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
};
|
||||
use serde_json::value::to_raw_value as to_raw_json_value;
|
||||
|
||||
use crate::{
|
||||
matrix::{Event, EventTypeExt, Pdu as PduEvent},
|
||||
state_res::{
|
||||
RoomVersion, StateMap,
|
||||
event_auth::valid_membership_change,
|
||||
test_utils::{
|
||||
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id,
|
||||
member_content_ban, member_content_join, room_id, to_pdu_event,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_ban_pass() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(charlie().as_str()),
|
||||
member_content_ban(),
|
||||
&[],
|
||||
&["IMC"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = charlie();
|
||||
let sender = alice();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_non_creator() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS_CREATE_ROOM();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
charlie(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(charlie().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE"],
|
||||
&["CREATE"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = charlie();
|
||||
let sender = charlie();
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_creator() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS_CREATE_ROOM();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(alice().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE"],
|
||||
&["CREATE"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = alice();
|
||||
let sender = alice();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ban_fail() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
charlie(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(alice().as_str()),
|
||||
member_content_ban(),
|
||||
&[],
|
||||
&["IMC"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = alice();
|
||||
let sender = charlie();
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restricted_join_rule() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let mut events = INITIAL_EVENTS();
|
||||
*events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event(
|
||||
"IJR",
|
||||
alice(),
|
||||
TimelineEventType::RoomJoinRules,
|
||||
Some(""),
|
||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Restricted(
|
||||
Restricted::new(vec![AllowRule::RoomMembership(RoomMembership::new(
|
||||
room_id().to_owned(),
|
||||
))]),
|
||||
)))
|
||||
.unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"],
|
||||
&["IPOWER"],
|
||||
);
|
||||
|
||||
let mut member = RoomMemberEventContent::new(MembershipState::Join);
|
||||
member.join_authorized_via_users_server = Some(alice().to_owned());
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
ella(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(ella().as_str()),
|
||||
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap(),
|
||||
&["CREATE", "IJR", "IPOWER", "new"],
|
||||
&["new"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = ella();
|
||||
let sender = ella();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V9,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
Some(alice()),
|
||||
&MembershipState::Join,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V9,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
Some(ella()),
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_knock() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let mut events = INITIAL_EVENTS();
|
||||
*events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event(
|
||||
"IJR",
|
||||
alice(),
|
||||
TimelineEventType::RoomJoinRules,
|
||||
Some(""),
|
||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"],
|
||||
&["IPOWER"],
|
||||
);
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
ella(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(ella().as_str()),
|
||||
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(),
|
||||
&[],
|
||||
&["IMC"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = ella();
|
||||
let sender = ella();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V7,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
warn!("no valid signature found for third-party invite");
|
||||
false
|
||||
}
|
||||
|
||||
@@ -717,9 +717,6 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
|
||||
// The key for this is (eventType + a state_key of the signed token not sender)
|
||||
// so search for it
|
||||
let current_third_party = auth_state.iter().find_map(|(_, pdu)| {
|
||||
(*pdu.event_type() == TimelineEventType::RoomThirdPartyInvite).then_some(pdu)
|
||||
});
|
||||
|
||||
let fetch_state = |ty: &StateEventType, key: &str| {
|
||||
future::ready(
|
||||
@@ -732,7 +729,6 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
let auth_result = auth_check(
|
||||
room_version,
|
||||
&event,
|
||||
current_third_party,
|
||||
fetch_state,
|
||||
&fetch_state(&StateEventType::RoomCreate, "")
|
||||
.await
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
pub mod with_lock;
|
||||
|
||||
pub use ::conduwuit_macros::implement;
|
||||
pub use ::ctor::{ctor, dtor};
|
||||
|
||||
pub use self::{
|
||||
arrayvec::ArrayVecExt,
|
||||
|
||||
@@ -64,7 +64,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
ctor.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -73,7 +73,13 @@ 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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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;
|
||||
@@ -13,12 +13,8 @@
|
||||
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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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! {}
|
||||
|
||||
@@ -184,7 +184,6 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&to_room_version(&room_version_id),
|
||||
&pdu_event,
|
||||
None, // TODO: third party invite
|
||||
state_fetch,
|
||||
create_event.as_pdu(),
|
||||
)
|
||||
|
||||
@@ -100,7 +100,6 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&room_version,
|
||||
&incoming_pdu,
|
||||
None, // TODO: third party invite
|
||||
|ty, sk| state_fetch(ty.clone(), sk.into()),
|
||||
create_event.as_pdu(),
|
||||
)
|
||||
@@ -140,7 +139,6 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&room_version,
|
||||
&incoming_pdu,
|
||||
None, // third-party invite
|
||||
state_fetch,
|
||||
create_event.as_pdu(),
|
||||
)
|
||||
|
||||
@@ -236,15 +236,9 @@ fn from_evt(
|
||||
| _ => create_pdu.as_ref().unwrap().as_pdu(),
|
||||
};
|
||||
|
||||
let auth_check = state_res::auth_check(
|
||||
&room_version,
|
||||
&pdu,
|
||||
None, // TODO: third_party_invite
|
||||
auth_fetch,
|
||||
create_event,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
|
||||
let auth_check = state_res::auth_check(&room_version, &pdu, auth_fetch, create_event)
|
||||
.await
|
||||
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
|
||||
|
||||
if !auth_check {
|
||||
return Err!(Request(Forbidden("Event is not authorized.")));
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user