mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 13:31:03 +03:00
fix: android: replace all-files access with scoped storage (#15602)
* fix: android: replace all-files access with scoped storage + system picker Remove MANAGE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, and WRITE_EXTERNAL_STORAGE from the Android manifest. Remove requestLegacyExternalStorage. Replace broad external storage with app-scoped external storage for the file-transfer workspace. File import uses the system file_picker. File export uses Android's SAF ACTION_CREATE_DOCUMENT with path validation that restricts export sources to app-owned directories. Remove the external_path dependency. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: refine file import feedback Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: use SAF for file imports Replace file_picker imports with Android's Storage Access Framework to avoid legacy storage permissions, stale cached files, and duplicate staging of large imports. Stream selected documents into app-scoped storage with failure-safe replacement, keep exports restricted to validated app storage roots, use filesDir for the internal fallback workspace, and remove legacy permissions contributed during manifest merging. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: keep file imports in the selected directory Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: reset projection and constrain file workspace Release capture resources when media projection is revoked externally. Keep Android local file navigation within the app-scoped workspace. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: handle scoped storage start-up regressions. Allow zero digits in POSIX filenames by rejecting NUL explicitly, and initialise the app-specific home directory before the Android service starts the native server. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: update content resolver mode to use 'wt' instead of 'w' to prevent trailing bytes from old document whilst reporting sucess Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android, enforce file workspace boundary on the server, and unblock the ui thread. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: validate rename destinations against the app workspace bound file-operation paths. report rename failures, general import failures, and unregister / reregister projection when its onStop callback fires. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: reconnect was refreshing the directory with net entry instances, while selected items retained the old instances, it was reporting a selected item, but checkbox statue used object identity, and appeared unchecked. Fixed by reconciling by path and entry type before replacing the directory snapshot, rebinding valid selections, and dropping missing ones. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: (android) add SAF folder import and multi item export - import directories using ACTION_OPEN_DOCUMENT_TREE. Export multiple files, logs, and screen recordings via export buttons, add localisation keys for new actions Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix(android): harden scoped storage file handling - create new SAF documents instead of overwriting export sources - reject empty peer paths except for home directory reads - report directory backup restore and cleanup failures - resolve log export paths from the configured app name Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): harden scoped-storage file operations - snapshot directory exports before writing to the destination - query document provider metadata off the main thread - reject invalid remote directories without read timeouts Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): handle SAF directory name collisions - reject dot-segment folder names during import - fail imports with duplicate document display names - only reuse matching directories during export Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): handle SAF folder import collisions Reject filesystem-equivalent destination names and avoid showing a failure when folder overwrite is skipped. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -222,6 +222,61 @@ pub fn need_fs_cm_send_files() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Android is scoped-storage only: the peer may never touch anything outside the app
|
||||
/// workspace (`Config::get_home()`, i.e. the app-specific external files directory).
|
||||
///
|
||||
/// Every peer supplied path must be validated with this before it reaches the
|
||||
/// filesystem, for reads, writes, renames, creations and deletions alike. The path is
|
||||
/// resolved to its canonical form (of the deepest existing ancestor, so paths that are
|
||||
/// about to be created are handled too) so symlinks cannot escape the workspace.
|
||||
///
|
||||
/// Only the `ReadDir` protocol action treats an empty path as the home directory.
|
||||
/// Callers must opt in to that protocol-specific behavior with `allow_empty`.
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn is_peer_path_allowed(path: &str, allow_empty: bool) -> bool {
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
// Canonicalize the deepest existing ancestor and re-append the missing tail.
|
||||
fn resolve(path: &Path) -> Option<PathBuf> {
|
||||
let mut tail: Vec<std::ffi::OsString> = Vec::new();
|
||||
let mut base = path.to_path_buf();
|
||||
loop {
|
||||
if let Ok(mut resolved) = base.canonicalize() {
|
||||
while let Some(component) = tail.pop() {
|
||||
resolved.push(component);
|
||||
}
|
||||
return Some(resolved);
|
||||
}
|
||||
tail.push(base.file_name()?.to_os_string());
|
||||
if !base.pop() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_empty() {
|
||||
return allow_empty;
|
||||
}
|
||||
let path = Path::new(path);
|
||||
// `..` is never needed by the protocol and would defeat the prefix check below.
|
||||
if !path.is_absolute() || path.components().any(|c| c == Component::ParentDir) {
|
||||
return false;
|
||||
}
|
||||
let home = Config::get_home();
|
||||
let home = home.canonicalize().unwrap_or(home);
|
||||
if home.as_os_str().is_empty() {
|
||||
return false;
|
||||
}
|
||||
// `Path::starts_with` compares whole components, and is true for equal paths.
|
||||
resolve(path).map_or(false, |target| target.starts_with(&home))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn is_peer_path_allowed(_path: &str, _allow_empty: bool) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_main() -> bool {
|
||||
*IS_MAIN
|
||||
|
||||
Reference in New Issue
Block a user