Skip to content

Commit

Permalink
split admin commands into modules.
Browse files Browse the repository at this point in the history
Signed-off-by: Jason Volk <[email protected]>
  • Loading branch information
jevolk committed Mar 24, 2024
1 parent f07f082 commit c8d9d8d
Show file tree
Hide file tree
Showing 11 changed files with 2,035 additions and 1,960 deletions.
92 changes: 92 additions & 0 deletions src/service/admin/appservice.rs
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))
},
}
}
294 changes: 294 additions & 0 deletions src/service/admin/debug.rs
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")
},
})
}
Loading

0 comments on commit c8d9d8d

Please sign in to comment.