add appservice MSC4190 support

Signed-off-by: June Clementine Strawberry <june@3.dog>
This commit is contained in:
June Clementine Strawberry 2025-04-03 12:20:10 -04:00
parent 0e0b8cc403
commit 24be579477
7 changed files with 125 additions and 50 deletions

22
Cargo.lock generated
View file

@ -3531,7 +3531,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"assign", "assign",
"js_int", "js_int",
@ -3551,7 +3551,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3563,7 +3563,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@ -3586,7 +3586,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@ -3618,7 +3618,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap 2.8.0", "indexmap 2.8.0",
@ -3643,7 +3643,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@ -3665,7 +3665,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror 2.0.12", "thiserror 2.0.12",
@ -3674,7 +3674,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3684,7 +3684,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-crate", "proc-macro-crate",
@ -3699,7 +3699,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3711,7 +3711,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=ea1278657125e9414caada074e8c172bc252fb1c#ea1278657125e9414caada074e8c172bc252fb1c" source = "git+https://github.com/girlbossceo/ruwuma?rev=0701341a2fd5a6ea74beada18d5974cc401a4fc1#0701341a2fd5a6ea74beada18d5974cc401a4fc1"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",

View file

@ -346,7 +346,7 @@ version = "0.1.2"
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://github.com/girlbossceo/ruwuma" git = "https://github.com/girlbossceo/ruwuma"
#branch = "conduwuit-changes" #branch = "conduwuit-changes"
rev = "ea1278657125e9414caada074e8c172bc252fb1c" rev = "0701341a2fd5a6ea74beada18d5974cc401a4fc1"
features = [ features = [
"compat", "compat",
"rand", "rand",

View file

@ -318,14 +318,14 @@ pub(crate) async fn register_route(
// Success! // Success!
}, },
| _ => match body.json_body { | _ => match body.json_body {
| Some(json) => { | Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create( services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name()) &UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(), .unwrap(),
"".into(), "".into(),
&uiaainfo, &uiaainfo,
&json, json,
); );
return Err(Error::Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
@ -373,8 +373,12 @@ pub(crate) async fn register_route(
) )
.await?; .await?;
// Inhibit login does not work for guests if (!is_guest && body.inhibit_login)
if !is_guest && body.inhibit_login { || body
.appservice_info
.as_ref()
.is_some_and(|appservice| appservice.registration.device_management)
{
return Ok(register::v3::Response { return Ok(register::v3::Response {
access_token: None, access_token: None,
user_id, user_id,

View file

@ -22,7 +22,13 @@ pub(crate) async fn appservice_ping(
))); )));
} }
if appservice_info.registration.url.is_none() { if appservice_info.registration.url.is_none()
|| appservice_info
.registration
.url
.as_ref()
.is_some_and(|url| url.is_empty() || url == "null")
{
return Err!(Request(UrlNotSet( return Err!(Request(UrlNotSet(
"Appservice does not have a URL set, there is nothing to ping." "Appservice does not have a URL set, there is nothing to ping."
))); )));

View file

@ -1,9 +1,9 @@
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, err}; use conduwuit::{Err, debug, err};
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
MilliSecondsSinceUnixEpoch, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
api::client::{ api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device}, device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind, error::ErrorKind,
@ -12,7 +12,7 @@ use ruma::{
}; };
use super::SESSION_ID_LENGTH; use super::SESSION_ID_LENGTH;
use crate::{Error, Result, Ruma, utils}; use crate::{Error, Result, Ruma, client::DEVICE_ID_LENGTH, utils};
/// # `GET /_matrix/client/r0/devices` /// # `GET /_matrix/client/r0/devices`
/// ///
@ -59,14 +59,15 @@ pub(crate) async fn update_device_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<update_device::v3::Request>, body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> { ) -> Result<update_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
let mut device = services match services
.users .users
.get_device_metadata(sender_user, &body.device_id) .get_device_metadata(sender_user, &body.device_id)
.await .await
.map_err(|_| err!(Request(NotFound("Device not found."))))?; {
| Ok(mut device) => {
device.display_name.clone_from(&body.display_name); device.display_name.clone_from(&body.display_name);
device.last_seen_ip.clone_from(&Some(client.to_string())); device.last_seen_ip.clone_from(&Some(client.to_string()));
device device
@ -79,6 +80,37 @@ pub(crate) async fn update_device_route(
.await?; .await?;
Ok(update_device::v3::Response {}) Ok(update_device::v3::Response {})
},
| Err(_) => {
let Some(appservice) = appservice else {
return Err!(Request(NotFound("Device not found.")));
};
if !appservice.registration.device_management {
return Err!(Request(NotFound("Device not found.")));
}
debug!(
"Creating new device for {sender_user} from appservice {} as MSC4190 is enabled \
and device ID does not exist",
appservice.registration.id
);
let device_id = OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH));
services
.users
.create_device(
sender_user,
&device_id,
&appservice.registration.as_token,
None,
Some(client.to_string()),
)
.await?;
return Ok(update_device::v3::Response {});
},
}
} }
/// # `DELETE /_matrix/client/r0/devices/{deviceId}` /// # `DELETE /_matrix/client/r0/devices/{deviceId}`
@ -95,8 +127,21 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>, body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> { ) -> Result<delete_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let (sender_user, sender_device) = body.sender();
let sender_device = body.sender_device.as_ref().expect("user is authenticated"); let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
services
.users
.remove_device(sender_user, &body.device_id)
.await;
return Ok(delete_device::v3::Response {});
}
// UIAA // UIAA
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
@ -120,11 +165,11 @@ pub(crate) async fn delete_device_route(
// Success! // Success!
}, },
| _ => match body.json_body { | _ => match body.json_body {
| Some(json) => { | Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services services
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, &json); .create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo)); return Err!(Uiaa(uiaainfo));
}, },
@ -142,11 +187,12 @@ pub(crate) async fn delete_device_route(
Ok(delete_device::v3::Response {}) Ok(delete_device::v3::Response {})
} }
/// # `PUT /_matrix/client/r0/devices/{deviceId}` /// # `POST /_matrix/client/v3/delete_devices`
/// ///
/// Deletes the given device. /// Deletes the given list of devices.
/// ///
/// - Requires UIAA to verify user password /// - Requires UIAA to verify user password unless from an appservice with
/// MSC4190 enabled.
/// ///
/// For each device: /// For each device:
/// - Invalidates access token /// - Invalidates access token
@ -158,8 +204,20 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>, body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> { ) -> Result<delete_devices::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let (sender_user, sender_device) = body.sender();
let sender_device = body.sender_device.as_ref().expect("user is authenticated"); let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;
}
return Ok(delete_devices::v3::Response {});
}
// UIAA // UIAA
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
@ -183,11 +241,11 @@ pub(crate) async fn delete_devices_route(
// Success! // Success!
}, },
| _ => match body.json_body { | _ => match body.json_body {
| Some(json) => { | Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services services
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, &json); .create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },

View file

@ -25,6 +25,10 @@ where
return Ok(None); return Ok(None);
}; };
if dest == *"null" || dest.is_empty() {
return Ok(None);
}
trace!("Appservice URL \"{dest}\", Appservice ID: {}", registration.id); trace!("Appservice URL \"{dest}\", Appservice ID: {}", registration.id);
let hs_token = registration.hs_token.as_str(); let hs_token = registration.hs_token.as_str();
@ -34,7 +38,11 @@ where
SendAccessToken::IfRequired(hs_token), SendAccessToken::IfRequired(hs_token),
&VERSIONS, &VERSIONS,
) )
.map_err(|e| err!(BadServerResponse(warn!("Failed to find destination {dest}: {e}"))))? .map_err(|e| {
err!(BadServerResponse(
warn!(appservice = %registration.id, "Failed to find destination {dest}: {e:?}")
))
})?
.map(BytesMut::freeze); .map(BytesMut::freeze);
let mut parts = http_request.uri().clone().into_parts(); let mut parts = http_request.uri().clone().into_parts();
@ -51,7 +59,7 @@ where
let reqwest_request = reqwest::Request::try_from(http_request)?; let reqwest_request = reqwest::Request::try_from(http_request)?;
let mut response = client.execute(reqwest_request).await.map_err(|e| { let mut response = client.execute(reqwest_request).await.map_err(|e| {
warn!("Could not send request to appservice \"{}\" at {dest}: {e}", registration.id); warn!("Could not send request to appservice \"{}\" at {dest}: {e:?}", registration.id);
e e
})?; })?;
@ -71,7 +79,7 @@ where
if !status.is_success() { if !status.is_success() {
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body)); debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body));
return Err!(BadServerResponse(error!( return Err!(BadServerResponse(warn!(
"Appservice \"{}\" returned unsuccessful HTTP response {status} at {dest}", "Appservice \"{}\" returned unsuccessful HTTP response {status} at {dest}",
registration.id registration.id
))); )));
@ -84,8 +92,8 @@ where
); );
response.map(Some).map_err(|e| { response.map(Some).map_err(|e| {
err!(BadServerResponse(error!( err!(BadServerResponse(warn!(
"Appservice \"{}\" returned invalid response bytes {dest}: {e}", "Appservice \"{}\" returned invalid/malformed response bytes {dest}: {e}",
registration.id registration.id
))) )))
}) })

View file

@ -350,7 +350,6 @@ impl Service {
token: &str, token: &str,
) -> Result<()> { ) -> Result<()> {
let key = (user_id, device_id); let key = (user_id, device_id);
// should not be None, but we shouldn't assert either lol...
if self.db.userdeviceid_metadata.qry(&key).await.is_err() { if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
return Err!(Database(error!( return Err!(Database(error!(
?user_id, ?user_id,