mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3feb32572c | |||
| ca51993ee3 | |||
| 655eacfa7b | |||
| 3825bff733 | |||
| d90d22c917 |
Generated
+14
-15
@@ -1090,7 +1090,6 @@ dependencies = [
|
||||
"core_affinity",
|
||||
"ctor",
|
||||
"cyborgtime",
|
||||
"ed25519-dalek",
|
||||
"either",
|
||||
"figment",
|
||||
"futures",
|
||||
@@ -1312,7 +1311,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "continuwuity-admin-api"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
@@ -1692,7 +1691,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "draupnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
@@ -3040,7 +3039,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
[[package]]
|
||||
name = "meowlnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
@@ -4255,7 +4254,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.10.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"continuwuity-admin-api",
|
||||
@@ -4278,7 +4277,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-appservice-api"
|
||||
version = "0.10.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4290,7 +4289,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.18.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4313,7 +4312,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.22.1",
|
||||
@@ -4345,7 +4344,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.28.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
@@ -4370,7 +4369,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"headers",
|
||||
@@ -4392,7 +4391,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.5"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4401,7 +4400,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identity-service-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4411,7 +4410,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
@@ -4426,7 +4425,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4438,7 +4437,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.15.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=db86e37d602cc4935853bf14d5aace47d22358ed#db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
|
||||
+1
-1
@@ -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 = "85d00fb5746cba23904234b4fd3c838dcf141541"
|
||||
rev = "db86e37d602cc4935853bf14d5aace47d22358ed"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
|
||||
+3
-9
@@ -34,14 +34,6 @@
|
||||
"name": "troubleshooting",
|
||||
"label": "Troubleshooting"
|
||||
},
|
||||
"security",
|
||||
{
|
||||
"type": "dir-section-header",
|
||||
"name": "community",
|
||||
"label": "Community",
|
||||
"collapsible": true,
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
@@ -71,5 +63,7 @@
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
}
|
||||
},
|
||||
"community",
|
||||
"security"
|
||||
]
|
||||
|
||||
+5
-10
@@ -19,21 +19,16 @@
|
||||
{
|
||||
"text": "Admin Command Reference",
|
||||
"link": "/reference/admin/"
|
||||
},
|
||||
{
|
||||
"text": "Server Reference",
|
||||
"link": "/reference/server"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Community",
|
||||
"items": [
|
||||
{
|
||||
"text": "Community Guidelines",
|
||||
"link": "/community/guidelines"
|
||||
},
|
||||
{
|
||||
"text": "Become a Partnered Homeserver!",
|
||||
"link": "/community/ops-guidelines"
|
||||
}
|
||||
]
|
||||
"link": "/community"
|
||||
},
|
||||
{
|
||||
"text": "Security",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[
|
||||
{
|
||||
"type": "file",
|
||||
"name": "guidelines",
|
||||
"label": "Community Guidelines"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "ops-guidelines",
|
||||
"label": "Partnered Homeserver Guidelines"
|
||||
}
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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.
|
||||
@@ -54,9 +54,6 @@ export default defineConfig({
|
||||
}, {
|
||||
from: '/server_reference',
|
||||
to: '/reference/server'
|
||||
}, {
|
||||
from: '/community$',
|
||||
to: '/community/guidelines'
|
||||
}
|
||||
]
|
||||
})],
|
||||
|
||||
+73
-2
@@ -3,8 +3,11 @@ use std::time::Duration;
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, err,
|
||||
Err, Result,
|
||||
debug::DebugInspect,
|
||||
debug_info, err,
|
||||
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_service::{
|
||||
Services,
|
||||
@@ -12,8 +15,9 @@ use conduwuit_service::{
|
||||
};
|
||||
use reqwest::Url;
|
||||
use ruma::{
|
||||
Mxc, UserId,
|
||||
Mxc, OwnedServerName, UserId,
|
||||
api::client::{
|
||||
authenticated_media,
|
||||
authenticated_media::{
|
||||
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||
get_media_preview,
|
||||
@@ -244,6 +248,73 @@ pub(crate) async fn get_media_preview_route(
|
||||
})
|
||||
}
|
||||
|
||||
async fn dispatch_redaction(
|
||||
server_name: OwnedServerName,
|
||||
media_id: String,
|
||||
servers: Vec<OwnedServerName>,
|
||||
services: crate::State,
|
||||
) {
|
||||
for server in servers {
|
||||
if services.globals.server_is_ours(&server) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug_info!("Asking {server} to redact media mxc://{server_name}/{media_id}");
|
||||
services
|
||||
.federation
|
||||
.execute(&server, authenticated_media::redact::unstable::Request {
|
||||
server_name: server_name.clone(),
|
||||
media_id: media_id.clone(),
|
||||
})
|
||||
.await
|
||||
.debug_inspect(|_| {
|
||||
debug_info!("Asked {server} to redact media mxc://{server_name}/{media_id}");
|
||||
})
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to ask {server} to redact media mxc://{server_name}/{media_id}: {e}"
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "media_redact",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(%_client),
|
||||
)]
|
||||
pub(crate) async fn redact_media_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(_client): InsecureClientIp,
|
||||
body: Ruma<authenticated_media::redact::unstable::Request>,
|
||||
) -> Result<authenticated_media::redact::unstable::Response> {
|
||||
let user = body.sender_user();
|
||||
|
||||
let mxc = Mxc {
|
||||
server_name: &body.server_name,
|
||||
media_id: &body.media_id,
|
||||
};
|
||||
|
||||
if !services.media.user_owns(user, &mxc).await {
|
||||
return Err!(Request(Forbidden("You do not have permission to redact this attachment.")));
|
||||
}
|
||||
|
||||
services.media.redact(&mxc).await?;
|
||||
|
||||
// TODO: This should be a persistent background task
|
||||
let servers = services.media.get_interested_servers(&mxc).await;
|
||||
services.server.runtime().spawn(dispatch_redaction(
|
||||
mxc.server_name.to_owned(),
|
||||
mxc.media_id.to_owned(),
|
||||
servers,
|
||||
services,
|
||||
));
|
||||
|
||||
Ok(authenticated_media::redact::unstable::Response {})
|
||||
}
|
||||
|
||||
async fn fetch_thumbnail(
|
||||
services: &Services,
|
||||
mxc: &Mxc<'_>,
|
||||
|
||||
@@ -657,6 +657,7 @@ 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
|
||||
|
||||
@@ -154,6 +154,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::get_content_route)
|
||||
.ruma_route(&client::get_content_as_filename_route)
|
||||
.ruma_route(&client::get_media_preview_route)
|
||||
.ruma_route(&client::redact_media_route)
|
||||
.ruma_route(&client::get_media_config_route)
|
||||
.ruma_route(&client::get_devices_route)
|
||||
.ruma_route(&client::get_device_route)
|
||||
|
||||
@@ -46,6 +46,7 @@ pub(crate) async fn get_content_route(
|
||||
content_type: content_type.map(Into::into),
|
||||
content_disposition: Some(content_disposition),
|
||||
};
|
||||
services.media.mark_server_interested(&mxc, body.origin());
|
||||
|
||||
Ok(get_content::v1::Response {
|
||||
content: FileOrLocation::File(content),
|
||||
@@ -89,6 +90,7 @@ pub(crate) async fn get_content_thumbnail_route(
|
||||
content_type: content_type.map(Into::into),
|
||||
content_disposition: Some(content_disposition),
|
||||
};
|
||||
services.media.mark_server_interested(&mxc, body.origin());
|
||||
|
||||
Ok(get_content_thumbnail::v1::Response {
|
||||
content: FileOrLocation::File(content),
|
||||
|
||||
+13
-14
@@ -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,7 +110,6 @@ tracing.workspace = true
|
||||
url.workspace = true
|
||||
parking_lot.workspace = true
|
||||
lock_api.workspace = true
|
||||
ed25519-dalek = "~2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
use std::{borrow::Borrow, collections::BTreeSet};
|
||||
|
||||
use ed25519_dalek::{Verifier, VerifyingKey};
|
||||
use futures::{
|
||||
Future,
|
||||
future::{OptionFuture, join, join3},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, Int, OwnedUserId, RoomVersionId, UserId,
|
||||
canonical_json::to_canonical_value,
|
||||
Int, OwnedUserId, RoomVersionId, UserId,
|
||||
events::room::{
|
||||
create::RoomCreateEventContent,
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, ThirdPartyInvite},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
third_party_invite::{PublicKey, RoomThirdPartyInviteEventContent},
|
||||
third_party_invite::RoomThirdPartyInviteEventContent,
|
||||
},
|
||||
int,
|
||||
serde::{
|
||||
Base64, Base64DecodeError, Raw,
|
||||
base64::{Standard, UrlSafe},
|
||||
},
|
||||
signatures::{ParseError, VerificationError},
|
||||
serde::{Base64, Raw},
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
de::{Error as _, IgnoredAny},
|
||||
};
|
||||
use serde_json::{
|
||||
from_str as from_json_str, to_value,
|
||||
value::{RawValue as RawJsonValue, to_raw_value},
|
||||
};
|
||||
use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue};
|
||||
|
||||
use super::{
|
||||
Error, Event, Result, StateEventType, StateKey, TimelineEventType,
|
||||
@@ -40,7 +30,7 @@ use super::{
|
||||
},
|
||||
room_version::RoomVersion,
|
||||
};
|
||||
use crate::{debug, error, trace, utils::to_canonical_object, warn};
|
||||
use crate::{debug, error, trace, warn};
|
||||
|
||||
// FIXME: field extracting could be bundled for `content`
|
||||
#[derive(Deserialize)]
|
||||
@@ -167,14 +157,15 @@ pub fn auth_types_for_event(
|
||||
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 + Sync,
|
||||
F: Fn(&StateEventType, &str) -> Fut + Send,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
for<'a> &'a E: Event + Send + Sync,
|
||||
for<'a> &'a E: Event + Send,
|
||||
{
|
||||
debug!(
|
||||
event_id = %incoming_event.event_id(),
|
||||
@@ -424,15 +415,13 @@ where
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -669,25 +658,23 @@ where
|
||||
/// event and the current State.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn valid_membership_change<F, Fut, E>(
|
||||
fn valid_membership_change<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 + Sync,
|
||||
for<'a> &'a E: Event + Send,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct GetThirdPartyInvite {
|
||||
@@ -963,62 +950,68 @@ where
|
||||
| MembershipState::Invite => {
|
||||
// If content has third_party_invite key
|
||||
trace!("starting target_membership=invite check");
|
||||
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);
|
||||
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 !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 {
|
||||
@@ -1506,187 +1499,399 @@ fn get_send_level(
|
||||
.unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) })
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
|
||||
/// 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())
|
||||
// The state key must match the invitee
|
||||
if target_user != Some(&tp_id.signed.mxid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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 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,
|
||||
};
|
||||
|
||||
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}",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
warn!("no valid signature found for third-party invite");
|
||||
false
|
||||
// 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,6 +717,9 @@ where
|
||||
|
||||
// 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(
|
||||
@@ -729,6 +732,7 @@ where
|
||||
let auth_result = auth_check(
|
||||
room_version,
|
||||
&event,
|
||||
current_third_party,
|
||||
fetch_state,
|
||||
&fetch_state(&StateEventType::RoomCreate, "")
|
||||
.await
|
||||
|
||||
@@ -108,6 +108,14 @@ pub(super) static MAPS: &[Descriptor] = &[
|
||||
name: "mediaid_user",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "mediaid_redacted",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "mediaid_interestedservername",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "onetimekeyid_onetimekeys",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_info, err,
|
||||
@@ -6,13 +6,15 @@ use conduwuit::{
|
||||
};
|
||||
use database::{Database, Interfix, Map};
|
||||
use futures::StreamExt;
|
||||
use ruma::{Mxc, OwnedMxcUri, UserId, http_headers::ContentDisposition};
|
||||
use ruma::{Mxc, OwnedMxcUri, OwnedServerName, UserId, http_headers::ContentDisposition};
|
||||
|
||||
use super::{preview::UrlPreviewData, thumbnail::Dim};
|
||||
|
||||
pub(crate) struct Data {
|
||||
mediaid_file: Arc<Map>,
|
||||
mediaid_user: Arc<Map>,
|
||||
mediaid_redacted: Arc<Map>,
|
||||
mediaid_interestedservername: Arc<Map>,
|
||||
url_previews: Arc<Map>,
|
||||
}
|
||||
|
||||
@@ -28,6 +30,8 @@ impl Data {
|
||||
Self {
|
||||
mediaid_file: db["mediaid_file"].clone(),
|
||||
mediaid_user: db["mediaid_user"].clone(),
|
||||
mediaid_redacted: db["mediaid_redacted"].clone(),
|
||||
mediaid_interestedservername: db["mediaid_interestedservername"].clone(),
|
||||
url_previews: db["url_previews"].clone(),
|
||||
}
|
||||
}
|
||||
@@ -77,6 +81,22 @@ impl Data {
|
||||
self.mediaid_user.remove(key);
|
||||
})
|
||||
.await;
|
||||
|
||||
self.mediaid_interestedservername
|
||||
.stream_prefix_raw(&prefix)
|
||||
.ignore_err()
|
||||
.ready_for_each(|(key, _)| {
|
||||
debug_assert!(
|
||||
key.starts_with(mxc.to_string().as_bytes()),
|
||||
"key should start with the mxc"
|
||||
);
|
||||
|
||||
debug_info!("Deleting interested server name key {key:?}");
|
||||
|
||||
self.mediaid_interestedservername.remove(key);
|
||||
})
|
||||
.await;
|
||||
// NOTE: Redaction status is kept even after deletion
|
||||
}
|
||||
|
||||
/// Searches for all files with the given MXC
|
||||
@@ -275,4 +295,35 @@ impl Data {
|
||||
image_height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Marks a media item as redacted, preventing it from being served or
|
||||
/// re-used.
|
||||
pub(super) fn mark_redacted(&self, media_id: &str) {
|
||||
self.mediaid_redacted.insert(media_id, []);
|
||||
}
|
||||
|
||||
/// Checks if a media item is redacted.
|
||||
pub(super) async fn is_redacted(&self, media_id: &str) -> bool {
|
||||
self.mediaid_redacted.contains(media_id).await
|
||||
}
|
||||
|
||||
pub(super) fn add_interested_server_name(&self, media_id: &str, server_name: &str) {
|
||||
let key = (media_id, server_name);
|
||||
self.mediaid_interestedservername
|
||||
.insert(&database::serialize_key(key).expect("key must be serializable"), []);
|
||||
}
|
||||
|
||||
pub(super) async fn interested_server_names(&self, media_id: &str) -> Vec<OwnedServerName> {
|
||||
let prefix = (media_id, Interfix);
|
||||
self.mediaid_interestedservername
|
||||
.stream_prefix_raw(&prefix)
|
||||
.ignore_err()
|
||||
.map(|(key, _)| {
|
||||
let parts: Vec<&[u8]> = key.rsplit(|&b| b == 0xFF).collect();
|
||||
OwnedServerName::parse(string_from_bytes(parts[0]).unwrap_or_default())
|
||||
.unwrap_or_else(|_| OwnedServerName::from_str("invalid.server").unwrap())
|
||||
})
|
||||
.collect()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ use conduwuit::{
|
||||
},
|
||||
warn,
|
||||
};
|
||||
use ruma::{Mxc, OwnedMxcUri, UserId, http_headers::ContentDisposition};
|
||||
use ruma::{
|
||||
Mxc, OwnedMxcUri, OwnedServerName, ServerName, UserId, http_headers::ContentDisposition,
|
||||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
@@ -139,6 +141,28 @@ impl Service {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a media ID as redacted, and deletes the associated file.
|
||||
pub async fn redact(&self, mxc: &Mxc<'_>) -> Result<()> {
|
||||
self.db.mark_redacted(mxc.media_id);
|
||||
self.delete(mxc).await
|
||||
}
|
||||
|
||||
/// Checks if a media ID is redacted.
|
||||
pub async fn is_redacted(&self, mxc: &Mxc<'_>) -> bool {
|
||||
self.db.is_redacted(mxc.media_id).await
|
||||
}
|
||||
|
||||
/// Marks a server as "interested" (i.e. has downloaded this media from us).
|
||||
pub fn mark_server_interested(&self, mxc: &Mxc<'_>, server_name: &ServerName) {
|
||||
self.db
|
||||
.add_interested_server_name(mxc.media_id, server_name.as_str());
|
||||
}
|
||||
|
||||
/// Gets all servers interested in this media ID.
|
||||
pub async fn get_interested_servers(&self, mxc: &Mxc<'_>) -> Vec<OwnedServerName> {
|
||||
self.db.interested_server_names(mxc.media_id).await
|
||||
}
|
||||
|
||||
/// Deletes all media by the specified user
|
||||
///
|
||||
/// currently, this is only practical for local users
|
||||
@@ -229,6 +253,18 @@ impl Service {
|
||||
Ok(mxcs)
|
||||
}
|
||||
|
||||
/// Checks if a user owns a given MXC URI
|
||||
pub async fn user_owns(&self, user: &UserId, mxc: &Mxc<'_>) -> bool {
|
||||
self
|
||||
.db
|
||||
.get_all_user_mxcs(user) // TODO: this can be more efficient.
|
||||
.await
|
||||
.iter()
|
||||
.any(|v| {
|
||||
v.parts().map(|x|(x.server_name, x.media_id)) == Ok((mxc.server_name, mxc.media_id))
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes all media files in the given time frame.
|
||||
/// Returns a usize with the amount of media files deleted.
|
||||
pub async fn delete_all_media_within_timeframe(
|
||||
|
||||
@@ -184,6 +184,7 @@ where
|
||||
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,6 +100,7 @@ where
|
||||
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(),
|
||||
)
|
||||
@@ -139,6 +140,7 @@ where
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&room_version,
|
||||
&incoming_pdu,
|
||||
None, // third-party invite
|
||||
state_fetch,
|
||||
create_event.as_pdu(),
|
||||
)
|
||||
|
||||
@@ -236,9 +236,15 @@ pub async fn create_hash_and_sign_event(
|
||||
| _ => create_pdu.as_ref().unwrap().as_pdu(),
|
||||
};
|
||||
|
||||
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:?}")))))?;
|
||||
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:?}")))))?;
|
||||
|
||||
if !auth_check {
|
||||
return Err!(Request(Forbidden("Event is not authorized.")));
|
||||
|
||||
@@ -304,7 +304,7 @@ impl Service {
|
||||
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
|
||||
|
||||
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
|
||||
self.db.userid_logindisabled.contains(user_id).await
|
||||
self.db.userid_logindisabled.exists(user_id).await.is_ok()
|
||||
}
|
||||
|
||||
/// Check if account is active, infallible
|
||||
|
||||
Reference in New Issue
Block a user