-
-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Jason Volk <[email protected]>
- Loading branch information
Showing
11 changed files
with
2,035 additions
and
1,960 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
use clap::Subcommand; | ||
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent}; | ||
|
||
use crate::{service::admin::escape_html, services, Result}; | ||
|
||
#[cfg_attr(test, derive(Debug))] | ||
#[derive(Subcommand)] | ||
pub(crate) enum AppserviceCommand { | ||
/// - Register an appservice using its registration YAML | ||
/// | ||
/// This command needs a YAML generated by an appservice (such as a bridge), | ||
/// which must be provided in a Markdown code block below the command. | ||
/// | ||
/// Registering a new bridge using the ID of an existing bridge will replace | ||
/// the old one. | ||
Register, | ||
|
||
/// - Unregister an appservice using its ID | ||
/// | ||
/// You can find the ID using the `list-appservices` command. | ||
Unregister { | ||
/// The appservice to unregister | ||
appservice_identifier: String, | ||
}, | ||
|
||
/// - Show an appservice's config using its ID | ||
/// | ||
/// You can find the ID using the `list-appservices` command. | ||
Show { | ||
/// The appservice to show | ||
appservice_identifier: String, | ||
}, | ||
|
||
/// - List all the currently registered appservices | ||
List, | ||
} | ||
|
||
pub(crate) async fn process(command: AppserviceCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> { | ||
match command { | ||
AppserviceCommand::Register => { | ||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { | ||
let appservice_config = body[1..body.len() - 1].join("\n"); | ||
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config); | ||
match parsed_config { | ||
Ok(yaml) => match services().appservice.register_appservice(yaml).await { | ||
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!( | ||
"Appservice registered with ID: {id}." | ||
))), | ||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!( | ||
"Failed to register appservice: {e}" | ||
))), | ||
}, | ||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!( | ||
"Could not parse appservice config: {e}" | ||
))), | ||
} | ||
} else { | ||
Ok(RoomMessageEventContent::text_plain( | ||
"Expected code block in command body. Add --help for details.", | ||
)) | ||
} | ||
}, | ||
AppserviceCommand::Unregister { | ||
appservice_identifier, | ||
} => match services().appservice.unregister_appservice(&appservice_identifier).await { | ||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Appservice unregistered.")), | ||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!( | ||
"Failed to unregister appservice: {e}" | ||
))), | ||
}, | ||
AppserviceCommand::Show { | ||
appservice_identifier, | ||
} => match services().appservice.get_registration(&appservice_identifier).await { | ||
Some(config) => { | ||
let config_str = serde_yaml::to_string(&config).expect("config should've been validated on register"); | ||
let output = format!("Config for {}:\n\n```yaml\n{}\n```", appservice_identifier, config_str,); | ||
let output_html = format!( | ||
"Config for {}:\n\n<pre><code class=\"language-yaml\">{}</code></pre>", | ||
escape_html(&appservice_identifier), | ||
escape_html(&config_str), | ||
); | ||
Ok(RoomMessageEventContent::text_html(output, output_html)) | ||
}, | ||
None => Ok(RoomMessageEventContent::text_plain("Appservice does not exist.")), | ||
}, | ||
AppserviceCommand::List => { | ||
let appservices = services().appservice.iter_ids().await; | ||
let output = format!("Appservices ({}): {}", appservices.len(), appservices.join(", ")); | ||
Ok(RoomMessageEventContent::text_plain(output)) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
use std::{collections::BTreeMap, sync::Arc, time::Instant}; | ||
|
||
use clap::Subcommand; | ||
use ruma::{ | ||
api::client::error::ErrorKind, events::room::message::RoomMessageEventContent, CanonicalJsonObject, EventId, | ||
RoomId, RoomVersionId, ServerName, | ||
}; | ||
use tokio::sync::RwLock; | ||
use tracing::{debug, error, info, warn}; | ||
|
||
use crate::{api::server_server::parse_incoming_pdu, services, utils::HtmlEscape, Error, PduEvent, Result}; | ||
|
||
#[cfg_attr(test, derive(Debug))] | ||
#[derive(Subcommand)] | ||
pub(crate) enum DebugCommand { | ||
/// - Get the auth_chain of a PDU | ||
GetAuthChain { | ||
/// An event ID (the $ character followed by the base64 reference hash) | ||
event_id: Box<EventId>, | ||
}, | ||
|
||
/// - Parse and print a PDU from a JSON | ||
/// | ||
/// The PDU event is only checked for validity and is not added to the | ||
/// database. | ||
/// | ||
/// This command needs a JSON blob provided in a Markdown code block below | ||
/// the command. | ||
ParsePdu, | ||
|
||
/// - Retrieve and print a PDU by ID from the conduwuit database | ||
GetPdu { | ||
/// An event ID (a $ followed by the base64 reference hash) | ||
event_id: Box<EventId>, | ||
}, | ||
|
||
/// - Attempts to retrieve a PDU from a remote server. Inserts it into our | ||
/// database/timeline if found and we do not have this PDU already | ||
/// (following normal event auth rules, handles it as an incoming PDU). | ||
GetRemotePdu { | ||
/// An event ID (a $ followed by the base64 reference hash) | ||
event_id: Box<EventId>, | ||
|
||
/// Argument for us to attempt to fetch the event from the | ||
/// specified remote server. | ||
server: Box<ServerName>, | ||
}, | ||
|
||
/// - Gets all the room state events for the specified room. | ||
/// | ||
/// This is functionally equivalent to `GET | ||
/// /_matrix/client/v3/rooms/{roomid}/state`, except the admin command does | ||
/// *not* check if the sender user is allowed to see state events. This is | ||
/// done because it's implied that server admins here have database access | ||
/// and can see/get room info themselves anyways if they were malicious | ||
/// admins. | ||
/// | ||
/// Of course the check is still done on the actual client API. | ||
GetRoomState { | ||
/// Room ID | ||
room_id: Box<RoomId>, | ||
}, | ||
|
||
/// - Forces device lists for all local and remote users to be updated (as | ||
/// having new keys available) | ||
ForceDeviceListUpdates, | ||
} | ||
|
||
pub(crate) async fn process(command: DebugCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> { | ||
Ok(match command { | ||
DebugCommand::GetAuthChain { | ||
event_id, | ||
} => { | ||
let event_id = Arc::<EventId>::from(event_id); | ||
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? { | ||
let room_id_str = event | ||
.get("room_id") | ||
.and_then(|val| val.as_str()) | ||
.ok_or_else(|| Error::bad_database("Invalid event in database"))?; | ||
|
||
let room_id = <&RoomId>::try_from(room_id_str) | ||
.map_err(|_| Error::bad_database("Invalid room id field in event in database"))?; | ||
let start = Instant::now(); | ||
let count = services().rooms.auth_chain.get_auth_chain(room_id, vec![event_id]).await?.count(); | ||
let elapsed = start.elapsed(); | ||
RoomMessageEventContent::text_plain(format!("Loaded auth chain with length {count} in {elapsed:?}")) | ||
} else { | ||
RoomMessageEventContent::text_plain("Event not found.") | ||
} | ||
}, | ||
DebugCommand::ParsePdu => { | ||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { | ||
let string = body[1..body.len() - 1].join("\n"); | ||
match serde_json::from_str(&string) { | ||
Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) { | ||
Ok(hash) => { | ||
let event_id = EventId::parse(format!("${hash}")); | ||
|
||
match serde_json::from_value::<PduEvent>( | ||
serde_json::to_value(value).expect("value is json"), | ||
) { | ||
Ok(pdu) => { | ||
RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}")) | ||
}, | ||
Err(e) => RoomMessageEventContent::text_plain(format!( | ||
"EventId: {event_id:?}\nCould not parse event: {e}" | ||
)), | ||
} | ||
}, | ||
Err(e) => RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}")), | ||
}, | ||
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json in command body: {e}")), | ||
} | ||
} else { | ||
RoomMessageEventContent::text_plain("Expected code block in command body.") | ||
} | ||
}, | ||
DebugCommand::GetPdu { | ||
event_id, | ||
} => { | ||
let mut outlier = false; | ||
let mut pdu_json = services().rooms.timeline.get_non_outlier_pdu_json(&event_id)?; | ||
if pdu_json.is_none() { | ||
outlier = true; | ||
pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?; | ||
} | ||
match pdu_json { | ||
Some(json) => { | ||
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json"); | ||
return Ok(RoomMessageEventContent::text_html( | ||
format!( | ||
"{}\n```json\n{}\n```", | ||
if outlier { | ||
"Outlier PDU found in our database" | ||
} else { | ||
"PDU found in our database" | ||
}, | ||
json_text | ||
), | ||
format!( | ||
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n", | ||
if outlier { | ||
"Outlier PDU found in our database" | ||
} else { | ||
"PDU found in our database" | ||
}, | ||
HtmlEscape(&json_text) | ||
), | ||
)); | ||
}, | ||
None => { | ||
return Ok(RoomMessageEventContent::text_plain("PDU not found locally.")); | ||
}, | ||
} | ||
}, | ||
DebugCommand::GetRemotePdu { | ||
event_id, | ||
server, | ||
} => { | ||
if !services().globals.config.allow_federation { | ||
return Ok(RoomMessageEventContent::text_plain( | ||
"Federation is disabled on this homeserver.", | ||
)); | ||
} | ||
|
||
if server == services().globals.server_name() { | ||
return Ok(RoomMessageEventContent::text_plain( | ||
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local \ | ||
PDUs.", | ||
)); | ||
} | ||
|
||
// TODO: use Futures as some requests may take a while so we dont block the | ||
// admin room | ||
match services() | ||
.sending | ||
.send_federation_request( | ||
&server, | ||
ruma::api::federation::event::get_event::v1::Request { | ||
event_id: event_id.clone().into(), | ||
}, | ||
) | ||
.await | ||
{ | ||
Ok(response) => { | ||
let json: CanonicalJsonObject = serde_json::from_str(response.pdu.get()).map_err(|e| { | ||
warn!( | ||
"Requested event ID {event_id} from server but failed to convert from RawValue to \ | ||
CanonicalJsonObject (malformed event/response?): {e}" | ||
); | ||
Error::BadRequest(ErrorKind::Unknown, "Received response from server but failed to parse PDU") | ||
})?; | ||
|
||
debug!("Attempting to parse PDU: {:?}", &response.pdu); | ||
let parsed_pdu = { | ||
let parsed_result = parse_incoming_pdu(&response.pdu); | ||
let (event_id, value, room_id) = match parsed_result { | ||
Ok(t) => t, | ||
Err(e) => { | ||
warn!("Failed to parse PDU: {e}"); | ||
info!("Full PDU: {:?}", &response.pdu); | ||
return Ok(RoomMessageEventContent::text_plain(format!( | ||
"Failed to parse PDU remote server {server} sent us: {e}" | ||
))); | ||
}, | ||
}; | ||
|
||
vec![(event_id, value, room_id)] | ||
}; | ||
|
||
let pub_key_map = RwLock::new(BTreeMap::new()); | ||
|
||
debug!("Attempting to fetch homeserver signing keys for {server}"); | ||
services() | ||
.rooms | ||
.event_handler | ||
.fetch_required_signing_keys( | ||
parsed_pdu.iter().map(|(_event_id, event, _room_id)| event), | ||
&pub_key_map, | ||
) | ||
.await | ||
.unwrap_or_else(|e| { | ||
warn!("Could not fetch all signatures for PDUs from {server}: {e:?}"); | ||
}); | ||
|
||
info!("Attempting to handle event ID {event_id} as backfilled PDU"); | ||
services().rooms.timeline.backfill_pdu(&server, response.pdu, &pub_key_map).await?; | ||
|
||
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json"); | ||
|
||
return Ok(RoomMessageEventContent::text_html( | ||
format!( | ||
"{}\n```json\n{}\n```", | ||
"Got PDU from specified server and handled as backfilled PDU successfully. Event body:", | ||
json_text | ||
), | ||
format!( | ||
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n", | ||
"Got PDU from specified server and handled as backfilled PDU successfully. Event body:", | ||
HtmlEscape(&json_text) | ||
), | ||
)); | ||
}, | ||
Err(_) => { | ||
return Ok(RoomMessageEventContent::text_plain( | ||
"Remote server did not have PDU or failed sending request to remote server.", | ||
)); | ||
}, | ||
} | ||
}, | ||
DebugCommand::GetRoomState { | ||
room_id, | ||
} => { | ||
let room_state = services() | ||
.rooms | ||
.state_accessor | ||
.room_state_full(&room_id) | ||
.await? | ||
.values() | ||
.map(|pdu| pdu.to_state_event()) | ||
.collect::<Vec<_>>(); | ||
|
||
if room_state.is_empty() { | ||
return Ok(RoomMessageEventContent::text_plain( | ||
"Unable to find room state in our database (vector is empty)", | ||
)); | ||
} | ||
|
||
let json_text = serde_json::to_string_pretty(&room_state).map_err(|e| { | ||
error!("Failed converting room state vector in our database to pretty JSON: {e}"); | ||
Error::bad_database( | ||
"Failed to convert room state events to pretty JSON, possible invalid room state events in our \ | ||
database", | ||
) | ||
})?; | ||
|
||
return Ok(RoomMessageEventContent::text_html( | ||
format!("{}\n```json\n{}\n```", "Found full room state", json_text), | ||
format!( | ||
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n", | ||
"Found full room state", | ||
HtmlEscape(&json_text) | ||
), | ||
)); | ||
}, | ||
DebugCommand::ForceDeviceListUpdates => { | ||
// Force E2EE device list updates for all users | ||
for user_id in services().users.iter().filter_map(std::result::Result::ok) { | ||
services().users.mark_device_key_update(&user_id)?; | ||
} | ||
RoomMessageEventContent::text_plain("Marked all devices for all users as having new keys to update") | ||
}, | ||
}) | ||
} |
Oops, something went wrong.