308 lines
11 KiB
Rust
308 lines
11 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 matrix_sdk::authentication::oauth::error::{
|
|
BasicErrorResponseType, OAuthDiscoveryError, RequestTokenError,
|
|
};
|
|
use matrix_sdk::authentication::oauth::{
|
|
ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession,
|
|
};
|
|
use matrix_sdk::ruma::serde::Raw;
|
|
use matrix_sdk::{Client, ClientBuildError, RefreshTokenError};
|
|
use ractor::ActorRef;
|
|
use serde::{Deserialize, Serialize};
|
|
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,
|
|
}
|
|
|
|
/// 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 disconnect user! {0}")]
|
|
DisconnectUser(anyhow::Error),
|
|
#[error("Failed to refresh access token! {0}")]
|
|
InitialRefreshToken(RefreshTokenError),
|
|
#[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),
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct FinishMatrixAuth {
|
|
code: String,
|
|
state: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MatrixClient {
|
|
manager: ActorRef<MatrixManagerMsg>,
|
|
pub email: UserEmail,
|
|
pub 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
|
|
let oauth = client.client.oauth();
|
|
let server_metadata = oauth
|
|
.server_metadata()
|
|
.await
|
|
.map_err(MatrixClientError::FetchServerMetadata)?;
|
|
log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer);
|
|
|
|
if is_restoring {
|
|
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)?;
|
|
|
|
// Force token refresh to make sure session is still alive, otherwise disconnect user
|
|
if let Err(refresh_error) = client.client.oauth().refresh_access_token().await {
|
|
if let RefreshTokenError::OAuth(e) = &refresh_error
|
|
&& let OAuthError::RefreshToken(RequestTokenError::ServerResponse(e)) = &**e
|
|
&& e.error() == &BasicErrorResponseType::InvalidGrant
|
|
{
|
|
log::warn!(
|
|
"Refresh token rejected by server, token must have been invalidated! {refresh_error}"
|
|
);
|
|
client
|
|
.disconnect()
|
|
.await
|
|
.map_err(MatrixClientError::DisconnectUser)?;
|
|
}
|
|
return Err(MatrixClientError::InitialRefreshToken(refresh_error).into());
|
|
}
|
|
}
|
|
|
|
// 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?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Automatically persist session onto disk
|
|
pub async fn setup_background_session_save(&self) {
|
|
let this = self.clone();
|
|
tokio::spawn(async move {
|
|
while let Ok(update) = this.client.subscribe_to_session_changes().recv().await {
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 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 user_session: UserSession = self
|
|
.client
|
|
.oauth()
|
|
.user_session()
|
|
.context("A logged in client must have a session")?;
|
|
|
|
let stored_session = StoredSession {
|
|
user_session,
|
|
client_id: self
|
|
.client
|
|
.oauth()
|
|
.client_id()
|
|
.context("Client ID should be set at this point!")?
|
|
.clone(),
|
|
};
|
|
|
|
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(())
|
|
}
|
|
|
|
/// 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(())
|
|
}
|
|
}
|