399 lines
14 KiB
Rust
399 lines
14 KiB
Rust
use crate::app_config::AppConfig;
|
|
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
|
use crate::users::UserEmail;
|
|
use crate::utils::rand_utils::rand_string;
|
|
use anyhow::Context;
|
|
use futures_util::Stream;
|
|
use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError;
|
|
use matrix_sdk::authentication::oauth::{
|
|
ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession,
|
|
};
|
|
use matrix_sdk::config::SyncSettings;
|
|
use matrix_sdk::encryption::recovery::RecoveryState;
|
|
use matrix_sdk::event_handler::{EventHandler, EventHandlerHandle, SyncEvent};
|
|
use matrix_sdk::ruma::presence::PresenceState;
|
|
use matrix_sdk::ruma::serde::Raw;
|
|
use matrix_sdk::ruma::{DeviceId, UserId};
|
|
use matrix_sdk::sync::SyncResponse;
|
|
use matrix_sdk::{Client, ClientBuildError, SendOutsideWasm};
|
|
use ractor::ActorRef;
|
|
use serde::de::DeserializeOwned;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::pin::Pin;
|
|
use url::Url;
|
|
|
|
/// The full session to persist.
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
struct StoredSession {
|
|
/// The OAuth 2.0 user session.
|
|
user_session: UserSession,
|
|
|
|
/// The OAuth 2.0 client ID.
|
|
client_id: ClientId,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
|
pub enum EncryptionRecoveryState {
|
|
Unknown,
|
|
Enabled,
|
|
Disabled,
|
|
Incomplete,
|
|
}
|
|
|
|
/// Matrix Gateway session errors
|
|
#[derive(thiserror::Error, Debug)]
|
|
enum MatrixClientError {
|
|
#[error("Failed to destroy previous client data! {0}")]
|
|
DestroyPreviousData(Box<MatrixClientError>),
|
|
#[error("Failed to create Matrix database storage directory! {0}")]
|
|
CreateMatrixDbDir(std::io::Error),
|
|
#[error("Failed to create database passphrase! {0}")]
|
|
CreateDbPassphrase(std::io::Error),
|
|
#[error("Failed to read database passphrase! {0}")]
|
|
ReadDbPassphrase(std::io::Error),
|
|
#[error("Failed to build Matrix client! {0}")]
|
|
BuildMatrixClient(ClientBuildError),
|
|
#[error("Failed to clear Matrix session file! {0}")]
|
|
ClearMatrixSessionFile(std::io::Error),
|
|
#[error("Failed to clear Matrix database storage directory! {0}")]
|
|
ClearMatrixDbDir(std::io::Error),
|
|
#[error("Failed to remove database passphrase! {0}")]
|
|
ClearDbPassphrase(std::io::Error),
|
|
#[error("Failed to fetch server metadata! {0}")]
|
|
FetchServerMetadata(OAuthDiscoveryError),
|
|
#[error("Failed to load stored session! {0}")]
|
|
LoadStoredSession(std::io::Error),
|
|
#[error("Failed to decode stored session! {0}")]
|
|
DecodeStoredSession(serde_json::Error),
|
|
#[error("Failed to restore stored session! {0}")]
|
|
RestoreSession(matrix_sdk::Error),
|
|
#[error("Failed to parse auth redirect URL! {0}")]
|
|
ParseAuthRedirectURL(url::ParseError),
|
|
#[error("Failed to build auth request! {0}")]
|
|
BuildAuthRequest(OAuthError),
|
|
#[error("Failed to finalize authentication! {0}")]
|
|
FinishLogin(matrix_sdk::Error),
|
|
#[error("Failed to write session file! {0}")]
|
|
WriteSessionFile(std::io::Error),
|
|
#[error("Failed to rename device! {0}")]
|
|
RenameDevice(matrix_sdk::HttpError),
|
|
#[error("Failed to set recovery key! {0}")]
|
|
SetRecoveryKey(matrix_sdk::encryption::recovery::RecoveryError),
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct FinishMatrixAuth {
|
|
code: String,
|
|
state: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MatrixClient {
|
|
manager: ActorRef<MatrixManagerMsg>,
|
|
pub email: UserEmail,
|
|
client: Client,
|
|
}
|
|
|
|
impl MatrixClient {
|
|
/// Start to build Matrix client to initiate user authentication
|
|
pub async fn build_client(
|
|
manager: ActorRef<MatrixManagerMsg>,
|
|
email: &UserEmail,
|
|
) -> anyhow::Result<Self> {
|
|
// Check if we are restoring a previous state
|
|
let session_file_path = AppConfig::get().user_matrix_session_file_path(email);
|
|
let is_restoring = session_file_path.is_file();
|
|
if !is_restoring {
|
|
Self::destroy_data(email).map_err(MatrixClientError::DestroyPreviousData)?;
|
|
}
|
|
|
|
// Determine Matrix database path
|
|
let db_path = AppConfig::get().user_matrix_db_path(email);
|
|
std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?;
|
|
|
|
// Generate or load passphrase
|
|
let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email);
|
|
if !passphrase_path.exists() {
|
|
std::fs::write(&passphrase_path, rand_string(32))
|
|
.map_err(MatrixClientError::CreateDbPassphrase)?;
|
|
}
|
|
let passphrase = std::fs::read_to_string(passphrase_path)
|
|
.map_err(MatrixClientError::ReadDbPassphrase)?;
|
|
|
|
let client = Client::builder()
|
|
.server_name_or_homeserver_url(&AppConfig::get().matrix_homeserver)
|
|
// Automatically refresh tokens if needed
|
|
.handle_refresh_tokens()
|
|
.sqlite_store(&db_path, Some(&passphrase))
|
|
.build()
|
|
.await
|
|
.map_err(MatrixClientError::BuildMatrixClient)?;
|
|
|
|
let client = Self {
|
|
manager,
|
|
email: email.clone(),
|
|
client,
|
|
};
|
|
|
|
// Check metadata
|
|
if !is_restoring {
|
|
let oauth = client.client.oauth();
|
|
let server_metadata = oauth
|
|
.server_metadata()
|
|
.await
|
|
.map_err(MatrixClientError::FetchServerMetadata)?;
|
|
log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer);
|
|
} else {
|
|
let session: StoredSession = serde_json::from_str(
|
|
std::fs::read_to_string(session_file_path)
|
|
.map_err(MatrixClientError::LoadStoredSession)?
|
|
.as_str(),
|
|
)
|
|
.map_err(MatrixClientError::DecodeStoredSession)?;
|
|
|
|
// Restore session
|
|
client
|
|
.client
|
|
.restore_session(OAuthSession {
|
|
client_id: session.client_id,
|
|
user: session.user_session,
|
|
})
|
|
.await
|
|
.map_err(MatrixClientError::RestoreSession)?;
|
|
|
|
// Wait for encryption tasks to complete
|
|
client
|
|
.client
|
|
.encryption()
|
|
.wait_for_e2ee_initialization_tasks()
|
|
.await;
|
|
}
|
|
|
|
// Automatically save session when token gets refreshed
|
|
client.setup_background_session_save().await;
|
|
|
|
Ok(client)
|
|
}
|
|
|
|
/// Destroy Matrix client related data
|
|
fn destroy_data(email: &UserEmail) -> anyhow::Result<(), Box<MatrixClientError>> {
|
|
let session_path = AppConfig::get().user_matrix_session_file_path(email);
|
|
if session_path.is_file() {
|
|
std::fs::remove_file(&session_path)
|
|
.map_err(MatrixClientError::ClearMatrixSessionFile)?;
|
|
}
|
|
|
|
let db_path = AppConfig::get().user_matrix_db_path(email);
|
|
if db_path.is_dir() {
|
|
std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?;
|
|
}
|
|
|
|
let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email);
|
|
if passphrase_path.is_file() {
|
|
std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Initiate OAuth authentication
|
|
pub async fn initiate_login(&self) -> anyhow::Result<Url> {
|
|
let oauth = self.client.oauth();
|
|
|
|
let metadata = AppConfig::get().matrix_client_metadata();
|
|
let client_metadata = Raw::new(&metadata).expect("Couldn't serialize client metadata");
|
|
|
|
let auth = oauth
|
|
.login(
|
|
Url::parse(&AppConfig::get().matrix_oauth_redirect_url())
|
|
.map_err(MatrixClientError::ParseAuthRedirectURL)?,
|
|
None,
|
|
Some(client_metadata.into()),
|
|
None,
|
|
)
|
|
.build()
|
|
.await
|
|
.map_err(MatrixClientError::BuildAuthRequest)?;
|
|
|
|
Ok(auth.url)
|
|
}
|
|
|
|
/// Finish OAuth authentication
|
|
pub async fn finish_login(&self, info: FinishMatrixAuth) -> anyhow::Result<()> {
|
|
let oauth = self.client.oauth();
|
|
oauth
|
|
.finish_login(UrlOrQuery::Query(format!(
|
|
"state={}&code={}",
|
|
info.state, info.code
|
|
)))
|
|
.await
|
|
.map_err(MatrixClientError::FinishLogin)?;
|
|
|
|
log::info!(
|
|
"User successfully authenticated as {}!",
|
|
self.client.user_id().unwrap()
|
|
);
|
|
|
|
// Persist session tokens
|
|
self.save_stored_session().await?;
|
|
|
|
// Rename created session to give it a more explicit name
|
|
self.client
|
|
.rename_device(
|
|
self.client
|
|
.session_meta()
|
|
.context("Missing device ID!")?
|
|
.device_id
|
|
.as_ref(),
|
|
&AppConfig::get().website_origin,
|
|
)
|
|
.await
|
|
.map_err(MatrixClientError::RenameDevice)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Automatically persist session onto disk
|
|
pub async fn setup_background_session_save(&self) {
|
|
let this = self.clone();
|
|
tokio::spawn(async move {
|
|
loop {
|
|
match this.client.subscribe_to_session_changes().recv().await {
|
|
Ok(update) => match update {
|
|
matrix_sdk::SessionChange::UnknownToken { soft_logout } => {
|
|
log::warn!(
|
|
"Received an unknown token error; soft logout? {soft_logout:?}"
|
|
);
|
|
if let Err(e) = this
|
|
.manager
|
|
.cast(MatrixManagerMsg::DisconnectClient(this.email))
|
|
{
|
|
log::warn!("Failed to propagate invalid token error: {e}");
|
|
}
|
|
break;
|
|
}
|
|
matrix_sdk::SessionChange::TokensRefreshed => {
|
|
// The tokens have been refreshed, persist them to disk.
|
|
if let Err(err) = this.save_stored_session().await {
|
|
log::error!("Unable to store a session in the background: {err}");
|
|
}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
log::error!("[!] Session change error: {e}");
|
|
log::error!("Session change background service INTERRUPTED!");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Update the session stored on the filesystem.
|
|
async fn save_stored_session(&self) -> anyhow::Result<()> {
|
|
log::debug!("Save the stored session for {:?}...", self.email);
|
|
|
|
let full_session = self
|
|
.client
|
|
.oauth()
|
|
.full_session()
|
|
.context("A logged in client must have a session")?;
|
|
|
|
let stored_session = StoredSession {
|
|
user_session: full_session.user,
|
|
client_id: full_session.client_id,
|
|
};
|
|
|
|
let serialized_session = serde_json::to_string(&stored_session)?;
|
|
std::fs::write(
|
|
AppConfig::get().user_matrix_session_file_path(&self.email),
|
|
serialized_session,
|
|
)
|
|
.map_err(MatrixClientError::WriteSessionFile)?;
|
|
|
|
log::debug!("Updating the stored session: done!");
|
|
Ok(())
|
|
}
|
|
|
|
/// Check whether a user is currently connected to this client or not
|
|
pub fn is_client_connected(&self) -> bool {
|
|
self.client.is_active()
|
|
}
|
|
|
|
/// Disconnect user from client
|
|
pub async fn disconnect(self) -> anyhow::Result<()> {
|
|
if let Err(e) = self.client.logout().await {
|
|
log::warn!("Failed to send logout request: {e}");
|
|
}
|
|
|
|
// Destroy user associated data
|
|
Self::destroy_data(&self.email)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get client Matrix device id
|
|
pub fn device_id(&self) -> Option<&DeviceId> {
|
|
self.client.device_id()
|
|
}
|
|
|
|
/// Get client Matrix user id
|
|
pub fn user_id(&self) -> Option<&UserId> {
|
|
self.client.user_id()
|
|
}
|
|
|
|
/// Get current encryption keys recovery state
|
|
pub fn recovery_state(&self) -> EncryptionRecoveryState {
|
|
match self.client.encryption().recovery().state() {
|
|
RecoveryState::Unknown => EncryptionRecoveryState::Unknown,
|
|
RecoveryState::Enabled => EncryptionRecoveryState::Enabled,
|
|
RecoveryState::Disabled => EncryptionRecoveryState::Disabled,
|
|
RecoveryState::Incomplete => EncryptionRecoveryState::Incomplete,
|
|
}
|
|
}
|
|
|
|
/// Set new encryption key recovery key
|
|
pub async fn set_recovery_key(&self, key: &str) -> anyhow::Result<()> {
|
|
Ok(self
|
|
.client
|
|
.encryption()
|
|
.recovery()
|
|
.recover(key)
|
|
.await
|
|
.map_err(MatrixClientError::SetRecoveryKey)?)
|
|
}
|
|
|
|
/// Get matrix synchronization settings to use
|
|
fn sync_settings() -> SyncSettings {
|
|
SyncSettings::default().set_presence(PresenceState::Offline)
|
|
}
|
|
|
|
/// Perform initial synchronization
|
|
pub async fn perform_initial_sync(&self) -> anyhow::Result<()> {
|
|
self.client.sync_once(Self::sync_settings()).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Perform routine synchronization
|
|
pub async fn sync_stream(
|
|
&self,
|
|
) -> Pin<Box<impl Stream<Item = matrix_sdk::Result<SyncResponse>>>> {
|
|
Box::pin(self.client.sync_stream(Self::sync_settings()).await)
|
|
}
|
|
|
|
/// Add new Matrix event handler
|
|
#[must_use]
|
|
pub fn add_event_handler<Ev, Ctx, H>(&self, handler: H) -> EventHandlerHandle
|
|
where
|
|
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
|
|
H: EventHandler<Ev, Ctx>,
|
|
{
|
|
self.client.add_event_handler(handler)
|
|
}
|
|
|
|
/// Remove Matrix event handler
|
|
pub fn remove_event_handler(&self, handle: EventHandlerHandle) {
|
|
self.client.remove_event_handler(handle)
|
|
}
|
|
}
|