Consider command execution secure when proper conditions are met.
continuous-integration/drone/push Build is failing Details

- If the room is end-to-end encrypted.
 - If only the sending user and the bot are present in the room.

This lays groundwork for sensitive commands like registering a user
account with the bot.
This commit is contained in:
projectmoon 2021-05-21 22:01:52 +00:00
parent 9de74d05a9
commit 34ee2c6e5d
8 changed files with 186 additions and 101 deletions

View File

@ -30,7 +30,8 @@ async fn main() -> Result<(), BotError> {
.expect("Could not create matrix client"), .expect("Could not create matrix client"),
room: RoomContext { room: RoomContext {
id: &room_id!("!fakeroomid:example.com"), id: &room_id!("!fakeroomid:example.com"),
display_name: "fake room", display_name: "fake room".to_owned(),
secure: false,
}, },
username: "@localuser:example.com", username: "@localuser:example.com",
message_body: &input, message_body: &input,

View File

@ -0,0 +1,122 @@
use crate::commands::{execute_command, ExecutionError, ExecutionResult, ResponseExtractor};
use crate::context::{Context, RoomContext};
use crate::db::sqlite::Database;
use crate::error::BotError;
use crate::matrix;
use futures::stream::{self, StreamExt};
use log::{error, info};
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client};
use std::clone::Clone;
/// Handle responding to a single command being executed. Wil print
/// out the full result of that command.
pub(super) async fn handle_single_result(
client: &Client,
cmd_result: &ExecutionResult,
respond_to: &str,
room: &Joined,
event_id: EventId,
) {
if cmd_result.is_err() {
error!(
"Command execution error: {}",
cmd_result.as_ref().err().unwrap()
);
}
let html = cmd_result.message_html(respond_to);
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
}
/// Handle responding to multiple commands being executed. Will print
/// out how many commands succeeded and failed (if any failed).
pub(super) async fn handle_multiple_results(
client: &Client,
results: &[(String, ExecutionResult)],
respond_to: &str,
room: &Joined,
) {
let respond_to = format!(
"<a href=\"https://matrix.to/#/{}\">{}</a>",
respond_to, respond_to
);
let errors: Vec<(&str, &ExecutionError)> = results
.into_iter()
.filter_map(|(cmd, result)| match result {
Err(e) => Some((cmd.as_ref(), e)),
_ => None,
})
.collect();
for result in errors.iter() {
error!("Command execution error: '{}' - {}", result.0, result.1);
}
let message = if errors.len() == 0 {
format!("{}: Executed {} commands", respond_to, results.len())
} else {
let failures: Vec<String> = errors
.iter()
.map(|&(cmd, err)| format!("<strong>{}:</strong> {}", cmd, err))
.collect();
format!(
"{}: Executed {} commands ({} failed)\n\nFailures:\n{}",
respond_to,
results.len(),
errors.len(),
failures.join("\n")
)
.replace("\n", "<br/>")
};
matrix::send_message(client, room.room_id(), &message, None).await;
}
/// Create a context for command execution. Can fai if the room
/// context creation fails.
async fn create_context<'a>(
db: &'a Database,
client: &'a Client,
room: &'a Joined,
sender: &'a str,
command: &'a str,
) -> Result<Context<'a>, BotError> {
let room_ctx = RoomContext::new(room, sender).await?;
Ok(Context {
db: db.clone(),
matrix_client: client,
room: room_ctx,
username: &sender,
message_body: &command,
})
}
/// Attempt to execute all commands sent to the bot in a message. This
/// asynchronously executes all commands given to it. A Vec of all
/// commands and their execution results are returned.
pub(super) async fn execute(
commands: Vec<&str>,
db: &Database,
client: &Client,
room: &Joined,
sender: &str,
) -> Vec<(String, ExecutionResult)> {
stream::iter(commands)
.then(|command| async move {
match create_context(db, client, room, sender, command).await {
Err(e) => (command.to_owned(), Err(ExecutionError(e))),
Ok(ctx) => {
let cmd_result = execute_command(&ctx).await;
info!(
"[{}] {} executed: {}",
ctx.room.display_name, sender, command
);
(command.to_owned(), cmd_result)
}
}
})
.collect()
.await
}

View File

@ -1,20 +1,18 @@
use crate::commands::{execute_command, ExecutionError, ExecutionResult, ResponseExtractor}; use crate::commands::{ExecutionError, ExecutionResult};
use crate::config::*; use crate::config::*;
use crate::context::{Context, RoomContext};
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::db::DbState; use crate::db::DbState;
use crate::error::BotError; use crate::error::BotError;
use crate::matrix;
use crate::state::DiceBotState; use crate::state::DiceBotState;
use dirs; use dirs;
use futures::stream::{self, StreamExt}; use log::info;
use log::{error, info};
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, ClientConfig, SyncSettings}; use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, ClientConfig, SyncSettings};
use std::clone::Clone; use std::clone::Clone;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use url::Url; use url::Url;
mod command_execution;
pub mod event_handlers; pub mod event_handlers;
/// How many commands can be in one message. If the amount is higher /// How many commands can be in one message. If the amount is higher
@ -53,72 +51,6 @@ fn create_client(config: &Config) -> Result<Client, BotError> {
Ok(Client::new_with_config(homeserver_url, client_config)?) Ok(Client::new_with_config(homeserver_url, client_config)?)
} }
/// Handle responding to a single command being executed. Wil print
/// out the full result of that command.
async fn handle_single_result(
client: &Client,
cmd_result: &ExecutionResult,
respond_to: &str,
room: &Joined,
event_id: EventId,
) {
if cmd_result.is_err() {
error!(
"Command execution error: {}",
cmd_result.as_ref().err().unwrap()
);
}
let html = cmd_result.message_html(respond_to);
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
}
/// Handle responding to multiple commands being executed. Will print
/// out how many commands succeeded and failed (if any failed).
async fn handle_multiple_results(
client: &Client,
results: &[(String, ExecutionResult)],
respond_to: &str,
room: &Joined,
) {
let respond_to = format!(
"<a href=\"https://matrix.to/#/{}\">{}</a>",
respond_to, respond_to
);
let errors: Vec<(&str, &ExecutionError)> = results
.into_iter()
.filter_map(|(cmd, result)| match result {
Err(e) => Some((cmd.as_ref(), e)),
_ => None,
})
.collect();
for result in errors.iter() {
error!("Command execution error: '{}' - {}", result.0, result.1);
}
let message = if errors.len() == 0 {
format!("{}: Executed {} commands", respond_to, results.len())
} else {
let failures: Vec<String> = errors
.iter()
.map(|&(cmd, err)| format!("<strong>{}:</strong> {}", cmd, err))
.collect();
format!(
"{}: Executed {} commands ({} failed)\n\nFailures:\n{}",
respond_to,
results.len(),
errors.len(),
failures.join("\n")
)
.replace("\n", "<br/>")
};
matrix::send_message(client, room.room_id(), &message, None).await;
}
impl DiceBot { impl DiceBot {
/// Create a new dicebot with the given configuration and state /// Create a new dicebot with the given configuration and state
/// actor. This function returns a Result because it is possible /// actor. This function returns a Result because it is possible
@ -184,11 +116,9 @@ impl DiceBot {
async fn execute_commands( async fn execute_commands(
&self, &self,
room: &Joined, room: &Joined,
sender_username: &str, sender: &str,
msg_body: &str, msg_body: &str,
) -> Vec<(String, ExecutionResult)> { ) -> Vec<(String, ExecutionResult)> {
let room_name: &str = &room.display_name().await.ok().unwrap_or_default();
let commands: Vec<&str> = msg_body let commands: Vec<&str> = msg_body
.lines() .lines()
.filter(|line| line.starts_with("!")) .filter(|line| line.starts_with("!"))
@ -197,22 +127,7 @@ impl DiceBot {
//Up to 50 commands allowed, otherwise we send back an error. //Up to 50 commands allowed, otherwise we send back an error.
let results: Vec<(String, ExecutionResult)> = if commands.len() < MAX_COMMANDS_PER_MESSAGE { let results: Vec<(String, ExecutionResult)> = if commands.len() < MAX_COMMANDS_PER_MESSAGE {
stream::iter(commands) command_execution::execute(commands, &self.db, &self.client, room, sender).await
.then(|command| async move {
let ctx = Context {
db: self.db.clone(),
matrix_client: &self.client,
room: RoomContext::new_with_name(&room, room_name),
username: &sender_username,
message_body: &command,
};
let cmd_result = execute_command(&ctx).await;
info!("[{}] {} executed: {}", room_name, sender_username, command);
(command.to_owned(), cmd_result)
})
.collect()
.await
} else { } else {
vec![( vec![(
"".to_owned(), "".to_owned(),
@ -232,7 +147,7 @@ impl DiceBot {
) { ) {
if results.len() >= 1 { if results.len() >= 1 {
if results.len() == 1 { if results.len() == 1 {
handle_single_result( command_execution::handle_single_result(
&self.client, &self.client,
&results[0].1, &results[0].1,
sender_username, sender_username,
@ -241,7 +156,13 @@ impl DiceBot {
) )
.await; .await;
} else if results.len() > 1 { } else if results.len() > 1 {
handle_multiple_results(&self.client, &results, sender_username, &room).await; command_execution::handle_multiple_results(
&self.client,
&results,
sender_username,
&room,
)
.await;
} }
} }
} }

View File

@ -333,7 +333,8 @@ mod tests {
() => { () => {
crate::context::RoomContext { crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
display_name: "displayname", display_name: "displayname".to_owned(),
secure: false,
} }
}; };
} }

View File

@ -117,7 +117,8 @@ mod tests {
() => { () => {
crate::context::RoomContext { crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
display_name: "displayname", display_name: "displayname".to_owned(),
secure: false,
} }
}; };
} }

View File

@ -1,7 +1,9 @@
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use matrix_sdk::identifiers::RoomId; use crate::error::BotError;
use matrix_sdk::identifiers::{RoomId, UserId};
use matrix_sdk::room::Joined; use matrix_sdk::room::Joined;
use matrix_sdk::Client; use matrix_sdk::Client;
use std::convert::TryFrom;
/// A context carried through the system providing access to things /// A context carried through the system providing access to things
/// like the database. /// like the database.
@ -18,19 +20,52 @@ impl Context<'_> {
pub fn room_id(&self) -> &RoomId { pub fn room_id(&self) -> &RoomId {
self.room.id self.room.id
} }
pub fn is_secure(&self) -> bool {
self.room.secure
}
} }
#[derive(Clone)] #[derive(Clone)]
pub struct RoomContext<'a> { pub struct RoomContext<'a> {
pub id: &'a RoomId, pub id: &'a RoomId,
pub display_name: &'a str, pub display_name: String,
pub secure: bool,
} }
impl RoomContext<'_> { impl RoomContext<'_> {
pub fn new_with_name<'a>(room: &'a Joined, display_name: &'a str) -> RoomContext<'a> { pub async fn new_with_name<'a>(
RoomContext { room: &'a Joined,
display_name: String,
sending_user: &str,
) -> Result<RoomContext<'a>, BotError> {
// TODO is_direct is a hack; should set rooms to Direct
// Message upon joining, if other contact has requested it.
// Waiting on SDK support.
let sending_user = UserId::try_from(sending_user)?;
let user_in_room = room.get_member(&sending_user).await.ok().is_some();
let is_direct = room.joined_members().await?.len() == 2;
Ok(RoomContext {
id: room.room_id(), id: room.room_id(),
display_name, display_name,
} secure: room.is_encrypted() && is_direct && user_in_room,
})
}
pub async fn new<'a>(
room: &'a Joined,
sending_user: &str,
) -> Result<RoomContext<'a>, BotError> {
Self::new_with_name(
&room,
room.display_name()
.await
.ok()
.unwrap_or_default()
.to_string(),
sending_user,
)
.await
} }
} }

View File

@ -427,7 +427,8 @@ mod tests {
() => { () => {
crate::context::RoomContext { crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
display_name: "displayname", display_name: "displayname".to_owned(),
secure: false,
} }
}; };
} }

View File

@ -75,6 +75,9 @@ pub enum BotError {
#[error("could not convert to proper integer type")] #[error("could not convert to proper integer type")]
TryFromIntError(#[from] std::num::TryFromIntError), TryFromIntError(#[from] std::num::TryFromIntError),
#[error("identifier error: {0}")]
IdentifierError(#[from] matrix_sdk::identifiers::Error),
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]