Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature
[target.'cfg(target_os = "ios")'.dependencies]
robius-web-auth-session = { git = "https://github.com/project-robius/robius" }

## On Android, the matrix-sdk no longer sets up TLS roots for rustls by default,
## so we have to do it manually.
[target.'cfg(target_os = "android")'.dependencies]
webpki-root-certs = "1"


[features]
default = []
Expand Down
4 changes: 2 additions & 2 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3718,11 +3718,11 @@ fn populate_message_view(
Cow::from(&fb.body),
Some(FormattedBody {
format: fb.format.clone(),
body: format!("* {} {}", &username, &fb.body),
body: format!("* {} {}", username, fb.body),
})
)
} else {
(Cow::from(format!("* {} {}", &username, body)), None)
(Cow::from(format!("* {} {}", username, body)), None)
};
let html_or_plaintext_ref =
item.html_or_plaintext(cx, ids!(content.message));
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ pub fn project_dir() -> &'static ProjectDirs {
pub fn app_data_dir() -> &'static Path {
project_dir().data_dir()
}

pub fn cache_dir() -> &'static Path {
project_dir().cache_dir()
}
99 changes: 56 additions & 43 deletions src/persistence/matrix_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};

use crate::{
app_data_dir,
cache_dir,
login::login_screen::LoginAction,
};

Expand Down Expand Up @@ -146,13 +147,20 @@ fn resolve_db_path(stored: PathBuf) -> PathBuf {
///
/// This basically scans every saved user session dir, not just the most recent one,
/// to help ensure that db dirs don't get orphaned on the filesystem forever.
async fn collect_referenced_db_paths() -> std::collections::HashSet<PathBuf> {
///
/// Returns `None` if the app data directory can't be accessed,
/// which means that nothing should be considered as eligible for deletion.
async fn collect_referenced_db_paths() -> Option<std::collections::HashSet<PathBuf>> {
use std::collections::HashSet;
let mut paths = HashSet::new();
let data_dir = app_data_dir();

let Ok(mut entries) = tokio::fs::read_dir(data_dir).await else {
return paths;
let mut entries = match tokio::fs::read_dir(data_dir).await {
Ok(entries) => entries,
Err(e) => {
log!("collect_referenced_db_paths: could not read data dir {}: {e}", data_dir.display());
return None;
}
};

while let Ok(Some(entry)) = entries.next_entry().await {
Expand All @@ -179,68 +187,81 @@ async fn collect_referenced_db_paths() -> std::collections::HashSet<PathBuf> {
paths.insert(resolve_db_path(session.client_session.db_path));
}

paths
Some(paths)
}

/// Deletes `db_*` subdirs not referenced by any saved session. Only touches
/// entries that match the `db_*` prefix and that came from
/// `read_dir(app_data_dir())`, so it can't escape the data dir even with a
/// malicious session file.
pub async fn cleanup_orphan_db_dirs() {
let data_dir = app_data_dir();
let active = collect_referenced_db_paths().await;

let mut entries = match tokio::fs::read_dir(data_dir).await {
/// Deletes old database files that start with `"db_"` within the given `dir` and its subdirectories.
///
/// Only deletes database files that are inactive, i.e., where `is_active` returns false.
async fn prune_orphan_db_dirs(dir: &std::path::Path, is_active: impl Fn(&std::ffi::OsStr) -> bool) {
let mut entries = match tokio::fs::read_dir(dir).await {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
log!("cleanup_orphan_db_dirs: could not read data dir {}: {e}", data_dir.display());
log!("prune_orphan_db_dirs: could not read {}: {e}", dir.display());
return;
}
};

let mut deleted = 0usize;
let mut bytes_freed = 0u64;
let mut kept = 0usize;
let mut deleted: usize = 0;
let mut bytes_freed: u64 = 0;
let mut kept: usize = 0;

while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
let Some(name) = path.file_name() else {
continue;
};
let Some(name_str) = name.to_str() else {
continue;
};
if !name.starts_with("db_") {
if !name_str.starts_with("db_") {
continue;
}
if active.contains(&path) {
if is_active(name) {
kept += 1;
// log!("cleanup_orphan_db_dirs: preserving referenced db dir: {}", path.display());
continue;
}
let size = dir_size_bytes(&path).await.unwrap_or(0);
match tokio::fs::remove_dir_all(&path).await {
Ok(()) => {
deleted += 1;
bytes_freed += size;
log!(
"cleanup_orphan_db_dirs: deleted orphaned db dir ({} bytes): {}",
size,
path.display(),
);
// log!(
// "prune_orphan_db_dirs: deleted orphaned dir ({size} bytes): {}",
// path.display(),
// );
}
Err(e) => {
log!(
"cleanup_orphan_db_dirs: failed to delete {}: {e}",
path.display(),
);
log!("prune_orphan_db_dirs: failed to delete {}: {e}", path.display());
}
}
}

if deleted > 0 || kept > 0 {
log!(
"cleanup_orphan_db_dirs: deleted {deleted} orphan(s), freed {bytes_freed} bytes; kept {kept} active referenced",
);
log!("prune_orphan_db_dirs ({}): deleted {deleted} orphan(s), freed {bytes_freed} bytes; kept {kept} active", dir.display());
}
}

/// Deletes orphaned (no longer used) database and cache directories.
pub async fn cleanup_orphan_db_dirs() {
use std::collections::HashSet;
use std::ffi::OsString;

// If we couldn't read the data directory, we can't know which ones are active, so skip pruning.
let Some(active) = collect_referenced_db_paths().await else {
return;
};
let active_names: HashSet<OsString> = active
.iter()
.filter_map(|p| p.file_name().map(ToOwned::to_owned))
.collect();

let data_dir = app_data_dir();
prune_orphan_db_dirs(data_dir, |name| active.contains(&data_dir.join(name))).await;
prune_orphan_db_dirs(cache_dir(), |name| active_names.contains(name)).await;
}

/// Recursive size sum, best-effort. Just for the cleanup log line.
async fn dir_size_bytes(path: &std::path::Path) -> Option<u64> {
let mut total = 0u64;
Expand Down Expand Up @@ -328,19 +349,11 @@ pub async fn restore_session(
db_path.display(),
original_stored.display(),
);
let store_config = crate::sliding_sync::build_sqlite_store_config(&db_path, &client_session.passphrase);
// Build the client with the previous settings from the session.
let client = Client::builder()
let client = crate::sliding_sync::base_client_builder(&db_path, &client_session.passphrase)
.homeserver_url(client_session.homeserver)
.sqlite_store_with_config_and_cache_path(store_config, None::<&std::path::Path>)
.with_threading_support(matrix_sdk::ThreadingSupport::Enabled {
with_subscriptions: true,
})
.handle_refresh_tokens()
.build()
.await?;
let sliding_sync_version = sliding_sync_version.into();
client.set_sliding_sync_version(sliding_sync_version);
client.set_sliding_sync_version(sliding_sync_version.into());
let status_str = format!("Authenticating previous login session for {}...", user_session.meta.user_id);
log!("{status_str}");
Cx::post_action(LoginAction::Status {
Expand Down
2 changes: 1 addition & 1 deletion src/room/typing_notice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl TypingNotice {
[user1, user2] => format!("{user1} and {user2} are typing "),
[user1, user2, others @ ..] => {
if others.len() > 1 {
format!("{user1}, {user2}, and {} are typing ", &others[0])
format!("{user1}, {user2}, and {} are typing ", others[0])
} else {
format!(
"{user1}, {user2}, and {} others are typing ",
Expand Down
76 changes: 49 additions & 27 deletions src/sliding_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use std::{borrow::Cow, cmp::{max, min}, future::Future, hash::{BuildHasherDefaul
use std::io;
use hashbrown::{HashMap, HashSet};
use crate::{
app::AppStateAction, app_data_dir, avatar_cache::AvatarUpdate, event_preview::{BeforeText, TextPreview, text_preview_of_raw_timeline_event, text_preview_of_timeline_item}, home::{
app::AppStateAction, app_data_dir, cache_dir, avatar_cache::AvatarUpdate, event_preview::{BeforeText, TextPreview, text_preview_of_raw_timeline_event, text_preview_of_timeline_item}, home::{
add_room::KnockResultAction, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, tombstone_footer::SuccessorRoomDetails
}, login::login_screen::LoginAction, logout::{logout_confirm_modal::LogoutAction, logout_state_machine::{LogoutConfig, is_logout_in_progress, logout_with_state_machine}}, media_cache::{MediaCacheEntry, MediaCacheEntryRef}, persistence::{self, ClientSessionPersisted, load_app_state}, profile::{
user_profile::UserProfile,
Expand Down Expand Up @@ -109,6 +109,51 @@ pub fn build_sqlite_store_config(
.passphrase(Some(passphrase))
}

/// Fix the SDK to use standard webpki TLS root certificates, only relevant on Android.
///
/// This is necessary because of: <https://github.com/matrix-org/matrix-rust-sdk/pull/6645>.
fn use_android_tls_roots(builder: matrix_sdk::ClientBuilder) -> matrix_sdk::ClientBuilder {
#[cfg(target_os = "android")]
let builder = {
let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|der| matrix_sdk::reqwest::Certificate::from_der(der.as_ref()).ok())
.collect();
builder
.disable_built_in_root_certificates()
.add_root_certificates(roots)
};
builder
}

/// Creates and returns a `ClientBuilder` configured with every setting Robrix needs.
pub(crate) fn base_client_builder(db_path: &Path, passphrase: &str) -> matrix_sdk::ClientBuilder {
let store_config = build_sqlite_store_config(db_path, passphrase);
// Store event and media cache content in the platform's intended cache dir.
let cache_path = db_path.file_name().map(|name| cache_dir().join(name));

let builder = Client::builder()
.sqlite_store_with_config_and_cache_path(store_config, cache_path)
.with_threading_support(matrix_sdk::ThreadingSupport::Enabled {
with_subscriptions: true,
})
.with_decryption_settings(DecryptionSettings {
sender_device_trust_requirement: TrustRequirement::Untrusted,
})
.with_encryption_settings(EncryptionSettings {
auto_enable_cross_signing: true,
backup_download_strategy: matrix_sdk::encryption::BackupDownloadStrategy::OneShot,
auto_enable_backups: true,
})
.with_enable_share_history_on_invite(true)
.handle_refresh_tokens()
// Use a 60 second timeout for all requests to the homeserver.
// Yes, this is a long timeout, but the standard matrix homeserver is often very slow.
.request_config(RequestConfig::new().timeout(std::time::Duration::from_secs(60)));

use_android_tls_roots(builder)
}

/// Build a new client.
async fn build_client(
cli: &Cli,
Expand Down Expand Up @@ -144,38 +189,15 @@ async fn build_client(
.unwrap_or("https://matrix-client.matrix.org/");
// .unwrap_or("https://matrix.org/");

let store_config = build_sqlite_store_config(&db_path, &passphrase);

let mut builder = Client::builder()
let mut builder = base_client_builder(&db_path, &passphrase)
.server_name_or_homeserver_url(homeserver_url)
// Use a sqlite database to persist the client's encryption setup.
.sqlite_store_with_config_and_cache_path(store_config, None::<&std::path::Path>)
.with_threading_support(matrix_sdk::ThreadingSupport::Enabled {
with_subscriptions: true,
})
// The sliding sync proxy has now been deprecated in favor of native sliding sync.
.sliding_sync_version_builder(VersionBuilder::DiscoverNative)
.with_decryption_settings(DecryptionSettings {
sender_device_trust_requirement: TrustRequirement::Untrusted,
})
.with_encryption_settings(EncryptionSettings {
auto_enable_cross_signing: true,
backup_download_strategy: matrix_sdk::encryption::BackupDownloadStrategy::OneShot,
auto_enable_backups: true,
})
.with_enable_share_history_on_invite(true)
.handle_refresh_tokens();
.sliding_sync_version_builder(VersionBuilder::DiscoverNative);

if let Some(proxy) = cli.proxy.as_ref() {
builder = builder.proxy(proxy.clone());
}

// Use a 60 second timeout for all requests to the homeserver.
// Yes, this is a long timeout, but the standard matrix homeserver is often very slow.
builder = builder.request_config(
RequestConfig::new()
.timeout(std::time::Duration::from_secs(60))
);
let client = builder.build().await?;
let homeserver_url = client.homeserver().to_string();
Ok((
Expand Down Expand Up @@ -4913,7 +4935,7 @@ async fn spawn_sso_server(
enqueue_rooms_list_update(RoomsListUpdate::Status {
status: format!(
"Logged in as {:?}.\n → Loading rooms...",
&identity_provider_res.user_id
identity_provider_res.user_id
),
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/tsp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ async fn create_did_web(
let transport = Url::parse(
&format!("https://{}/endpoint/{}",
server,
&did.replace("%", "%25")
did.replace("%", "%25")
)
).map_err(|e| anyhow!("Invalid transport URL: {e}"))?;

Expand Down
Loading