diff --git a/Cargo.toml b/Cargo.toml index 6b6f62615..8e1735690 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,6 @@ edition = "2018" [features] default = [] webrtc = ["dep:webrtc"] -# The isolated Wayland socket-probe fallback (src/platform/linux/wayland_probe.rs). Off by default -# so the base Wayland enumeration is untouched; the DRM login-screen build (scrap/drm) turns it on. -wayland_probe = [] [dependencies] # new flexi_logger failed on rustc 1.75 @@ -35,12 +32,10 @@ serde_json = "1.0" lazy_static = "1.5" confy = { git = "https://github.com/rustdesk-org/confy" } dirs-next = "2.0" -filetime = "0.2" sodiumoxide = "0.2" regex = "1.11" tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" } chrono = "0.4" -backtrace = "0.3" libc = "0.2" dlopen = "0.1" toml = "0.7" @@ -88,21 +83,7 @@ protobuf-codegen = { version = "3.7" } clap = "4.5.51" webrtc = "0.13.0" # keep in lockstep with [dependencies] webrtc (rustc 1.75 pin, see above) -[target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "0.3", features = [ - "winuser", - "synchapi", - "pdh", - "memoryapi", - "sysinfoapi", -] } - -[target.'cfg(target_os = "macos")'.dependencies] -osascript = "0.3" [target.'cfg(target_os = "linux")'.dependencies] -sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-features = false, features = [ - "calloop", -] } users = { version = "0.11" } x11 = "2.21" diff --git a/build.rs b/build.rs index 5ebc3a287..43e328243 100644 --- a/build.rs +++ b/build.rs @@ -6,7 +6,7 @@ fn main() { protobuf_codegen::Codegen::new() .pure() .out_dir(out_dir) - .inputs(["protos/rendezvous.proto", "protos/message.proto"]) + .inputs(["protos/rendezvous.proto"]) .include("protos") .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() diff --git a/examples/system_message.rs b/examples/system_message.rs deleted file mode 100644 index 0be788428..000000000 --- a/examples/system_message.rs +++ /dev/null @@ -1,20 +0,0 @@ -extern crate hbb_common; -#[cfg(target_os = "linux")] -use hbb_common::platform::linux; -#[cfg(target_os = "macos")] -use hbb_common::platform::macos; - -fn main() { - #[cfg(target_os = "linux")] - let res = linux::system_message("test title", "test message", true); - #[cfg(target_os = "macos")] - let res = macos::alert( - "System Preferences".to_owned(), - "warning".to_owned(), - "test title".to_owned(), - "test message".to_owned(), - ["Ok".to_owned()].to_vec(), - ); - #[cfg(any(target_os = "linux", target_os = "macos"))] - println!("result {:?}", &res); -} diff --git a/protos/message.proto b/protos/message.proto deleted file mode 100644 index 7ede2adce..000000000 --- a/protos/message.proto +++ /dev/null @@ -1,1032 +0,0 @@ -syntax = "proto3"; -package hbb; - -message EncodedVideoFrame { - bytes data = 1; - bool key = 2; - int64 pts = 3; -} - -message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } - -message RGB { bool compress = 1; } - -// planes data send directly in binary for better use arraybuffer on web -message YUV { - bool compress = 1; - int32 stride = 2; -} - -enum Chroma { - I420 = 0; - I444 = 1; -} - -message VideoFrame { - oneof union { - EncodedVideoFrames vp9s = 6; - RGB rgb = 7; - YUV yuv = 8; - EncodedVideoFrames h264s = 10; - EncodedVideoFrames h265s = 11; - EncodedVideoFrames vp8s = 12; - EncodedVideoFrames av1s = 13; - } - int32 display = 14; -} - -message IdPk { - string id = 1; - bytes pk = 2; - // DTLS certificate fingerprint of the signer's WebRTC endpoint, signed together with id/pk so - // a WebRTC peer's DTLS channel can be bound to its verified identity (defeats a rendezvous/relay - // that swaps SDP fingerprints). Empty for non-WebRTC handshakes. - string dtls_fingerprint = 3; -} - -message DisplayInfo { - sint32 x = 1; - sint32 y = 2; - int32 width = 3; - int32 height = 4; - string name = 5; - bool online = 6; - bool cursor_embedded = 7; - Resolution original_resolution = 8; - double scale = 9; -} - -message PortForward { - string host = 1; - int32 port = 2; - bool multiplex = 3; -} - -message FileTransfer { - string dir = 1; - bool show_hidden = 2; -} - -message ViewCamera {} - -message OSLogin { - string username = 1; - string password = 2; -} - -message LoginRequest { - string username = 1; - bytes password = 2; - string my_id = 4; - string my_name = 5; - OptionMessage option = 6; - oneof union { - FileTransfer file_transfer = 7; - PortForward port_forward = 8; - ViewCamera view_camera = 15; - Terminal terminal = 16; - } - bool video_ack_required = 9; - uint64 session_id = 10; - string version = 11; - OSLogin os_login = 12; - string my_platform = 13; - bytes hwid = 14; - string avatar = 17; -} - -message Terminal { - string service_id = 1; // Service ID for reconnecting to existing session -} - -message Auth2FA { - string code = 1; - bytes hwid = 2; -} - -message ChatMessage { string text = 1; } - -message Features { - bool privacy_mode = 1; - bool terminal = 2; - bool port_forward_mux = 3; -} - -message CodecAbility { - bool vp8 = 1; - bool vp9 = 2; - bool av1 = 3; - bool h264 = 4; - bool h265 = 5; -} - -message SupportedEncoding { - bool h264 = 1; - bool h265 = 2; - bool vp8 = 3; - bool av1 = 4; - CodecAbility i444 = 5; -} - -message PeerInfo { - string username = 1; - string hostname = 2; - string platform = 3; - repeated DisplayInfo displays = 4; - int32 current_display = 5; - bool sas_enabled = 6; - string version = 7; - Features features = 9; - SupportedEncoding encoding = 10; - SupportedResolutions resolutions = 11; - // Use JSON's key-value format which is friendly for peer to handle. - // NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string. - string platform_additions = 12; - WindowsSessions windows_sessions = 13; -} - -message WindowsSession { - uint32 sid = 1; - string name = 2; -} - -message LoginResponse { - oneof union { - string error = 1; - PeerInfo peer_info = 2; - } - bool enable_trusted_devices = 3; -} - -message TouchScaleUpdate { - // The delta scale factor relative to the previous scale. - // delta * 1000 - // 0 means scale end - int32 scale = 1; -} - -message TouchPanStart { - int32 x = 1; - int32 y = 2; -} - -message TouchPanUpdate { - // The delta x position relative to the previous position. - int32 x = 1; - // The delta y position relative to the previous position. - int32 y = 2; -} - -message TouchPanEnd { - int32 x = 1; - int32 y = 2; -} - -message TouchEvent { - oneof union { - TouchScaleUpdate scale_update = 1; - TouchPanStart pan_start = 2; - TouchPanUpdate pan_update = 3; - TouchPanEnd pan_end = 4; - } -} - -message PointerDeviceEvent { - oneof union { - TouchEvent touch_event = 1; - } - repeated ControlKey modifiers = 2; -} - -message MouseEvent { - int32 mask = 1; - sint32 x = 2; - sint32 y = 3; - repeated ControlKey modifiers = 4; -} - -enum KeyboardMode{ - Legacy = 0; - Map = 1; - Translate = 2; - Auto = 3; -} - -enum ControlKey { - Unknown = 0; - Alt = 1; - Backspace = 2; - CapsLock = 3; - Control = 4; - Delete = 5; - DownArrow = 6; - End = 7; - Escape = 8; - F1 = 9; - F10 = 10; - F11 = 11; - F12 = 12; - F2 = 13; - F3 = 14; - F4 = 15; - F5 = 16; - F6 = 17; - F7 = 18; - F8 = 19; - F9 = 20; - Home = 21; - LeftArrow = 22; - /// meta key (also known as "windows"; "super"; and "command") - Meta = 23; - /// option key on macOS (alt key on Linux and Windows) - Option = 24; // deprecated, use Alt instead - PageDown = 25; - PageUp = 26; - Return = 27; - RightArrow = 28; - Shift = 29; - Space = 30; - Tab = 31; - UpArrow = 32; - Numpad0 = 33; - Numpad1 = 34; - Numpad2 = 35; - Numpad3 = 36; - Numpad4 = 37; - Numpad5 = 38; - Numpad6 = 39; - Numpad7 = 40; - Numpad8 = 41; - Numpad9 = 42; - Cancel = 43; - Clear = 44; - Menu = 45; // deprecated, use Alt instead - Pause = 46; - Kana = 47; - Hangul = 48; - Junja = 49; - Final = 50; - Hanja = 51; - Kanji = 52; - Convert = 53; - Select = 54; - Print = 55; - Execute = 56; - Snapshot = 57; - Insert = 58; - Help = 59; - Sleep = 60; - Separator = 61; - Scroll = 62; - NumLock = 63; - RWin = 64; - Apps = 65; - Multiply = 66; - Add = 67; - Subtract = 68; - Decimal = 69; - Divide = 70; - Equals = 71; - NumpadEnter = 72; - RShift = 73; - RControl = 74; - RAlt = 75; - VolumeMute = 76; // mainly used on mobile devices as controlled side - VolumeUp = 77; - VolumeDown = 78; - Power = 79; // mainly used on mobile devices as controlled side - CtrlAltDel = 100; - LockScreen = 101; -} - -message KeyEvent { - // `down` indicates the key's state(down or up). - bool down = 1; - // `press` indicates a click event(down and up). - bool press = 2; - oneof union { - ControlKey control_key = 3; - // position key code. win: scancode, linux: key code, macos: key code - uint32 chr = 4; - uint32 unicode = 5; - string seq = 6; - // high word. virtual keycode - // low word. unicode - uint32 win2win_hotkey = 7; - } - repeated ControlKey modifiers = 8; - KeyboardMode mode = 9; -} - -message CursorData { - uint64 id = 1; - sint32 hotx = 2; - sint32 hoty = 3; - int32 width = 4; - int32 height = 5; - bytes colors = 6; -} - -message CursorPosition { - sint32 x = 1; - sint32 y = 2; -} - -message Hash { - string salt = 1; - string challenge = 2; -} - -enum ClipboardFormat { - Text = 0; - Rtf = 1; - Html = 2; - ImageRgba = 21; - ImagePng = 22; - ImageSvg = 23; - Special = 31; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; - int32 width = 3; - int32 height = 4; - ClipboardFormat format = 5; - // Special format name, only used when format is Special. - string special_name = 6; -} - -message MultiClipboards { repeated Clipboard clipboards = 1; } - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadEmptyDirs { - string path = 1; - bool include_hidden = 2; -} - -message ReadEmptyDirsResponse { - string path = 1; - repeated FileDirectory empty_dirs = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileRename { - int32 id = 1; - string path = 2; - string new_name = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - FileRename rename = 10; - ReadEmptyDirs read_empty_dirs = 11; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - ReadEmptyDirsResponse empty_dirs = 6; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; - bool is_identical = 6; - uint64 transferred_size = 7; // For resume. Indicates the size of the file already transferred - bool is_resume = 8; // For resume. Indicates if the transfer is a resume. - // `is_resume` can let the controlled side know whether to check the `.digest` file. - // When `is_resume` is false, `.digest` exists, the same file does not exist, - // the controlled side should not check `.digest`, it should confirm with a new transfer request. -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; - - enum FileType { - Generic = 0; - Printer = 1; - } - FileType file_type = 5; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; - uint64 total_size = 5; -} - -message FileRemoveDir { - int32 id = 1; - string path = 2; - bool recursive = 3; -} - -message FileRemoveFile { - int32 id = 1; - string path = 2; - sint32 file_num = 3; -} - -message FileDirCreate { - int32 id = 1; - string path = 2; -} - -// main logic from freeRDP -message CliprdrMonitorReady { -} - -message CliprdrFormat { - int32 id = 2; - string format = 3; -} - -message CliprdrServerFormatList { - repeated CliprdrFormat formats = 2; -} - -message CliprdrServerFormatListResponse { - int32 msg_flags = 2; -} - -message CliprdrServerFormatDataRequest { - int32 requested_format_id = 2; -} - -message CliprdrServerFormatDataResponse { - int32 msg_flags = 2; - bytes format_data = 3; -} - -message CliprdrFileContentsRequest { - int32 stream_id = 2; - int32 list_index = 3; - int32 dw_flags = 4; - int32 n_position_low = 5; - int32 n_position_high = 6; - int32 cb_requested = 7; - bool have_clip_data_id = 8; - int32 clip_data_id = 9; -} - -message CliprdrFileContentsResponse { - int32 msg_flags = 3; - int32 stream_id = 4; - bytes requested_data = 5; -} - -// Try empty clipboard in the following case(Windows only): -// 1. `A`(Windows) -> `B`, `C` -// 2. Copy in `A, file clipboards on `B` and `C` are updated. -// 3. Copy in `B`. -// `A` should tell `C` to empty the file clipboard. -message CliprdrTryEmpty { -} - -// Clipobard file message for audit. -message CliprdrFile { - string name = 1; - uint64 size = 2; -} - -message CliprdrFiles { - repeated CliprdrFile files = 1; -} - -message Cliprdr { - oneof union { - CliprdrMonitorReady ready = 1; - CliprdrServerFormatList format_list = 2; - CliprdrServerFormatListResponse format_list_response = 3; - CliprdrServerFormatDataRequest format_data_request = 4; - CliprdrServerFormatDataResponse format_data_response = 5; - CliprdrFileContentsRequest file_contents_request = 6; - CliprdrFileContentsResponse file_contents_response = 7; - CliprdrTryEmpty try_empty = 8; - CliprdrFiles files = 9; - } -} - -message Resolution { - int32 width = 1; - int32 height = 2; -} - -message DisplayResolution { - int32 display = 1; - Resolution resolution = 2; -} - -message SupportedResolutions { repeated Resolution resolutions = 1; } - -message SwitchDisplay { - int32 display = 1; - sint32 x = 2; - sint32 y = 3; - int32 width = 4; - int32 height = 5; - bool cursor_embedded = 6; - SupportedResolutions resolutions = 7; - // Do not care about the origin point for now. - Resolution original_resolution = 8; -} - -message CaptureDisplays { - repeated int32 add = 1; - repeated int32 sub = 2; - repeated int32 set = 3; -} - -message ToggleVirtualDisplay { - int32 display = 1; - bool on = 2; -} - -message TogglePrivacyMode { - string impl_key = 1; - bool on = 2; -} - -message PermissionInfo { - enum Permission { - Keyboard = 0; - Clipboard = 2; - Audio = 3; - File = 4; - Restart = 5; - Recording = 6; - BlockInput = 7; - PrivacyMode = 8; - } - - Permission permission = 1; - bool enabled = 2; -} - -enum ImageQuality { - NotSet = 0; - Low = 2; - Balanced = 3; - Best = 4; -} - -message SupportedDecoding { - enum PreferCodec { - Auto = 0; - VP9 = 1; - H264 = 2; - H265 = 3; - VP8 = 4; - AV1 = 5; - } - - int32 ability_vp9 = 1; - int32 ability_h264 = 2; - int32 ability_h265 = 3; - PreferCodec prefer = 4; - int32 ability_vp8 = 5; - int32 ability_av1 = 6; - CodecAbility i444 = 7; - Chroma prefer_chroma = 8; -} - -message OptionMessage { - enum BoolOption { - NotSet = 0; - No = 1; - Yes = 2; - } - ImageQuality image_quality = 1; - BoolOption lock_after_session_end = 2; - BoolOption show_remote_cursor = 3; - BoolOption privacy_mode = 4; - BoolOption block_input = 5; - int32 custom_image_quality = 6; - BoolOption disable_audio = 7; - BoolOption disable_clipboard = 8; - BoolOption enable_file_transfer = 9; - SupportedDecoding supported_decoding = 10; - int32 custom_fps = 11; - BoolOption disable_keyboard = 12; -// Position 13 is used for Resolution. Remove later. -// Resolution custom_resolution = 13; -// BoolOption support_windows_specific_session = 14; - // starting from 15 please, do not use removed fields - BoolOption follow_remote_cursor = 15; - BoolOption follow_remote_window = 16; - BoolOption disable_camera = 17; - BoolOption terminal_persistent = 18; - BoolOption show_my_cursor = 19; -} - -message TestDelay { - int64 time = 1; - bool from_client = 2; - uint32 last_delay = 3; - uint32 target_bitrate = 4; -} - -message PublicKey { - bytes asymmetric_value = 1; - bytes symmetric_value = 2; -} - -message SignedId { bytes id = 1; } - -message AudioFormat { - uint32 sample_rate = 1; - uint32 channels = 2; -} - -message AudioFrame { - bytes data = 1; -} - -// Notify peer to show message box. -message MessageBox { - // Message type. Refer to flutter/lib/common.dart/msgBox(). - string msgtype = 1; - string title = 2; - // English - string text = 3; - // If not empty, msgbox provides a button to following the link. - // The link here can't be directly http url. - // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). - string link = 4; -} - -message BackNotification { - // no need to consider block input by someone else - enum BlockInputState { - BlkStateUnknown = 0; - BlkOnSucceeded = 2; - BlkOnFailed = 3; - BlkOffSucceeded = 4; - BlkOffFailed = 5; - } - enum PrivacyModeState { - PrvStateUnknown = 0; - // Privacy mode on by someone else - PrvOnByOther = 2; - // Privacy mode is not supported on the remote side - PrvNotSupported = 3; - // Privacy mode on by self - PrvOnSucceeded = 4; - // Privacy mode on by self, but denied - PrvOnFailedDenied = 5; - // Some plugins are not found - PrvOnFailedPlugin = 6; - // Privacy mode on by self, but failed - PrvOnFailed = 7; - // Privacy mode off by self - PrvOffSucceeded = 8; - // Ctrl + P - PrvOffByPeer = 9; - // Privacy mode off by self, but failed - PrvOffFailed = 10; - PrvOffUnknown = 11; - } - - oneof union { - PrivacyModeState privacy_mode_state = 1; - BlockInputState block_input_state = 2; - } - // Supplementary message, for "PrvOnFailed" and "PrvOffFailed" - string details = 3; - // The key of the implementation - string impl_key = 4; -} - -message ElevationRequestWithLogon { - string username = 1; - string password = 2; -} - -message ElevationRequest { - oneof union { - bool direct = 1; - ElevationRequestWithLogon logon = 2; - } -} - -message SwitchSidesRequest { - bytes uuid = 1; -} - -message SwitchSidesResponse { - bytes uuid = 1; - LoginRequest lr = 2; -} - -message SwitchBack {} - -message PluginRequest { - string id = 1; - bytes content = 2; -} - -message PluginFailure { - string id = 1; - string name = 2; - string msg = 3; -} - -message WindowsSessions { - repeated WindowsSession sessions = 1; - uint32 current_sid = 2; -} - -// Query messages from peer. -message MessageQuery { - // The SwitchDisplay message of the target display. - // If the target display is not found, the message will be ignored. - int32 switch_display = 1; -} - -message Misc { - oneof union { - ChatMessage chat_message = 4; - SwitchDisplay switch_display = 5; - PermissionInfo permission_info = 6; - OptionMessage option = 7; - AudioFormat audio_format = 8; - string close_reason = 9; - bool refresh_video = 10; - bool video_received = 12; - BackNotification back_notification = 13; - bool restart_remote_device = 14; - bool uac = 15; - bool foreground_window_elevated = 16; - bool stop_service = 17; - ElevationRequest elevation_request = 18; - string elevation_response = 19; - bool portable_service_running = 20; - SwitchSidesRequest switch_sides_request = 21; - SwitchBack switch_back = 22; - // Deprecated since 1.2.4, use `change_display_resolution` (36) instead. - // But we must keep it for compatibility when peer version < 1.2.4. - Resolution change_resolution = 24; - PluginRequest plugin_request = 25; - PluginFailure plugin_failure = 26; - uint32 full_speed_fps = 27; // deprecated - uint32 auto_adjust_fps = 28; - bool client_record_status = 29; - CaptureDisplays capture_displays = 30; - int32 refresh_video_display = 31; - ToggleVirtualDisplay toggle_virtual_display = 32; - TogglePrivacyMode toggle_privacy_mode = 33; - SupportedEncoding supported_encoding = 34; - uint32 selected_sid = 35; - DisplayResolution change_display_resolution = 36; - MessageQuery message_query = 37; - int32 follow_current_display = 38; - } -} - -message VoiceCallRequest { - int64 req_timestamp = 1; - // Indicates whether the request is a connect action or a disconnect action. - bool is_connect = 2; -} - -message VoiceCallResponse { - bool accepted = 1; - int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. - int64 ack_timestamp = 3; -} - -message ScreenshotRequest { - int32 display = 1; - // sid is the session id on the controlling side - // It is used to forward the message to the correct remote (session) window. - string sid = 2; -} - -message ScreenshotResponse { - string sid = 1; - // empty if success - string msg = 2; - bytes data = 3; -} - -// Terminal messages - standalone feature like FileAction -message OpenTerminal { - int32 terminal_id = 1; // 0 for default terminal - uint32 rows = 2; - uint32 cols = 3; -} - -message ResizeTerminal { - int32 terminal_id = 1; - uint32 rows = 2; - uint32 cols = 3; -} - -message TerminalData { - int32 terminal_id = 1; - bytes data = 2; - bool compressed = 3; -} - -message CloseTerminal { - int32 terminal_id = 1; -} - -message TerminalAction { - oneof union { - OpenTerminal open = 1; - TerminalData data = 2; - ResizeTerminal resize = 3; - CloseTerminal close = 4; - } -} - -message TerminalOpened { - int32 terminal_id = 1; - bool success = 2; - string message = 3; - uint32 pid = 4; - string service_id = 5; // Service ID for persistent sessions - repeated int32 persistent_sessions = 6; // Used to restore the persistent sessions. - bool replay_terminal_output = 7; // Whether the next data response replays buffered terminal output. -} - -message TerminalClosed { - int32 terminal_id = 1; - int32 exit_code = 2; -} - -message TerminalError { - int32 terminal_id = 1; - string message = 2; -} - -message TerminalResponse { - oneof union { - TerminalOpened opened = 1; - TerminalData data = 2; - TerminalClosed closed = 3; - TerminalError error = 4; - } -} - -message PortForwardOpen { - int32 channel_id = 1; - string host = 2; - int32 port = 3; - uint32 window = 4; -} - -message PortForwardOpened { - int32 channel_id = 1; - bool success = 2; - string message = 3; - uint32 window = 4; -} - -message PortForwardData { - int32 channel_id = 1; - bytes data = 2; -} - -message PortForwardClose { - int32 channel_id = 1; -} - -message PortForwardWindowUpdate { - int32 channel_id = 1; - uint32 add = 2; -} - -// One symmetric message for both directions. `open` only travels controller -> controlled, -// `opened` only controlled -> controller; a frame in the wrong direction is ignored. -message PortForwardChannel { - oneof union { - PortForwardOpen open = 1; - PortForwardOpened opened = 2; - PortForwardData data = 3; - PortForwardClose close = 4; - PortForwardWindowUpdate window_update = 5; - } -} - -message Message { - oneof union { - SignedId signed_id = 3; - PublicKey public_key = 4; - TestDelay test_delay = 5; - VideoFrame video_frame = 6; - LoginRequest login_request = 7; - LoginResponse login_response = 8; - Hash hash = 9; - MouseEvent mouse_event = 10; - AudioFrame audio_frame = 11; - CursorData cursor_data = 12; - CursorPosition cursor_position = 13; - uint64 cursor_id = 14; - KeyEvent key_event = 15; - Clipboard clipboard = 16; - FileAction file_action = 17; - FileResponse file_response = 18; - Misc misc = 19; - Cliprdr cliprdr = 20; - MessageBox message_box = 21; - SwitchSidesResponse switch_sides_response = 22; - VoiceCallRequest voice_call_request = 23; - VoiceCallResponse voice_call_response = 24; - PeerInfo peer_info = 25; - PointerDeviceEvent pointer_device_event = 26; - Auth2FA auth_2fa = 27; - MultiClipboards multi_clipboards = 28; - ScreenshotRequest screenshot_request = 29; - ScreenshotResponse screenshot_response= 30; - TerminalAction terminal_action = 31; - TerminalResponse terminal_response = 32; - PortForwardChannel port_forward_channel = 33; - } -} diff --git a/protos/rendezvous.proto b/protos/rendezvous.proto index 8f796502f..f87882385 100644 --- a/protos/rendezvous.proto +++ b/protos/rendezvous.proto @@ -1,6 +1,15 @@ syntax = "proto3"; package hbb; +message IdPk { + string id = 1; + bytes pk = 2; + // DTLS certificate fingerprint of the signer's WebRTC endpoint, signed together with id/pk so + // a WebRTC peer's DTLS channel can be bound to its verified identity (defeats a rendezvous/relay + // that swaps SDP fingerprints). Empty for non-WebRTC handshakes. + string dtls_fingerprint = 3; +} + message RegisterPeer { string id = 1; int32 serial = 2; diff --git a/src/config.rs b/src/config.rs index 0d995ea67..6fe100455 100644 --- a/src/config.rs +++ b/src/config.rs @@ -471,10 +471,10 @@ fn patch(path: PathBuf) -> PathBuf { #[cfg(target_os = "linux")] { if _tmp == "/root" { - if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") { + if let Ok(user) = crate::sh::run_cmds_trim_newline("whoami") { if user != "root" { let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user); - if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) { + if let Ok(output) = crate::sh::run_cmds_trim_newline(&cmd) { return output.into(); } return format!("/home/{user}").into(); @@ -755,9 +755,9 @@ impl Config { /// /// **DO NOT use this function in privileged contexts** (e.g., code executed via /// `gtk_sudo` or system services running as root). For privileged operations on - /// Linux, use `crate::platform::linux::get_home_dir_trusted()` which bypasses - /// the `$HOME` environment variable and queries the system password database - /// directly via `getpwuid`. + /// Linux, use `get_home_dir_trusted()` -- a `getpwuid`-based lookup that + /// bypasses the `$HOME` environment variable and queries the system password + /// database directly. It lives with the platform code in the client tree. /// /// Using `$HOME` in privileged contexts creates a confused-deputy vulnerability /// where an attacker can manipulate the environment variable to inject malicious @@ -2848,21 +2848,10 @@ pub fn allow_insecure_tls_fallback() -> bool { } pub mod keys { - pub const OPTION_VIEW_ONLY: &str = "view_only"; - pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar"; + // Only the keys hbb_common itself references. pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_toolbar"; - pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor"; - pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor"; - pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window"; pub const OPTION_ZOOM_CURSOR: &str = "zoom-cursor"; - pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor"; - pub const OPTION_DISABLE_AUDIO: &str = "disable_audio"; - pub const OPTION_ENABLE_REMOTE_PRINTER: &str = "enable-remote-printer"; pub const OPTION_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste"; - pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard"; - pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end"; - pub const OPTION_PRIVACY_MODE: &str = "privacy_mode"; - pub const OPTION_TOUCH_MODE: &str = "touch-mode"; pub const OPTION_I444: &str = "i444"; pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel"; pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse"; @@ -2876,380 +2865,27 @@ pub mod keys { pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; - pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; - pub const OPTION_THEME: &str = "theme"; pub const OPTION_LANGUAGE: &str = "lang"; - pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; - pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right"; - pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel"; - pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs"; - pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str = - "enable-open-new-connections-in-tabs"; - pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render"; - // Internal health record written by the texture-render watchdog/probe; - // "failed-*" flips the texture-render default to opt-in on this machine. - pub const OPTION_TEXTURE_RENDER_HEALTH: &str = "texture-render-health"; - pub const OPTION_ALLOW_D3D_RENDER: &str = "allow-d3d-render"; - pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update"; - pub const OPTION_ALLOW_AUTO_UPDATE: &str = "allow-auto-update"; - pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions"; - pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags"; - pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection"; - pub const OPTION_ACCESS_MODE: &str = "access-mode"; - pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard"; - pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard"; - pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer"; - pub const OPTION_ENABLE_CAMERA: &str = "enable-camera"; - pub const OPTION_ENABLE_TERMINAL: &str = "enable-terminal"; - pub const OPTION_TERMINAL_PERSISTENT: &str = "terminal-persistent"; - pub const OPTION_ENABLE_AUDIO: &str = "enable-audio"; - pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel"; - pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart"; - pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session"; - pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input"; - pub const OPTION_ENABLE_PRIVACY_MODE: &str = "enable-privacy-mode"; - pub const OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW: &str = - "enable-perm-change-in-accept-window"; - pub const OPTION_ALLOW_SCOPE_VIOLATION_CLOSE: &str = "allow-scope-violation-close"; - pub const OPTION_ALLOW_SCOPE_VIOLATION_ALARM: &str = "allow-scope-violation-alarm"; - pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification"; pub const OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD: &str = "allow-numeric-one-time-password"; - pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery"; pub const OPTION_DIRECT_SERVER: &str = "direct-server"; - pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port"; - pub const OPTION_WHITELIST: &str = "whitelist"; - pub const OPTION_ID_WHITELIST: &str = "id-whitelist"; - pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect"; - pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout"; - pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open"; - pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming"; - pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing"; - pub const OPTION_HIDE_RECORDING_BUTTON: &str = "hide-recording-button"; - pub const OPTION_WINDOWS_SERVICE_VIDEO_SAVE_DIRECTORY: &str = - "windows-service-video-save-directory"; - pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory"; - pub const OPTION_ENABLE_ABR: &str = "enable-abr"; - pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper"; - pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render"; - pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec"; - pub const OPTION_APPROVE_MODE: &str = "approve-mode"; - pub const OPTION_VERIFICATION_METHOD: &str = "verification-method"; - pub const OPTION_TEMPORARY_PASSWORD_LENGTH: &str = "temporary-password-length"; - pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server"; - pub const OPTION_API_SERVER: &str = "api-server"; - pub const OPTION_KEY: &str = "key"; pub const OPTION_ALLOW_WEBSOCKET: &str = "allow-websocket"; - pub const OPTION_PRESET_ADDRESS_BOOK_NAME: &str = "preset-address-book-name"; - pub const OPTION_PRESET_ADDRESS_BOOK_TAG: &str = "preset-address-book-tag"; - pub const OPTION_PRESET_ADDRESS_BOOK_ALIAS: &str = "preset-address-book-alias"; - pub const OPTION_PRESET_ADDRESS_BOOK_PASSWORD: &str = "preset-address-book-password"; - pub const OPTION_PRESET_ADDRESS_BOOK_NOTE: &str = "preset-address-book-note"; - pub const OPTION_PRESET_DEVICE_USERNAME: &str = "preset-device-username"; - pub const OPTION_PRESET_DEVICE_NAME: &str = "preset-device-name"; - pub const OPTION_PRESET_NOTE: &str = "preset-note"; - pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture"; - pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = - "enable-android-software-encoding-half-scale"; - pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices"; - pub const OPTION_AV1_TEST: &str = "av1-test"; pub const OPTION_TRACKPAD_SPEED: &str = "trackpad-speed"; pub const OPTION_REGISTER_DEVICE: &str = "register-device"; pub const OPTION_RELAY_SERVER: &str = "relay-server"; pub const OPTION_ICE_SERVERS: &str = "ice-servers"; - /// Maximum number of files allowed during a single file transfer request. - /// - /// Key: `file-transfer-max-files`. - /// Unit: number of files (not bytes). - /// - /// Behaviour: - /// - If set to a positive integer N, at most N files are allowed. - /// - If set to 0, a safe built-in default is used (see DEFAULT_MAX_VALIDATED_FILES). - /// - If unset, negative, or non-integer, no explicit limit is enforced for backward compatibility. - pub const OPTION_FILE_TRANSFER_MAX_FILES: &str = "file-transfer-max-files"; - pub const OPTION_DISABLE_UDP: &str = "disable-udp"; pub const OPTION_ALLOW_INSECURE_TLS_FALLBACK: &str = "allow-insecure-tls-fallback"; - pub const OPTION_SHOW_VIRTUAL_MOUSE: &str = "show-virtual-mouse"; - // joystick is the virtual mouse. - // So `OPTION_SHOW_VIRTUAL_MOUSE` should also be set if `OPTION_SHOW_VIRTUAL_JOYSTICK` is set. - pub const OPTION_SHOW_VIRTUAL_JOYSTICK: &str = "show-virtual-joystick"; - pub const OPTION_ENABLE_FLUTTER_HTTP_ON_RUST: &str = "enable-flutter-http-on-rust"; - pub const OPTION_ALLOW_ASK_FOR_NOTE: &str = "allow-ask-for-note"; - - // built-in options - pub const OPTION_DISPLAY_NAME: &str = "display-name"; - pub const OPTION_AVATAR: &str = "avatar"; - pub const OPTION_PRESET_DEVICE_GROUP_NAME: &str = "preset-device-group-name"; - pub const OPTION_PRESET_USERNAME: &str = "preset-user-name"; - pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name"; - pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning"; - pub const OPTION_HIDE_GENERAL_SETTINGS: &str = "hide-general-settings"; - pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings"; - pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings"; - pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings"; - pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings"; - pub const OPTION_HIDE_REMOTE_PRINTER_SETTINGS: &str = "hide-remote-printer-settings"; - pub const OPTION_HIDE_WEBSOCKET_SETTINGS: &str = "hide-websocket-settings"; - pub const OPTION_HIDE_STOP_SERVICE: &str = "hide-stop-service"; - pub const OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED: &str = - "allow-command-line-settings-when-settings-disabled"; - - // Connection punch-through / port-forward options - pub const OPTION_ENABLE_TCP_PUNCH: &str = "enable-tcp-punch"; - pub const OPTION_ENABLE_UDP_PUNCH: &str = "enable-udp-punch"; - pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-punch"; - pub const OPTION_ENABLE_PORT_FORWARD_MUX: &str = "enable-port-forward-mux"; - pub const OPTION_ENABLE_WEBRTC: &str = "enable-webrtc"; - pub const OPTION_ALLOW_KCP_CC: &str = "allow-kcp-congestion-control"; pub const OPTION_ALLOW_WEBRTC_CC: &str = "allow-webrtc-congestion-control"; - pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card"; - pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; - pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; - pub const OPTION_HIDE_TRAY: &str = "hide-tray"; - pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; - pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; - pub const OPTION_ALLOW_DEEP_LINK_PASSWORD: &str = "allow-deep-link-password"; - pub const OPTION_ALLOW_DEEP_LINK_SERVER_SETTINGS: &str = "allow-deep-link-server-settings"; - pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; - pub const OPTION_ALLOW_HTTPS_21114: &str = "allow-https-21114"; - pub const OPTION_USE_RAW_TCP_FOR_API: &str = "use-raw-tcp-for-api"; pub const OPTION_ALLOW_HOSTNAME_AS_ID: &str = "allow-hostname-as-id"; - pub const OPTION_HIDE_POWERED_BY_ME: &str = "hide-powered-by-me"; - pub const OPTION_MAIN_WINDOW_ALWAYS_ON_TOP: &str = "main-window-always-on-top"; pub const OPTION_DISABLE_CHANGE_PERMANENT_PASSWORD: &str = "disable-change-permanent-password"; pub const OPTION_DISABLE_CHANGE_ID: &str = "disable-change-id"; pub const OPTION_DISABLE_UNLOCK_PIN: &str = "disable-unlock-pin"; - // flutter local options - pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; - pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting"; - pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index"; - pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order"; - pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible"; - pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type"; - pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name"; - pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification"; - pub const OPTION_ALLOW_SYNC_CLIPBOARD_BETWEEN_SESSIONS: &str = - "allow-sync-clipboard-between-sessions"; - - pub const OPTION_PRINTER_INCOMING_JOB_ACTION: &str = "printer-incomming-job-action"; - pub const OPTION_PRINTER_ALLOW_AUTO_PRINT: &str = "allow-printer-auto-print"; - pub const OPTION_PRINTER_SELECTED_NAME: &str = "printer-selected-name"; - - // android floating window options - pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window"; - pub const OPTION_FLOATING_WINDOW_SIZE: &str = "floating-window-size"; - pub const OPTION_FLOATING_WINDOW_UNTOUCHABLE: &str = "floating-window-untouchable"; - pub const OPTION_FLOATING_WINDOW_TRANSPARENCY: &str = "floating-window-transparency"; - pub const OPTION_FLOATING_WINDOW_SVG: &str = "floating-window-svg"; - - // android keep screen on - pub const OPTION_KEEP_SCREEN_ON: &str = "keep-screen-on"; - - // Server-side: keep host system awake during incoming sessions (Security setting) - pub const OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS: &str = - "keep-awake-during-incoming-sessions"; - - // Client-side: keep client system awake during outgoing sessions (General setting) - pub const OPTION_KEEP_AWAKE_DURING_OUTGOING_SESSIONS: &str = - "keep-awake-during-outgoing-sessions"; - - pub const OPTION_DISABLE_GROUP_PANEL: &str = "disable-group-panel"; - pub const OPTION_DISABLE_DISCOVERY_PANEL: &str = "disable-discovery-panel"; - pub const OPTION_PRE_ELEVATE_SERVICE: &str = "pre-elevate-service"; - // proxy settings // The following options are not real keys, they are just used for custom client advanced settings. // The real keys are in Config2::socks. pub const OPTION_PROXY_URL: &str = "proxy-url"; pub const OPTION_PROXY_USERNAME: &str = "proxy-username"; pub const OPTION_PROXY_PASSWORD: &str = "proxy-password"; - - // DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS - pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[ - OPTION_VIEW_ONLY, - OPTION_SHOW_MONITORS_TOOLBAR, - OPTION_COLLAPSE_TOOLBAR, - OPTION_SHOW_REMOTE_CURSOR, - OPTION_FOLLOW_REMOTE_CURSOR, - OPTION_FOLLOW_REMOTE_WINDOW, - OPTION_ZOOM_CURSOR, - OPTION_SHOW_QUALITY_MONITOR, - OPTION_DISABLE_AUDIO, - OPTION_ENABLE_FILE_COPY_PASTE, - OPTION_DISABLE_CLIPBOARD, - OPTION_LOCK_AFTER_SESSION_END, - OPTION_PRIVACY_MODE, - OPTION_TOUCH_MODE, - OPTION_I444, - OPTION_REVERSE_MOUSE_WHEEL, - OPTION_SWAP_LEFT_RIGHT_MOUSE, - OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS, - OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION, - OPTION_VIEW_STYLE, - OPTION_TERMINAL_PERSISTENT, - OPTION_SCROLL_STYLE, - OPTION_EDGE_SCROLL_EDGE_THICKNESS, - OPTION_IMAGE_QUALITY, - OPTION_CUSTOM_IMAGE_QUALITY, - OPTION_CUSTOM_FPS, - OPTION_CODEC_PREFERENCE, - OPTION_SYNC_INIT_CLIPBOARD, - OPTION_TRACKPAD_SPEED, - ]; - // DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS - pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ - OPTION_THEME, - OPTION_LANGUAGE, - OPTION_ENABLE_CONFIRM_CLOSING_TABS, - OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS, - OPTION_TEXTURE_RENDER, - OPTION_ALLOW_D3D_RENDER, - OPTION_SYNC_AB_WITH_RECENT_SESSIONS, - OPTION_SYNC_AB_TAGS, - OPTION_FILTER_AB_BY_INTERSECTION, - OPTION_REMOTE_MENUBAR_DRAG_LEFT, - OPTION_REMOTE_MENUBAR_DRAG_RIGHT, - OPTION_HIDE_AB_TAGS_PANEL, - OPTION_FLUTTER_REMOTE_MENUBAR_STATE, - OPTION_FLUTTER_PEER_SORTING, - OPTION_FLUTTER_PEER_TAB_INDEX, - OPTION_FLUTTER_PEER_TAB_ORDER, - OPTION_FLUTTER_PEER_TAB_VISIBLE, - OPTION_FLUTTER_PEER_CARD_UI_TYLE, - OPTION_FLUTTER_CURRENT_AB_NAME, - OPTION_DISABLE_FLOATING_WINDOW, - OPTION_FLOATING_WINDOW_SIZE, - OPTION_FLOATING_WINDOW_UNTOUCHABLE, - OPTION_FLOATING_WINDOW_TRANSPARENCY, - OPTION_FLOATING_WINDOW_SVG, - OPTION_KEEP_SCREEN_ON, - // Client-side: keep client system awake during outgoing sessions (General setting) - OPTION_KEEP_AWAKE_DURING_OUTGOING_SESSIONS, - OPTION_DISABLE_GROUP_PANEL, - OPTION_DISABLE_DISCOVERY_PANEL, - OPTION_PRE_ELEVATE_SERVICE, - OPTION_ALLOW_REMOTE_CM_MODIFICATION, - OPTION_ALLOW_SYNC_CLIPBOARD_BETWEEN_SESSIONS, - OPTION_ENABLE_CHECK_UPDATE, - OPTION_PRINTER_INCOMING_JOB_ACTION, - OPTION_PRINTER_ALLOW_AUTO_PRINT, - OPTION_PRINTER_SELECTED_NAME, - OPTION_ALLOW_AUTO_RECORD_OUTGOING, - OPTION_HIDE_RECORDING_BUTTON, - OPTION_VIDEO_SAVE_DIRECTORY, - OPTION_ENABLE_TCP_PUNCH, - OPTION_ENABLE_UDP_PUNCH, - OPTION_ENABLE_IPV6_PUNCH, - OPTION_ENABLE_PORT_FORWARD_MUX, - OPTION_ENABLE_WEBRTC, - OPTION_TOUCH_MODE, - OPTION_SHOW_VIRTUAL_MOUSE, - OPTION_SHOW_VIRTUAL_JOYSTICK, - OPTION_ENABLE_FLUTTER_HTTP_ON_RUST, - OPTION_ALLOW_ASK_FOR_NOTE, - ]; - // DEFAULT_SETTINGS, OVERWRITE_SETTINGS - pub const KEYS_SETTINGS: &[&str] = &[ - OPTION_ACCESS_MODE, - OPTION_ENABLE_KEYBOARD, - OPTION_ENABLE_CLIPBOARD, - OPTION_ENABLE_FILE_TRANSFER, - OPTION_ENABLE_CAMERA, - OPTION_ENABLE_TERMINAL, - OPTION_ENABLE_REMOTE_PRINTER, - OPTION_ENABLE_AUDIO, - OPTION_ENABLE_TUNNEL, - OPTION_ENABLE_REMOTE_RESTART, - OPTION_ENABLE_RECORD_SESSION, - OPTION_ENABLE_BLOCK_INPUT, - OPTION_ENABLE_PRIVACY_MODE, - OPTION_ALLOW_SCOPE_VIOLATION_CLOSE, - OPTION_ALLOW_SCOPE_VIOLATION_ALARM, - OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION, - OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD, - OPTION_ENABLE_LAN_DISCOVERY, - OPTION_DIRECT_SERVER, - OPTION_DIRECT_ACCESS_PORT, - OPTION_WHITELIST, - OPTION_ID_WHITELIST, - OPTION_ALLOW_AUTO_DISCONNECT, - OPTION_AUTO_DISCONNECT_TIMEOUT, - OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, - OPTION_ALLOW_AUTO_RECORD_INCOMING, - OPTION_WINDOWS_SERVICE_VIDEO_SAVE_DIRECTORY, - OPTION_ENABLE_ABR, - OPTION_ALLOW_REMOVE_WALLPAPER, - OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, - OPTION_ENABLE_HWCODEC, - OPTION_APPROVE_MODE, - OPTION_VERIFICATION_METHOD, - OPTION_TEMPORARY_PASSWORD_LENGTH, - OPTION_PROXY_URL, - OPTION_PROXY_USERNAME, - OPTION_PROXY_PASSWORD, - OPTION_CUSTOM_RENDEZVOUS_SERVER, - OPTION_API_SERVER, - OPTION_KEY, - OPTION_ALLOW_WEBSOCKET, - OPTION_PRESET_ADDRESS_BOOK_NAME, - OPTION_PRESET_ADDRESS_BOOK_TAG, - OPTION_PRESET_ADDRESS_BOOK_ALIAS, - OPTION_PRESET_ADDRESS_BOOK_PASSWORD, - OPTION_PRESET_ADDRESS_BOOK_NOTE, - OPTION_PRESET_DEVICE_USERNAME, - OPTION_PRESET_DEVICE_NAME, - OPTION_PRESET_NOTE, - OPTION_ENABLE_DIRECTX_CAPTURE, - OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE, - OPTION_ENABLE_TRUSTED_DEVICES, - OPTION_RELAY_SERVER, - OPTION_ICE_SERVERS, - OPTION_DISABLE_UDP, - OPTION_ALLOW_INSECURE_TLS_FALLBACK, - OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, - OPTION_ALLOW_AUTO_UPDATE, - OPTION_ALLOW_KCP_CC, - OPTION_ALLOW_WEBRTC_CC, - ]; - - // BUILDIN_SETTINGS - pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[ - OPTION_DISPLAY_NAME, - OPTION_AVATAR, - OPTION_PRESET_DEVICE_GROUP_NAME, - OPTION_PRESET_USERNAME, - OPTION_PRESET_STRATEGY_NAME, - OPTION_REMOVE_PRESET_PASSWORD_WARNING, - OPTION_HIDE_GENERAL_SETTINGS, - OPTION_HIDE_SECURITY_SETTINGS, - OPTION_HIDE_NETWORK_SETTINGS, - OPTION_HIDE_SERVER_SETTINGS, - OPTION_HIDE_PROXY_SETTINGS, - OPTION_HIDE_REMOTE_PRINTER_SETTINGS, - OPTION_HIDE_WEBSOCKET_SETTINGS, - OPTION_HIDE_STOP_SERVICE, - OPTION_HIDE_USERNAME_ON_CARD, - OPTION_HIDE_HELP_CARDS, - OPTION_DEFAULT_CONNECT_PASSWORD, - OPTION_HIDE_TRAY, - OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, - OPTION_ALLOW_LOGON_SCREEN_PASSWORD, - OPTION_ALLOW_DEEP_LINK_PASSWORD, - OPTION_ALLOW_DEEP_LINK_SERVER_SETTINGS, - OPTION_ONE_WAY_FILE_TRANSFER, - OPTION_ALLOW_HTTPS_21114, - OPTION_ALLOW_HOSTNAME_AS_ID, - OPTION_REGISTER_DEVICE, - OPTION_HIDE_POWERED_BY_ME, - OPTION_MAIN_WINDOW_ALWAYS_ON_TOP, - OPTION_FILE_TRANSFER_MAX_FILES, - OPTION_DISABLE_CHANGE_PERMANENT_PASSWORD, - OPTION_DISABLE_CHANGE_ID, - OPTION_DISABLE_UNLOCK_PIN, - OPTION_USE_RAW_TCP_FOR_API, - OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED, - ]; } pub fn common_load< diff --git a/src/fs.rs b/src/fs.rs deleted file mode 100644 index 3dcc1c6db..000000000 --- a/src/fs.rs +++ /dev/null @@ -1,1807 +0,0 @@ -#[cfg(windows)] -use std::os::windows::prelude::*; -use std::{ - fmt::{Debug, Display}, - io::Cursor, - path::{Path, PathBuf}, - sync::atomic::{AtomicI32, Ordering}, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use serde_derive::{Deserialize, Serialize}; -use serde_json::json; -use tokio::{ - fs::{File, OpenOptions}, - io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufStream as TokioBufStream}, -}; - -use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream}; -// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html -use crate::{ - compress::{compress, decompress}, - config::Config, -}; - -static NEXT_JOB_ID: AtomicI32 = AtomicI32::new(1); - -pub fn get_next_job_id() -> i32 { - NEXT_JOB_ID.fetch_add(1, Ordering::SeqCst) -} - -pub fn update_next_job_id(id: i32) { - NEXT_JOB_ID.store(id, Ordering::SeqCst); -} - -pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { - let mut dir = FileDirectory { - path: get_string(path), - ..Default::default() - }; - #[cfg(windows)] - if "/" == &get_string(path) { - let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; - for i in 0..32 { - if drives & (1 << i) != 0 { - let name = format!( - "{}:", - std::char::from_u32('A' as u32 + i as u32).unwrap_or('A') - ); - dir.entries.push(FileEntry { - name, - entry_type: FileType::DirDrive.into(), - ..Default::default() - }); - } - } - return Ok(dir); - } - for entry in path.read_dir()?.flatten() { - let p = entry.path(); - let name = p - .file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned(); - if name.is_empty() { - continue; - } - let mut is_hidden = false; - let meta; - if let Ok(tmp) = std::fs::symlink_metadata(&p) { - meta = tmp; - } else { - continue; - } - // docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants - #[cfg(windows)] - if meta.file_attributes() & 0x2 != 0 { - is_hidden = true; - } - #[cfg(not(windows))] - if name.find('.').unwrap_or(usize::MAX) == 0 { - is_hidden = true; - } - if is_hidden && !include_hidden { - continue; - } - let (entry_type, size) = { - if p.is_dir() { - if meta.file_type().is_symlink() { - (FileType::DirLink.into(), 0) - } else { - (FileType::Dir.into(), 0) - } - } else if meta.file_type().is_symlink() { - (FileType::FileLink.into(), 0) - } else { - (FileType::File.into(), meta.len()) - } - }; - let modified_time = meta - .modified() - .map(|x| { - x.duration_since(std::time::SystemTime::UNIX_EPOCH) - .map(|x| x.as_secs()) - .unwrap_or(0) - }) - .unwrap_or(0); - dir.entries.push(FileEntry { - name: get_file_name(&p), - entry_type, - is_hidden, - size, - modified_time, - ..Default::default() - }); - } - Ok(dir) -} - -#[inline] -pub fn get_file_name(p: &Path) -> String { - p.file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned() -} - -#[inline] -pub fn get_string(path: &Path) -> String { - path.to_str().unwrap_or("").to_owned() -} - -#[inline] -pub fn get_path(path: &str) -> PathBuf { - Path::new(path).to_path_buf() -} - -#[inline] -pub fn get_home_as_string() -> String { - get_string(&Config::get_home()) -} - -fn read_dir_recursive( - path: &Path, - prefix: &Path, - include_hidden: bool, -) -> ResultType> { - let mut files = Vec::new(); - if path.is_dir() { - // to-do: symbol link handling, cp the link rather than the content - // to-do: file mode, for unix - let fd = read_dir(path, include_hidden)?; - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::File) => { - let mut entry = entry.clone(); - entry.name = get_string(&prefix.join(entry.name)); - files.push(entry); - } - Ok(FileType::Dir) => { - if let Ok(mut tmp) = read_dir_recursive( - &path.join(&entry.name), - &prefix.join(&entry.name), - include_hidden, - ) { - for entry in tmp.drain(0..) { - files.push(entry); - } - } - } - _ => {} - } - } - Ok(files) - } else if path.is_file() { - let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { - ( - meta.len(), - meta.modified() - .map(|x| { - x.duration_since(std::time::SystemTime::UNIX_EPOCH) - .map(|x| x.as_secs()) - .unwrap_or(0) - }) - .unwrap_or(0), - ) - } else { - (0, 0) - }; - files.push(FileEntry { - entry_type: FileType::File.into(), - size, - modified_time, - ..Default::default() - }); - Ok(files) - } else { - bail!("Not exists"); - } -} - -pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType> { - read_dir_recursive(&get_path(path), &get_path(""), include_hidden) -} - -fn read_empty_dirs_recursive( - path: &Path, - prefix: &Path, - include_hidden: bool, -) -> ResultType> { - let mut dirs = Vec::new(); - if path.is_dir() { - // to-do: symbol link handling, cp the link rather than the content - // to-do: file mode, for unix - let fd = read_dir(path, include_hidden)?; - if fd.entries.is_empty() { - dirs.push(fd); - } else { - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::Dir) => { - if let Ok(mut tmp) = read_empty_dirs_recursive( - &path.join(&entry.name), - &prefix.join(&entry.name), - include_hidden, - ) { - for entry in tmp.drain(0..) { - dirs.push(entry); - } - } - } - _ => {} - } - } - } - Ok(dirs) - } else if path.is_file() { - Ok(dirs) - } else { - bail!("Not exists"); - } -} - -pub fn get_empty_dirs_recursive( - path: &str, - include_hidden: bool, -) -> ResultType> { - read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden) -} - -#[inline] -pub fn is_file_exists(file_path: &str) -> bool { - return Path::new(file_path).exists(); -} - -#[inline] -pub fn can_enable_overwrite_detection(version: i64) -> bool { - version >= get_version_number("1.1.10") -} - -#[repr(i32)] -#[derive(Copy, Clone, Serialize, Debug, PartialEq)] -pub enum JobType { - Generic = 0, - Printer = 1, -} - -impl Default for JobType { - fn default() -> Self { - JobType::Generic - } -} - -impl From for file_transfer_send_request::FileType { - fn from(t: JobType) -> Self { - match t { - JobType::Generic => file_transfer_send_request::FileType::Generic, - JobType::Printer => file_transfer_send_request::FileType::Printer, - } - } -} - -impl From for JobType { - fn from(value: i32) -> Self { - match value { - 0 => JobType::Generic, - 1 => JobType::Printer, - _ => JobType::Generic, - } - } -} - -impl Into for JobType { - fn into(self) -> i32 { - self as i32 - } -} - -impl JobType { - pub fn from_proto(t: ::protobuf::EnumOrUnknown) -> Self { - match t.enum_value() { - Ok(file_transfer_send_request::FileType::Generic) => JobType::Generic, - Ok(file_transfer_send_request::FileType::Printer) => JobType::Printer, - _ => JobType::Generic, - } - } -} - -#[derive(Debug)] -pub enum DataSource { - FilePath(PathBuf), - MemoryCursor(Cursor>), -} - -impl Default for DataSource { - fn default() -> Self { - DataSource::FilePath(PathBuf::new()) - } -} - -impl serde::Serialize for DataSource { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - match self { - DataSource::FilePath(p) => serializer.serialize_str(p.to_str().unwrap_or("")), - DataSource::MemoryCursor(_) => serializer.serialize_str(""), - } - } -} - -impl Display for DataSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DataSource::FilePath(p) => write!(f, "File: {}", p.to_string_lossy().to_string()), - DataSource::MemoryCursor(_) => write!(f, "Bytes"), - } - } -} - -impl DataSource { - fn to_meta(&self) -> String { - match self { - DataSource::FilePath(p) => p.to_string_lossy().to_string(), - DataSource::MemoryCursor(_) => "".to_string(), - } - } -} - -enum DataStream { - FileStream(File), - BufStream(TokioBufStream>>), -} - -impl Debug for DataStream { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DataStream::FileStream(fs) => write!(f, "{:?}", fs), - DataStream::BufStream(_) => write!(f, "BufStream"), - } - } -} - -impl DataStream { - async fn write_all(&mut self, buf: &[u8]) -> ResultType<()> { - match self { - DataStream::FileStream(fs) => fs.write_all(buf).await?, - DataStream::BufStream(bs) => bs.write_all(buf).await?, - } - Ok(()) - } - - async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - match self { - DataStream::FileStream(fs) => fs.read(buf).await, - DataStream::BufStream(bs) => bs.read(buf).await, - } - } -} - -#[derive(Default, Serialize, Deserialize, Debug)] -pub struct FileDigest { - pub size: u64, - pub modified: u64, -} - -#[derive(Default, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct TransferJob { - pub id: i32, - pub r#type: JobType, - pub remote: String, - pub data_source: DataSource, - pub show_hidden: bool, - pub is_remote: bool, - pub is_last_job: bool, - pub is_resume: bool, - pub file_num: i32, - #[serde(skip_serializing)] - files: Vec, - pub conn_id: i32, // server only - - #[serde(skip_serializing)] - data_stream: Option, - pub total_size: u64, - finished_size: u64, - transferred: u64, - enable_overwrite_detection: bool, - file_confirmed: bool, - // indicating the last file is skipped - file_skipped: bool, - file_is_waiting: bool, - default_overwrite_strategy: Option, - #[serde(skip_serializing)] - digest: FileDigest, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct TransferJobMeta { - #[serde(default)] - pub id: i32, - #[serde(default)] - pub remote: String, - #[serde(default)] - pub to: String, - #[serde(default)] - pub show_hidden: bool, - #[serde(default)] - pub file_num: i32, - #[serde(default)] - pub is_remote: bool, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct RemoveJobMeta { - #[serde(default)] - pub path: String, - #[serde(default)] - pub is_remote: bool, - #[serde(default)] - pub no_confirm: bool, -} - -#[inline] -fn get_ext(name: &str) -> &str { - if let Some(i) = name.rfind('.') { - return &name[i + 1..]; - } - "" -} - -#[inline] -fn is_compressed_file(name: &str) -> bool { - let compressed_exts = ["xz", "gz", "zip", "7z", "rar", "bz2", "tgz", "png", "jpg"]; - let ext = get_ext(name); - compressed_exts.contains(&ext) -} - -pub fn validate_file_name_no_traversal(name: &str) -> ResultType<()> { - if name.bytes().any(|b| b == 0) { - bail!("file name contains null bytes"); - } - let has_traversal = name - .split(|c: char| c == '/' || (cfg!(windows) && c == '\\')) - .filter(|s| !s.is_empty()) - .any(|s| s == ".."); - if has_traversal { - bail!("path traversal detected in file name"); - } - #[cfg(windows)] - { - if name.len() >= 2 { - let bytes = name.as_bytes(); - if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { - bail!("absolute path detected in file name"); - } - } - if name.starts_with('/') || name.starts_with('\\') { - bail!("absolute path detected in file name"); - } - } - #[cfg(not(windows))] - if name.starts_with('/') { - bail!("absolute path detected in file name"); - } - Ok(()) -} - -fn validate_transfer_file_names(files: &[FileEntry]) -> ResultType<()> { - // Single-file transfer may use an empty relative name, because - // the destination file path is carried by transfer metadata. - if files.len() == 1 && files.first().map_or(false, |f| f.name.is_empty()) { - return Ok(()); - } - for file in files { - if file.name.is_empty() { - bail!("empty file name in multi-file transfer"); - } - validate_file_name_no_traversal(&file.name)?; - } - Ok(()) -} - -#[inline] -fn validate_fs_path_argument(path: &str, arg_name: &str) -> ResultType<()> { - if path.is_empty() { - bail!("{arg_name} cannot be empty"); - } - if path.bytes().any(|b| b == 0) { - bail!("{arg_name} contains null bytes"); - } - Ok(()) -} - -fn validate_no_symlink_components(base: &PathBuf, name: &str) -> ResultType<()> { - if name.is_empty() { - return Ok(()); - } - let mut current = base.clone(); - for component in Path::new(name).components() { - match component { - std::path::Component::Normal(seg) => { - current.push(seg); - // Best-effort guard: path-based checks are inherently TOCTOU-prone - // if local filesystem state changes between validation and write. - match std::fs::symlink_metadata(¤t) { - Ok(meta) => { - // This is inherent to filesystem-based checks and acknowledged as a limitation. - // For true protection, you'd need openat(2) / O_NOFOLLOW at write time. - if meta.file_type().is_symlink() { - bail!("symlink path component is not allowed"); - } - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - // Component does not exist yet, continue best-effort validation. - } - Err(err) => { - bail!( - "failed to validate path component '{}': {}", - current.display(), - err - ); - } - } - } - std::path::Component::CurDir => {} - _ => { - bail!("invalid file name component"); - } - } - } - Ok(()) -} - -/// Validate an untrusted relative file name and existing path components before joining it. -pub fn join_validated_path(base: &PathBuf, name: &str) -> ResultType { - validate_file_name_no_traversal(name)?; - validate_no_symlink_components(base, name)?; - Ok(TransferJob::join(base, name)) -} - -impl TransferJob { - #[allow(clippy::too_many_arguments)] - pub fn new_write( - id: i32, - r#type: JobType, - remote: String, - data_source: DataSource, - file_num: i32, - show_hidden: bool, - is_remote: bool, - enable_overwrite_detection: bool, - ) -> Self { - log::info!("new write {}", data_source); - Self { - id, - r#type, - remote, - data_source, - file_num, - show_hidden, - is_remote, - files: Vec::new(), - total_size: 0, - enable_overwrite_detection, - ..Default::default() - } - } - - pub fn with_files(mut self, files: Vec) -> ResultType { - self.set_files(files)?; - Ok(self) - } - - pub fn new_read( - id: i32, - r#type: JobType, - remote: String, - data_source: DataSource, - file_num: i32, - show_hidden: bool, - is_remote: bool, - enable_overwrite_detection: bool, - ) -> ResultType { - log::info!("new read {}", data_source); - let (files, total_size) = match &data_source { - DataSource::FilePath(p) => { - let p = p.to_str().ok_or(anyhow!("Invalid path"))?; - let files = get_recursive_files(p, show_hidden)?; - let total_size = files.iter().map(|x| x.size).sum(); - (files, total_size) - } - DataSource::MemoryCursor(c) => (Vec::new(), c.get_ref().len() as u64), - }; - Ok(Self { - id, - r#type, - remote, - data_source, - file_num, - show_hidden, - is_remote, - files, - total_size, - enable_overwrite_detection, - ..Default::default() - }) - } - - pub async fn get_buf_data(self) -> ResultType>> { - match self.data_stream { - Some(DataStream::BufStream(mut bs)) => { - bs.flush().await?; - Ok(Some(bs.into_inner().into_inner())) - } - _ => Ok(None), - } - } - - #[inline] - pub fn files(&self) -> &Vec { - &self.files - } - - #[inline] - pub fn set_files(&mut self, files: Vec) -> ResultType<()> { - validate_transfer_file_names(&files)?; - if let DataSource::FilePath(base) = &self.data_source { - for file in &files { - validate_no_symlink_components(base, &file.name)?; - } - } - self.total_size = files.iter().map(|x| x.size).sum(); - self.files = files; - Ok(()) - } - - #[inline] - pub fn set_digest(&mut self, size: u64, modified: u64) { - self.digest.size = size; - self.digest.modified = modified; - } - - #[inline] - pub fn id(&self) -> i32 { - self.id - } - - #[inline] - pub fn total_size(&self) -> u64 { - self.total_size - } - - #[inline] - pub fn finished_size(&self) -> u64 { - self.finished_size - } - - #[inline] - pub fn transferred(&self) -> u64 { - self.transferred - } - - #[inline] - pub fn file_num(&self) -> i32 { - self.file_num - } - - fn resolve_entry_path(&self, base: &PathBuf, name: &str) -> Option { - if self.r#type == JobType::Generic { - match join_validated_path(base, name) { - Ok(path) => Some(path), - Err(err) => { - log::error!("Invalid file name in transfer job {}: {}", self.id, err); - None - } - } - } else { - Some(Self::join(base, name)) - } - } - - pub fn modify_time(&self) { - if self.r#type == JobType::Printer { - return; - } - if let DataSource::FilePath(p) = &self.data_source { - let file_num = self.file_num as usize; - if file_num < self.files.len() { - let entry = &self.files[file_num]; - let Some(path) = self.resolve_entry_path(p, &entry.name) else { - return; - }; - let download_path = format!("{}.download", get_string(&path)); - let digest_path = format!("{}.digest", get_string(&path)); - std::fs::remove_file(digest_path).ok(); - std::fs::rename(download_path, &path).ok(); - filetime::set_file_mtime( - &path, - filetime::FileTime::from_unix_time(entry.modified_time as _, 0), - ) - .ok(); - } - } - } - - pub fn remove_download_file(&self) { - if self.r#type == JobType::Printer { - return; - } - if let DataSource::FilePath(p) = &self.data_source { - let file_num = self.file_num as usize; - if file_num < self.files.len() { - let entry = &self.files[file_num]; - let Some(path) = self.resolve_entry_path(p, &entry.name) else { - return; - }; - let download_path = format!("{}.download", get_string(&path)); - let digest_path = format!("{}.digest", get_string(&path)); - std::fs::remove_file(download_path).ok(); - std::fs::remove_file(digest_path).ok(); - } - } - } - - #[inline] - pub fn set_finished_size_on_resume(&mut self) { - if self.is_resume && self.file_num > 0 { - let finished_size: u64 = self - .files - .iter() - .take(self.file_num as usize) - .map(|file| file.size) - .sum(); - self.finished_size = finished_size; - } - } - - pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { - if block.id != self.id { - bail!("Wrong id"); - } - match &self.data_source { - DataSource::FilePath(p) => { - let file_num = block.file_num as usize; - if file_num >= self.files.len() { - bail!("Wrong file number"); - } - if file_num != self.file_num as usize || self.data_stream.is_none() { - self.modify_time(); - if let Some(DataStream::FileStream(file)) = self.data_stream.as_mut() { - file.sync_all().await?; - } - self.file_num = block.file_num; - let entry = &self.files[file_num]; - let (path, digest_path) = if self.r#type == JobType::Printer { - (p.to_string_lossy().to_string(), None) - } else { - let path = join_validated_path(p, &entry.name)?; - // NOTE: We intentionally keep path-based validation + regular file open here. - // This still has a known TOCTOU window for symlink races, but avoids a large - // cross-platform rewrite for now. - // Revisit with descriptor/handle-based no-follow open in future hardening. - if let Some(pp) = path.parent() { - std::fs::create_dir_all(pp).ok(); - } - let file_path = get_string(&path); - ( - format!("{}.download", &file_path), - Some(format!("{}.digest", &file_path)), - ) - }; - if let Some(dp) = digest_path.as_ref() { - if Path::new(dp).exists() { - std::fs::remove_file(dp)?; - } - } - self.data_stream = Some(DataStream::FileStream(File::create(&path).await?)); - if let Some(dp) = digest_path.as_ref() { - std::fs::write(dp, json!(self.digest).to_string()).ok(); - } - } - } - DataSource::MemoryCursor(c) => { - if self.data_stream.is_none() { - self.data_stream = Some(DataStream::BufStream(TokioBufStream::new(c.clone()))); - } - } - } - if block.compressed { - let tmp = decompress(&block.data); - self.data_stream - .as_mut() - .ok_or(anyhow!("data stream is None"))? - .write_all(&tmp) - .await?; - self.finished_size += tmp.len() as u64; - } else { - self.data_stream - .as_mut() - .ok_or(anyhow!("file is None"))? - .write_all(&block.data) - .await?; - self.finished_size += block.data.len() as u64; - } - self.transferred += block.data.len() as u64; - Ok(()) - } - - #[inline] - pub fn join(p: &PathBuf, name: &str) -> PathBuf { - if name.is_empty() { - p.clone() - } else { - p.join(name) - } - } - - /// Open the data stream for the current file. - /// Returns Ok(true) if job is done, Ok(false) otherwise. - async fn open_data_stream(&mut self) -> ResultType { - let file_num = self.file_num as usize; - match &mut self.data_source { - DataSource::FilePath(p) => { - if file_num >= self.files.len() { - // job done - self.data_stream.take(); - return Ok(true); - }; - if self.data_stream.is_none() { - match File::open(Self::join(p, &self.files[file_num].name)).await { - Ok(file) => { - self.data_stream = Some(DataStream::FileStream(file)); - self.file_confirmed = false; - self.file_is_waiting = false; - } - // On open error, behave the same as validation failure: advance - // to next file and return the error. - Err(err) => { - self.file_num += 1; - self.file_confirmed = false; - self.file_is_waiting = false; - return Err(err.into()); - } - } - } - } - DataSource::MemoryCursor(c) => { - if self.data_stream.is_none() { - let mut t = std::io::Cursor::new(Vec::new()); - std::mem::swap(&mut t, c); - self.data_stream = Some(DataStream::BufStream(TokioBufStream::new(t))); - } - } - } - Ok(false) - } - - /// Get current file's digest (last_modified, file_size) for overwrite detection. - async fn get_current_digest(&self) -> ResultType<(u64, u64)> { - let meta = match self.data_stream.as_ref().ok_or(anyhow!("file is None"))? { - DataStream::FileStream(file) => file.metadata().await?, - DataStream::BufStream(_) => bail!("No digest for buf stream"), - }; - let last_modified = meta - .modified()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(); - Ok((last_modified, meta.len())) - } - - async fn init_data_stream(&mut self, stream: &mut crate::Stream) -> ResultType<()> { - if self.open_data_stream().await? { - return Ok(()); - } - if self.r#type == JobType::Generic - && self.enable_overwrite_detection - && !self.file_confirmed() - && !self.file_is_waiting() - { - self.send_current_digest(stream).await?; - self.set_file_is_waiting(true); - } - Ok(()) - } - - /// Initialize data stream for CM (Connection Manager) scenario. - /// Returns digest info (last_modified, file_size) if overwrite detection is enabled, - /// so caller can send it via IPC instead of network stream. - /// Returns Ok(None) if job is done or already initialized. - pub async fn init_data_stream_for_cm(&mut self) -> ResultType> { - if self.open_data_stream().await? { - return Ok(None); - } - // For overwrite detection, return digest info instead of sending via stream - if self.r#type == JobType::Generic - && self.enable_overwrite_detection - && !self.file_confirmed() - && !self.file_is_waiting() - { - let digest = self.get_current_digest().await?; - self.set_file_is_waiting(true); - return Ok(Some(digest)); - } - Ok(None) - } - - pub async fn read(&mut self) -> ResultType> { - if self.r#type == JobType::Generic { - if self.enable_overwrite_detection && !self.file_confirmed() { - return Ok(None); - } - } - - let file_num = self.file_num as usize; - let name = match &self.data_source { - DataSource::FilePath(p) => { - if file_num >= self.files.len() { - self.data_stream.take(); - return Ok(None); - }; - if self.files.len() == 1 && self.files[file_num].name.is_empty() { - p.file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - } else { - &self.files[file_num].name - } - } - DataSource::MemoryCursor(..) => "", - }; - const BUF_SIZE: usize = 128 * 1024; - let mut buf: Vec = vec![0; BUF_SIZE]; - let mut compressed = false; - let mut offset: usize = 0; - loop { - match self - .data_stream - .as_mut() - .ok_or(anyhow!("data stream is None"))? - .read(&mut buf[offset..]) - .await - { - Err(err) => { - self.file_num += 1; - self.data_stream = None; - self.file_confirmed = false; - self.file_is_waiting = false; - return Err(err.into()); - } - Ok(n) => { - offset += n; - if n == 0 || offset == BUF_SIZE { - break; - } - } - } - } - unsafe { buf.set_len(offset) }; - if offset == 0 { - if matches!(self.data_source, DataSource::MemoryCursor(_)) { - self.data_stream.take(); - return Ok(None); - } - self.file_num += 1; - self.data_stream = None; - self.file_confirmed = false; - self.file_is_waiting = false; - } else { - self.finished_size += offset as u64; - if matches!(self.data_source, DataSource::FilePath(_)) && !is_compressed_file(name) { - let tmp = compress(&buf); - if tmp.len() < buf.len() { - buf = tmp; - compressed = true; - } - } - self.transferred += buf.len() as u64; - } - Ok(Some(FileTransferBlock { - id: self.id, - file_num: file_num as _, - data: buf.into(), - compressed, - ..Default::default() - })) - } - - // Only for generic job and file stream - async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> { - let (last_modified, file_size) = self.get_current_digest().await?; - let mut msg = Message::new(); - let mut resp = FileResponse::new(); - resp.set_digest(FileTransferDigest { - id: self.id, - file_num: self.file_num, - last_modified, - file_size, - is_resume: self.is_resume, - ..Default::default() - }); - msg.set_file_response(resp); - stream.send(&msg).await?; - log::info!( - "id: {}, file_num: {}, digest message is sent. waiting for confirm. msg: {:?}", - self.id, - self.file_num, - msg - ); - Ok(()) - } - - pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option) { - self.default_overwrite_strategy = overwrite_strategy; - } - - pub fn default_overwrite_strategy(&self) -> Option { - self.default_overwrite_strategy - } - - pub fn set_file_confirmed(&mut self, file_confirmed: bool) { - log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed); - self.file_confirmed = file_confirmed; - self.file_skipped = false; - } - - pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) { - self.file_is_waiting = file_is_waiting; - } - - #[inline] - pub fn file_is_waiting(&self) -> bool { - self.file_is_waiting - } - - #[inline] - pub fn file_confirmed(&self) -> bool { - self.file_confirmed - } - - /// Indicating whether the last file is skipped - #[inline] - pub fn file_skipped(&self) -> bool { - self.file_skipped - } - - /// Indicating whether the whole task is skipped - #[inline] - pub fn job_skipped(&self) -> bool { - self.file_skipped() && self.files.len() == 1 - } - - /// Check whether the job is completed after `read` returns `None` - /// This is a helper function which gives additional lifecycle when the job reads `None`. - /// If returns `true`, it means we can delete the job automatically. `False` otherwise. - /// - /// [`Note`] - /// Conditions: - /// 1. Files are not waiting for confirmation by peers. - #[inline] - pub fn job_completed(&self) -> bool { - // has no error, Condition 2 - !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) - } - - /// Get job error message, useful for getting status when job had finished - pub fn job_error(&self) -> Option { - if self.job_skipped() { - return Some("skipped".to_string()); - } - None - } - - pub fn set_file_skipped(&mut self) -> bool { - log::debug!("skip file {} in job {}", self.file_num, self.id); - self.data_stream.take(); - self.set_file_confirmed(false); - self.set_file_is_waiting(false); - self.file_num += 1; - self.file_skipped = true; - true - } - - async fn set_stream_offset(&mut self, file_num: usize, offset: u64) { - if let DataSource::FilePath(p) = &self.data_source { - let entry = &self.files[file_num]; - let Some(path) = self.resolve_entry_path(p, &entry.name) else { - return; - }; - let file_path = get_string(&path); - let download_path = format!("{}.download", &file_path); - let digest_path = format!("{}.digest", &file_path); - - let mut f = if Path::new(&download_path).exists() && Path::new(&digest_path).exists() { - // If both download and digest files exist, seek (writer) to the offset - // NOTE: same as write path: best-effort symlink validation happened earlier, - // but this reopen remains TOCTOU-prone by design for now. - match OpenOptions::new() - .create(true) - .write(true) - .open(&download_path) - .await - { - Ok(f) => f, - Err(e) => { - log::warn!("Failed to open file {}: {}", download_path, e); - return; - } - } - } else if Path::new(&file_path).exists() { - // If `file_path` exists, seek (reader) to the offset - match File::open(&file_path).await { - Ok(f) => f, - Err(e) => { - log::warn!("Failed to open file {}: {}", file_path, e); - return; - } - } - } else { - log::warn!( - "File {} not found, cannot seek to offset {}", - file_path, - offset - ); - return; - }; - if f.seek(std::io::SeekFrom::Start(offset)).await.is_ok() { - self.data_stream = Some(DataStream::FileStream(f)); - self.transferred += offset; - self.finished_size += offset; - } - } - } - - pub async fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool { - if self.file_num() != r.file_num { - // This branch will always be hit if: - // 1. `confirm()` is called in `ui_cm_interface.rs` - // 2. Not resuming - // - // It is ok. Because `confirm()` in `ui_cm_interface.rs` is only used for resuming. - log::info!("file num truncated, ignoring"); - } else { - match r.union { - Some(file_transfer_send_confirm_request::Union::Skip(s)) => { - if s { - self.set_file_skipped(); - } else { - self.set_file_confirmed(true); - } - } - Some(file_transfer_send_confirm_request::Union::OffsetBlk(offset)) => { - self.set_file_confirmed(true); - // If offset is greater than 0, we need to seek to the offset - if offset > 0 { - self.set_stream_offset(r.file_num as usize, offset as u64) - .await; - } - } - _ => {} - } - } - true - } - - #[inline] - pub fn gen_meta(&self) -> TransferJobMeta { - TransferJobMeta { - id: self.id, - remote: self.remote.to_string(), - to: self.data_source.to_meta(), - file_num: self.file_num, - show_hidden: self.show_hidden, - is_remote: self.is_remote, - } - } -} - -#[inline] -pub fn new_error(id: i32, err: T, file_num: i32) -> Message { - let mut resp = FileResponse::new(); - resp.set_error(FileTransferError { - id, - error: err.to_string(), - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_dir(id: i32, path: String, files: Vec) -> Message { - let mut resp = FileResponse::new(); - resp.set_dir(FileDirectory { - id, - path, - entries: files, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_block(block: FileTransferBlock) -> Message { - let mut resp = FileResponse::new(); - resp.set_block(block); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message { - let mut msg_out = Message::new(); - let mut action = FileAction::new(); - action.set_send_confirm(r); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_receive( - id: i32, - path: String, - file_num: i32, - files: Vec, - total_size: u64, -) -> Message { - let mut action = FileAction::new(); - action.set_receive(FileTransferReceiveRequest { - id, - path, - files, - file_num, - total_size, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_send( - id: i32, - r#type: JobType, - path: String, - file_num: i32, - include_hidden: bool, -) -> Message { - log::info!("new send: {}, id: {}", path, id); - let mut action = FileAction::new(); - let t: file_transfer_send_request::FileType = r#type.into(); - action.set_send(FileTransferSendRequest { - id, - path, - include_hidden, - file_num, - file_type: t.into(), - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_done(id: i32, file_num: i32) -> Message { - let mut resp = FileResponse::new(); - resp.set_done(FileTransferDone { - id, - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn remove_job(id: i32, jobs: &mut Vec) -> Option { - jobs.iter() - .position(|x| x.id() == id) - .map(|index| jobs.remove(index)) -} - -#[inline] -pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { - jobs.iter_mut().find(|x| x.id() == id) -} - -#[inline] -pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> { - jobs.iter().find(|x| x.id() == id) -} - -async fn init_jobs(jobs: &mut Vec, stream: &mut crate::Stream) -> ResultType<()> { - for job in jobs.iter_mut() { - if job.is_last_job { - continue; - } - if let Err(err) = job.init_data_stream(stream).await { - stream - .send(&new_error(job.id(), err, job.file_num())) - .await?; - } - } - Ok(()) -} - -pub async fn handle_read_jobs( - jobs: &mut Vec, - stream: &mut crate::Stream, -) -> ResultType { - init_jobs(jobs, stream).await?; - - let mut job_log = Default::default(); - let mut finished = Vec::new(); - for job in jobs.iter_mut() { - if job.is_last_job { - continue; - } - match job.read().await { - Err(err) => { - stream - .send(&new_error(job.id(), err, job.file_num())) - .await?; - } - Ok(Some(block)) => { - stream.send(&new_block(block)).await?; - } - Ok(None) => { - if job.job_completed() { - job_log = serialize_transfer_job(job, true, false, ""); - finished.push(job.id()); - match job.job_error() { - Some(err) => { - job_log = serialize_transfer_job(job, false, false, &err); - stream - .send(&new_error(job.id(), err, job.file_num())) - .await? - } - None => stream.send(&new_done(job.id(), job.file_num())).await?, - } - } else { - // waiting confirmation. - } - } - } - // Break to handle jobs one by one. - break; - } - for id in finished { - let _ = remove_job(id, jobs); - } - Ok(job_log) -} - -pub fn remove_all_empty_dir(path: &Path) -> ResultType<()> { - let fd = read_dir(path, true)?; - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::Dir) => { - remove_all_empty_dir(&path.join(&entry.name)).ok(); - } - Ok(FileType::DirLink) | Ok(FileType::FileLink) => { - std::fs::remove_file(path.join(&entry.name)).ok(); - } - _ => {} - } - } - std::fs::remove_dir(path).ok(); - Ok(()) -} - -#[inline] -pub fn remove_file(file: &str) -> ResultType<()> { - validate_fs_path_argument(file, "file path")?; - std::fs::remove_file(get_path(file))?; - Ok(()) -} - -#[inline] -pub fn create_dir(dir: &str) -> ResultType<()> { - validate_fs_path_argument(dir, "directory path")?; - std::fs::create_dir_all(get_path(dir))?; - Ok(()) -} - -#[inline] -pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> { - validate_fs_path_argument(path, "path")?; - if new_name.is_empty() { - bail!("new file name cannot be empty"); - } - validate_file_name_no_traversal(new_name)?; - let path = std::path::Path::new(&path); - if path.exists() { - let dir = path - .parent() - .ok_or(anyhow!("Parent directoy of {path:?} not exists"))?; - let new_path = dir.join(&new_name); - std::fs::rename(&path, &new_path)?; - Ok(()) - } else { - bail!("{path:?} not exists"); - } -} - -#[inline] -pub fn transform_windows_path(entries: &mut Vec) { - for entry in entries { - entry.name = entry.name.replace('\\', "/"); - } -} - -pub enum DigestCheckResult { - IsSame, - NeedConfirm(FileTransferDigest), - NoSuchFile, -} - -#[inline] -pub fn is_write_need_confirmation( - is_resume: bool, - file_path: &str, - digest: &FileTransferDigest, -) -> ResultType { - let path = Path::new(file_path); - let digest_file = format!("{}.digest", file_path); - let download_file = format!("{}.download", file_path); - if is_resume && Path::new(&digest_file).exists() && Path::new(&download_file).exists() { - // If the digest file exists, it means the file was transferred before. - // We can use the digest file to check whether the file is the same. - if let Ok(content) = std::fs::read_to_string(digest_file) { - if let Ok(local_digest) = serde_json::from_str::(&content) { - let is_identical = local_digest.modified == digest.last_modified - && local_digest.size == digest.file_size; - if is_identical { - if let Ok(download_metadata) = std::fs::metadata(download_file) { - // Get the file size of the local file - // Only send confirmation if the file is not empty. - let transferred_size = download_metadata.len(); - if transferred_size > 0 { - return Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { - id: digest.id, - file_num: digest.file_num, - last_modified: digest.last_modified, - file_size: digest.file_size, - is_identical, - transferred_size, - ..Default::default() - })); - } - } - } - } - } - } - - if path.exists() && path.is_file() { - let metadata = std::fs::metadata(path)?; - let modified_time = metadata.modified()?; - let remote_mt = Duration::from_secs(digest.last_modified); - let local_mt = modified_time.duration_since(UNIX_EPOCH)?; - // [Note] - // We decide to give the decision whether to override the existing file to users, - // which obey the behavior of the file manager in our system. - let mut is_identical = false; - if remote_mt == local_mt && digest.file_size == metadata.len() { - is_identical = true; - } - Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { - id: digest.id, - file_num: digest.file_num, - last_modified: local_mt.as_secs(), - file_size: metadata.len(), - is_identical, - ..Default::default() - })) - } else { - // If the file does not exist, or the digest file and download file do not exist, we return NoSuchFile. - Ok(DigestCheckResult::NoSuchFile) - } -} - -pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String { - let mut v = vec![]; - for job in jobs { - let value = serde_json::to_value(job).unwrap_or_default(); - v.push(value); - } - serde_json::to_string(&v).unwrap_or_default() -} - -pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String { - let mut value = serde_json::to_value(job).unwrap_or_default(); - value["done"] = json!(done); - value["cancel"] = json!(cancel); - value["error"] = json!(error); - serde_json::to_string(&value).unwrap_or_default() -} - -#[cfg(test)] -mod tests { - use super::*; - - struct TestTempDir { - path: PathBuf, - } - - impl TestTempDir { - fn new(prefix: &str) -> Self { - Self { - path: unique_temp_dir(prefix), - } - } - - fn join(&self, path: &str) -> PathBuf { - self.path.join(path) - } - } - - impl Drop for TestTempDir { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.path); - } - } - - fn unique_temp_dir(prefix: &str) -> PathBuf { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - std::env::temp_dir().join(format!("{}_{}_{}", prefix, std::process::id(), timestamp)) - } - - fn new_file_entry(name: &str) -> FileEntry { - let mut entry = FileEntry::new(); - entry.name = name.to_string(); - entry - } - - fn new_validation_job(id: i32) -> TransferJob { - TransferJob::new_write( - id, - JobType::Generic, - "/fake/remote".to_string(), - DataSource::FilePath(std::env::temp_dir().join(format!("rustdesk_validation_{id}"))), - 0, - false, - true, - false, - ) - } - - fn new_write_job(id: i32, download_dir: PathBuf, name: &str) -> ResultType { - let job = TransferJob::new_write( - id, - JobType::Generic, - "/fake/remote".to_string(), - DataSource::FilePath(download_dir), - 0, - false, - true, - false, - ) - .with_files(vec![new_file_entry(name)])?; - Ok(job) - } - - fn assert_err_contains(err: anyhow::Error, expected: &str) { - assert!( - err.to_string().contains(expected), - "expected error containing '{}', got: {}", - expected, - err - ); - } - - #[test] - fn path_traversal_e2e_write_rejects_relative_escape() { - let tmp_root = TestTempDir::new("rustdesk_e2e_relative"); - let downloads = tmp_root.join("downloads"); - std::fs::create_dir_all(&downloads).expect("create downloads dir"); - - let err = new_write_job(1, downloads, "../traversal_proof.txt") - .expect_err("relative path traversal must be rejected"); - assert_err_contains(err, "path traversal"); - assert!(!tmp_root.join("traversal_proof.txt").exists()); - } - - #[test] - fn path_traversal_e2e_write_rejects_absolute_path() { - let tmp_root = TestTempDir::new("rustdesk_e2e_absolute"); - let downloads = tmp_root.join("downloads"); - let absolute_target = tmp_root.join("fake_ssh").join("authorized_keys"); - std::fs::create_dir_all(&downloads).expect("create downloads dir"); - - let err = new_write_job(2, downloads, &absolute_target.to_string_lossy()) - .expect_err("absolute path must be rejected"); - assert_err_contains(err, "absolute path"); - assert!(!absolute_target.exists()); - } - - #[test] - #[cfg_attr(windows, ignore = "requires symlink privilege to create test symlink")] - fn path_traversal_e2e_write_rejects_symlink_escape() { - let tmp_root = TestTempDir::new("rustdesk_e2e_symlink"); - let downloads = tmp_root.join("downloads"); - let outside = tmp_root.join("outside"); - let escaped_target = outside.join("escape.txt"); - std::fs::create_dir_all(&downloads).expect("create downloads dir"); - std::fs::create_dir_all(&outside).expect("create outside dir"); - - let symlink_path = downloads.join("link"); - #[cfg(unix)] - { - use std::os::unix::fs::symlink; - symlink(&outside, &symlink_path).expect("create symlink for test"); - } - #[cfg(windows)] - { - use std::os::windows::fs::symlink_dir; - symlink_dir(&outside, &symlink_path).expect("create directory symlink for test"); - } - - let err = new_write_job(3, downloads, "link/escape.txt") - .expect_err("symlink traversal must be rejected"); - assert_err_contains(err, "symlink"); - assert!(!escaped_target.exists()); - } - - #[test] - fn set_files_allows_single_empty_name_for_single_file_transfer() { - let mut job = new_validation_job(101); - assert!(job.set_files(vec![new_file_entry("")]).is_ok()); - } - - #[test] - fn set_files_rejects_empty_name_in_multi_file_transfer() { - let mut job = new_validation_job(102); - let err = job - .set_files(vec![new_file_entry(""), new_file_entry("ok.txt")]) - .expect_err("empty name in multi-file transfer must be rejected"); - assert_err_contains(err, "empty file name"); - } - - #[test] - fn set_files_rejects_null_byte_name() { - let mut job = new_validation_job(103); - let err = job - .set_files(vec![new_file_entry("bad\0name.txt")]) - .expect_err("null byte in file name must be rejected"); - assert_err_contains(err, "null bytes"); - } - - #[test] - fn set_files_rejects_mixed_entries_when_one_is_traversal() { - let mut job = new_validation_job(104); - let err = job - .set_files(vec![ - new_file_entry("safe/file.txt"), - new_file_entry("../../escape.txt"), - ]) - .expect_err("any traversal entry must reject the full file list"); - assert_err_contains(err, "path traversal"); - } - - #[cfg(windows)] - #[test] - fn set_files_rejects_unc_absolute_path() { - let mut job = new_validation_job(105); - let err = job - .set_files(vec![new_file_entry("\\\\server\\share\\payload.txt")]) - .expect_err("UNC absolute path must be rejected"); - assert_err_contains(err, "absolute path"); - } - - #[cfg(not(windows))] - #[test] - fn set_files_allows_backslash_prefixed_name_on_unix() { - let mut job = new_validation_job(105); - assert!(job - .set_files(vec![new_file_entry("\\\\server\\share\\payload.txt")]) - .is_ok()); - } - - #[test] - fn remove_file_rejects_empty_path() { - let err = remove_file("").expect_err("empty file path must be rejected"); - assert_err_contains(err, "cannot be empty"); - } - - #[test] - fn remove_file_rejects_null_byte_path() { - let err = remove_file("bad\0path").expect_err("null byte path must be rejected"); - assert_err_contains(err, "null bytes"); - } - - #[test] - fn create_dir_rejects_empty_path() { - let err = create_dir("").expect_err("empty directory path must be rejected"); - assert_err_contains(err, "cannot be empty"); - } - - #[test] - fn create_dir_rejects_null_byte_path() { - let err = create_dir("bad\0path").expect_err("null byte path must be rejected"); - assert_err_contains(err, "null bytes"); - } - - #[test] - fn rename_file_rejects_invalid_new_name() { - let tmp_root = TestTempDir::new("rustdesk_rename_invalid"); - let src = tmp_root.join("source.txt"); - std::fs::create_dir_all(&tmp_root.path).expect("create temp dir"); - std::fs::write(&src, b"content").expect("create source file"); - - let src_str = src.to_string_lossy().to_string(); - - let err_empty = - rename_file(&src_str, "").expect_err("empty new file name must be rejected"); - assert_err_contains(err_empty, "cannot be empty"); - - let err_traversal = rename_file(&src_str, "../escape.txt") - .expect_err("traversal new file name must be rejected"); - assert_err_contains(err_traversal, "path traversal"); - - let err_null = rename_file(&src_str, "bad\0name.txt") - .expect_err("null byte in new file name must be rejected"); - assert_err_contains(err_null, "null bytes"); - - #[cfg(windows)] - { - let err_abs = rename_file(&src_str, "C:\\Windows\\Temp\\payload.txt") - .expect_err("absolute new file name must be rejected"); - assert_err_contains(err_abs, "absolute path"); - } - #[cfg(not(windows))] - { - let err_abs = rename_file(&src_str, "/tmp/payload.txt") - .expect_err("absolute new file name must be rejected"); - assert_err_contains(err_abs, "absolute path"); - } - } - - #[test] - fn rename_file_accepts_valid_new_name() { - let tmp_root = TestTempDir::new("rustdesk_rename_ok"); - let src = tmp_root.join("rename_src.txt"); - let dst = tmp_root.join("renamed.txt"); - std::fs::create_dir_all(&tmp_root.path).expect("create temp dir"); - std::fs::write(&src, b"content").expect("create source file"); - - let src_str = src.to_string_lossy().to_string(); - rename_file(&src_str, "renamed.txt").expect("rename should succeed"); - - assert!(!src.exists()); - assert!(dst.exists()); - } - - #[cfg(windows)] - #[test] - fn set_files_rejects_windows_drive_absolute_path() { - let mut job = new_validation_job(106); - let err = job - .set_files(vec![new_file_entry("C:\\Windows\\Temp\\payload.txt")]) - .expect_err("drive-letter absolute path must be rejected"); - assert_err_contains(err, "absolute path"); - } - - #[cfg(windows)] - #[test] - fn set_files_rejects_windows_verbatim_drive_absolute_path() { - let mut job = new_validation_job(1061); - let err = job - .set_files(vec![new_file_entry(r"\\?\C:\Windows\Temp\x.txt")]) - .expect_err("verbatim drive absolute path must be rejected"); - assert_err_contains(err, "absolute path"); - } -} diff --git a/src/keyboard.rs b/src/keyboard.rs deleted file mode 100644 index 10979f520..000000000 --- a/src/keyboard.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{fmt, slice::Iter, str::FromStr}; - -use crate::protos::message::KeyboardMode; - -impl fmt::Display for KeyboardMode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - KeyboardMode::Legacy => write!(f, "legacy"), - KeyboardMode::Map => write!(f, "map"), - KeyboardMode::Translate => write!(f, "translate"), - KeyboardMode::Auto => write!(f, "auto"), - } - } -} - -impl FromStr for KeyboardMode { - type Err = (); - fn from_str(s: &str) -> Result { - match s { - "legacy" => Ok(KeyboardMode::Legacy), - "map" => Ok(KeyboardMode::Map), - "translate" => Ok(KeyboardMode::Translate), - "auto" => Ok(KeyboardMode::Auto), - _ => Err(()), - } - } -} - -impl KeyboardMode { - pub fn iter() -> Iter<'static, KeyboardMode> { - static KEYBOARD_MODES: [KeyboardMode; 4] = [ - KeyboardMode::Legacy, - KeyboardMode::Map, - KeyboardMode::Translate, - KeyboardMode::Auto, - ]; - KEYBOARD_MODES.iter() - } -} diff --git a/src/lib.rs b/src/lib.rs index 5ee3d816c..2974f2d8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,9 @@ pub mod compress; -pub mod platform; pub mod protos; pub use bytes; use config::Config; pub use futures; pub use protobuf; -pub use protos::message as message_proto; pub use protos::rendezvous as rendezvous_proto; use serde_derive::{Deserialize, Serialize}; use std::{ @@ -27,7 +25,6 @@ pub mod bytes_codec; pub use anyhow::{self, bail}; pub use futures_util; pub mod config; -pub mod fs; pub mod mem; pub use lazy_static; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -42,7 +39,8 @@ pub mod password_security; pub use chrono; pub use directories_next; pub use libc; -pub mod keyboard; +#[cfg(target_os = "linux")] +pub mod sh; pub use base64; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use dlopen; diff --git a/src/platform/linux.rs b/src/platform/linux.rs deleted file mode 100644 index 9dbf96c33..000000000 --- a/src/platform/linux.rs +++ /dev/null @@ -1,671 +0,0 @@ -use crate::ResultType; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - process::Command, -}; -use users::{get_current_uid, get_user_by_uid, os::unix::UserExt}; - -use sctk::{ - output::OutputData, - output::{OutputHandler, OutputState}, - reexports::client::protocol::wl_output::WlOutput, - reexports::client::{globals, Proxy}, - reexports::client::{Connection, QueueHandle}, - registry::{ProvidesRegistryState, RegistryState}, -}; - -lazy_static::lazy_static! { - pub static ref DISTRO: Distro = Distro::new(); -} - -// to-do: There seems to be some runtime issue that causes the audit logs to be generated. -// We may need to fix this and remove this workaround in the future. -// -// We use the pre-search method to find the command path to avoid the audit logs on some systems. -// No idea why the audit logs happen. -// Though the audit logs may disappear after rebooting. -// -// See https://github.com/rustdesk/rustdesk/discussions/11959 -// -// `ausearch -x /usr/share/rustdesk/rustdesk` will return -// ... -// time->Tue Jun 24 10:40:43 2025 -// type=PROCTITLE msg=audit(1750776043.446:192757): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365 -// type=PATH msg=audit(1750776043.446:192757): item=0 name="/usr/local/bin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 -// type=CWD msg=audit(1750776043.446:192757): cwd="/" -// type=SYSCALL msg=audit(1750776043.446:192757): arch=c000003e syscall=59 success=no exit=-2 a0=7fb7dbd22da0 a1=1d65f2c0 a2=7ffc25193360 a3=7ffc25194ec0 items=1 ppid=172208 pid=267565 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="rustdesk" exe="/usr/share/rustdesk/rustdesk" subj=unconfined key="processos_criados" -// ---- -// time->Tue Jun 24 10:40:43 2025 -// type=PROCTITLE msg=audit(1750776043.446:192758): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365 -// type=PATH msg=audit(1750776043.446:192758): item=0 name="/usr/sbin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 -// ... -lazy_static::lazy_static! { - pub static ref CMD_LOGINCTL: String = find_cmd_path("loginctl"); - pub static ref CMD_PS: String = find_cmd_path("ps"); - pub static ref CMD_SH: String = find_cmd_path("sh"); -} - -pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; -pub const DISPLAY_SERVER_X11: &str = "x11"; -pub const DISPLAY_DESKTOP_KDE: &str = "KDE"; - -pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; - -pub struct Distro { - pub name: String, - pub version_id: String, -} - -impl Distro { - fn new() -> Self { - let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - Self { name, version_id } - } -} - -fn find_cmd_path(cmd: &'static str) -> String { - let test_cmd = format!("/bin/{}", cmd); - if std::path::Path::new(&test_cmd).exists() { - return test_cmd; - } - let test_cmd = format!("/usr/bin/{}", cmd); - if std::path::Path::new(&test_cmd).exists() { - return test_cmd; - } - if let Ok(output) = Command::new("which").arg(cmd).output() { - if output.status.success() { - return String::from_utf8_lossy(&output.stdout).trim().to_string(); - } - } - cmd.to_string() -} - -// Deprecated. Use `hbb_common::platform::linux::is_kde_session()` instead for now. -// Or we need to set the correct environment variable in the server process. -#[inline] -pub fn is_kde() -> bool { - if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) { - env == DISPLAY_DESKTOP_KDE - } else { - false - } -} - -// Don't use `hbb_common::platform::linux::is_kde()` here. -// It's not correct in the server process. -pub fn is_kde_session() -> bool { - std::process::Command::new(CMD_SH.as_str()) - .arg("-c") - .arg("pgrep -f kded[0-9]+") - .stdout(std::process::Stdio::piped()) - .output() - .map(|o| !o.stdout.is_empty()) - .unwrap_or(false) -} - -#[inline] -pub fn is_gdm_user(username: &str) -> bool { - username == "gdm" || username == "sddm" - // || username == "lightgdm" -} - -#[inline] -pub fn is_desktop_wayland() -> bool { - get_display_server() == DISPLAY_SERVER_WAYLAND -} - -#[inline] -pub fn is_x11_or_headless() -> bool { - !is_desktop_wayland() -} - -// -1 -const INVALID_SESSION: &str = "4294967295"; - -pub fn get_display_server() -> String { - // Check for forced display server environment variable first - if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") { - return forced_display; - } - - // Check if `loginctl` can be called successfully - if run_loginctl(None).is_err() { - return DISPLAY_SERVER_X11.to_owned(); - } - - let mut session = get_values_of_seat0(&[0])[0].clone(); - if session.is_empty() { - // loginctl has not given the expected output. try something else. - if let Ok(sid) = std::env::var("XDG_SESSION_ID") { - // could also execute "cat /proc/self/sessionid" - session = sid; - } - if session.is_empty() { - session = run_cmds("cat /proc/self/sessionid").unwrap_or_default(); - if session == INVALID_SESSION { - session = "".to_owned(); - } - } - } - if session.is_empty() { - std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned()) - } else { - get_display_server_of_session(&session) - } -} - -pub fn get_display_server_of_session(session: &str) -> String { - let mut display_server = if let Ok(output) = - run_loginctl(Some(vec!["show-session", "-p", "Type", session])) - // Check session type of the session - { - String::from_utf8_lossy(&output.stdout) - .replace("Type=", "") - .trim_end() - .into() - } else { - "".to_owned() - }; - if display_server.is_empty() || display_server == "tty" || display_server == "unspecified" { - if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { - if !sestype.is_empty() { - return sestype.to_lowercase(); - } - } - display_server = "x11".to_owned(); - } - display_server.to_lowercase() -} - -#[inline] -fn line_values(indices: &[usize], line: &str) -> Vec { - indices - .into_iter() - .map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned()) - .collect::>() -} - -#[inline] -pub fn get_values_of_seat0(indices: &[usize]) -> Vec { - _get_values_of_seat0(indices, true) -} - -#[inline] -pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec { - _get_values_of_seat0(indices, false) -} - -// Ignore "3 sessions listed." -fn ignore_loginctl_line(line: &str) -> bool { - line.contains("sessions") || line.split(" ").count() < 4 -} - -fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec { - if let Ok(output) = run_loginctl(None) { - for line in String::from_utf8_lossy(&output.stdout).lines() { - if ignore_loginctl_line(line) { - continue; - } - if line.contains("seat0") { - if let Some(sid) = line.split_whitespace().next() { - if is_active(sid) { - if ignore_gdm_wayland { - if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) - && get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND - { - continue; - } - } - return line_values(indices, line); - } - } - } - } - - // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 - for line in String::from_utf8_lossy(&output.stdout).lines() { - if ignore_loginctl_line(line) { - continue; - } - if let Some(sid) = line.split_whitespace().next() { - if is_active(sid) { - let d = get_display_server_of_session(sid); - if ignore_gdm_wayland { - if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) - && d == DISPLAY_SERVER_WAYLAND - { - continue; - } - } - if d == "tty" || d == "unspecified" { - continue; - } - return line_values(indices, line); - } - } - } - } - - line_values(indices, "") -} - -pub fn is_active(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { - String::from_utf8_lossy(&output.stdout).contains("active") - } else { - false - } -} - -pub fn is_active_and_seat0(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) { - String::from_utf8_lossy(&output.stdout).contains("State=active") - && String::from_utf8_lossy(&output.stdout).contains("Seat=seat0") - } else { - false - } -} - -// Check both "Lock" and "Switch user" -pub fn is_session_locked(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", sid, "--property=LockedHint"])) { - String::from_utf8_lossy(&output.stdout).contains("LockedHint=yes") - } else { - false - } -} - -// **Note** that the return value here, the last character is '\n'. -// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end. -pub fn run_cmds(cmds: &str) -> ResultType { - let output = std::process::Command::new(CMD_SH.as_str()) - .args(vec!["-c", cmds]) - .output()?; - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - -pub fn run_cmds_trim_newline(cmds: &str) -> ResultType { - let output = std::process::Command::new(CMD_SH.as_str()) - .args(vec!["-c", cmds]) - .output()?; - let out = String::from_utf8_lossy(&output.stdout); - Ok(if out.ends_with('\n') { - out[..out.len() - 1].to_string() - } else { - out.to_string() - }) -} - -fn run_loginctl(args: Option>) -> std::io::Result { - if std::env::var("FLATPAK_ID").is_ok() { - let mut l_args = CMD_LOGINCTL.to_string(); - if let Some(a) = args.as_ref() { - l_args = format!("{} {}", l_args, a.join(" ")); - } - let res = std::process::Command::new("flatpak-spawn") - .args(vec![String::from("--host"), l_args]) - .output(); - if res.is_ok() { - return res; - } - } - let mut cmd = std::process::Command::new(CMD_LOGINCTL.as_str()); - if let Some(a) = args { - return cmd.args(a).output(); - } - cmd.output() -} - -/// forever: may not work -#[cfg(target_os = "linux")] -pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ - ("notify-send", [title, msg].to_vec()), - ( - "zenity", - [ - "--info", - "--timeout", - if forever { "0" } else { "3" }, - "--title", - title, - "--text", - msg, - ] - .to_vec(), - ), - ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), - ( - "xmessage", - [ - "-center", - "-timeout", - if forever { "0" } else { "3" }, - title, - msg, - ] - .to_vec(), - ), - ]); - for (k, v) in cmds { - if Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - crate::bail!("failed to post system message"); -} - -#[derive(Debug, Clone, serde_derive::Serialize, serde_derive::Deserialize)] -pub struct WaylandDisplayInfo { - pub name: String, - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, - pub logical_size: Option<(i32, i32)>, - pub refresh_rate: i32, - /// Output rotation in degrees (0/90/180/270), from `wl_output.geometry`. The mode keeps its - /// unrotated dimensions and `logical_size` arrives already swapped, so without this field a - /// rotated output is indistinguishable from a scaled one. Flipped variants map to their - /// rotation. Defaulted so a serialized snapshot from an older probe child still deserializes. - #[serde(default)] - pub transform: i32, -} - -/// The isolated socket-probe fallback, in its own file and behind the `wayland_probe` feature so -/// the base Wayland path never compiles it. The DRM login-screen build turns it on. -#[cfg(feature = "wayland_probe")] -pub mod wayland_probe; -#[cfg(feature = "wayland_probe")] -pub use wayland_probe::{wayland_display_probe_child_main, WAYLAND_DISPLAY_PROBE_ARG}; - -// Retrieves information about all connected displays via the Wayland protocol. -pub fn get_wayland_displays() -> ResultType> { - // Read before connecting: `connect_to_env` consumes `WAYLAND_SOCKET`. Only the probe fallback - // needs this, so it is computed only when that feature is compiled in. - #[cfg(feature = "wayland_probe")] - let named_endpoint = wayland_probe::env_names_wayland_endpoint(); - match Connection::connect_to_env() { - Ok(conn) => collect_wayland_displays(&conn), - // Without the feature, the connect error is final, exactly as before this fallback existed. - #[cfg(not(feature = "wayland_probe"))] - Err(err) => Err(err.into()), - #[cfg(feature = "wayland_probe")] - Err(err) => wayland_probe::wayland_displays_from_runtime_dir(named_endpoint) - .map_err(|fallback_err| anyhow::anyhow!("{err}; {fallback_err}")), - } -} - -/// `wl_output::Transform` as degrees. Flipped variants report their rotation ONLY: wayland -/// defines them as a vertical-axis mirror followed by the rotation, and the mirror half is -/// dropped here - a consumer correcting frames by this value serves a flipped output mirrored. -/// Said once in the log rather than silently, because no compositor of ours produces a flipped -/// output to measure the mirror half against; carrying it must wait for a measured producer. -fn transform_degrees(t: sctk::reexports::client::protocol::wl_output::Transform) -> i32 { - use sctk::reexports::client::protocol::wl_output::Transform; - match t { - Transform::Normal => 0, - Transform::_90 => 90, - Transform::_180 => 180, - Transform::_270 => 270, - Transform::Flipped | Transform::Flipped90 | Transform::Flipped180 - | Transform::Flipped270 => { - static FLIPPED_WARNED: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - if !FLIPPED_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { - log::warn!( - "an output reports a flipped transform ({t:?}); only its rotation is \ - corrected, the mirror is not" - ); - } - match t { - Transform::Flipped90 => 90, - Transform::Flipped180 => 180, - Transform::Flipped270 => 270, - _ => 0, - } - } - _ => 0, - } -} - -fn collect_wayland_displays(conn: &Connection) -> ResultType> { - struct WaylandEnv { - registry_state: RegistryState, - output_state: OutputState, - } - - impl OutputHandler for WaylandEnv { - fn output_state(&mut self) -> &mut OutputState { - &mut self.output_state - } - - fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} - fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} - fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} - } - - impl ProvidesRegistryState for WaylandEnv { - fn registry(&mut self) -> &mut RegistryState { - &mut self.registry_state - } - - sctk::registry_handlers![OutputState]; - } - - sctk::delegate_output!(WaylandEnv); - sctk::delegate_registry!(WaylandEnv); - - let (globals, mut event_queue) = globals::registry_queue_init(conn)?; - let queue_handle = event_queue.handle(); - - let registry_state = RegistryState::new(&globals); - let output_state = OutputState::new(&globals, &queue_handle); - - let mut environment = WaylandEnv { - registry_state, - output_state, - }; - - event_queue.roundtrip(&mut environment)?; - - let outputs: Vec<_> = environment.output_state.outputs().collect(); - let mut display_infos = Vec::new(); - - for output in outputs { - if let Some(output_data) = output.data::() { - output_data.with_output_info(|info| { - if let Some(mode) = info.modes.iter().find(|m| m.current) { - // wlroots compositors leave wl_output.geometry at (0, 0) for every output and - // publish the real layout only through xdg-output, so taking `location` there - // stacks the whole desktop on the origin. Mutter fills both, so this stays a - // no-op on GNOME. - let (x, y) = info.logical_position.unwrap_or(info.location); - let (width, height) = mode.dimensions; - let refresh_rate = mode.refresh_rate; - let name = info.name.clone().unwrap_or_default(); - let logical_size = info.logical_size; - let transform = transform_degrees(info.transform); - display_infos.push(WaylandDisplayInfo { - name, - x, - y, - width, - height, - logical_size, - refresh_rate, - transform, - }); - } - }); - } - } - - Ok(display_infos) -} - -/// Escape a string for safe use in shell commands by wrapping in single quotes. -/// -/// This function handles the edge case of single quotes within the string by: -/// 1. Ending the current single-quoted section -/// 2. Adding an escaped single quote -/// 3. Starting a new single-quoted section -/// -/// Example: "it's here" -> "'it'\''s here'" -#[inline] -pub fn shell_quote(s: &str) -> String { - format!("'{}'", s.replace("'", "'\\''")) -} - -/// Get the current user's home directory via getpwuid (trusted source). -/// -/// This function uses the system's password database (via `getpwuid`) to retrieve -/// the home directory, avoiding the security risk of relying on the `HOME` -/// environment variable which can be manipulated by untrusted input. -/// -/// # Returns -/// - `Some(PathBuf)` if the home directory was found and exists -/// - `None` if the user lookup failed or the directory doesn't exist -/// -/// # Security -/// This function is designed to be safe against confused-deputy attacks where -/// an attacker might manipulate environment variables to influence privileged -/// operations. -pub fn get_home_dir_trusted() -> Option { - let uid = get_current_uid(); - match get_user_by_uid(uid) { - Some(user) => { - let home = user.home_dir(); - if Path::is_dir(home) { - Some(PathBuf::from(home)) - } else { - log::warn!( - "Home directory for uid {} does not exist or is not a directory: {:?}", - uid, - home - ); - None - } - } - None => { - log::warn!("Failed to get user info for uid {}", uid); - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_transform_degrees_maps_all_eight_variants() { - use sctk::reexports::client::protocol::wl_output::Transform; - // Flipped variants report their rotation: the frame still needs that turn to read - // upright, and the mirror half has no producer among desktop compositors to test. - for (t, deg) in [ - (Transform::Normal, 0), - (Transform::_90, 90), - (Transform::_180, 180), - (Transform::_270, 270), - (Transform::Flipped, 0), - (Transform::Flipped90, 90), - (Transform::Flipped180, 180), - (Transform::Flipped270, 270), - ] { - assert_eq!(transform_degrees(t), deg, "{t:?}"); - } - } - - #[test] - fn test_display_info_without_transform_defaults_to_zero() { - // A snapshot serialized by an older probe child carries no transform field; it must - // deserialize with 0 rather than fail, or a greeter-side child update becomes a - // lockstep upgrade. - let old = r#"{"name":"HDMI-1","x":0,"y":0,"width":1920,"height":1080,"logical_size":null,"refresh_rate":60}"#; - let info: WaylandDisplayInfo = serde_json::from_str(old).unwrap(); - assert_eq!(info.transform, 0); - let roundtrip: WaylandDisplayInfo = - serde_json::from_str(&serde_json::to_string(&info).unwrap()).unwrap(); - assert_eq!(roundtrip.transform, 0); - } - - #[test] - fn test_run_cmds_trim_newline() { - assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123"); - assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123"); - assert_eq!( - run_cmds_trim_newline("whoami").unwrap() + "\n", - run_cmds("whoami").unwrap() - ); - } - - /// Test get_home_dir_trusted: returns valid path and ignores HOME env var - #[test] - fn test_get_home_dir_trusted() { - let original_home = std::env::var("HOME").ok(); - - // Set HOME to a fake/malicious path - std::env::set_var("HOME", "/tmp/fake_malicious_home"); - let result = get_home_dir_trusted(); - - // Restore original HOME - match original_home { - Some(home) => std::env::set_var("HOME", home), - None => std::env::remove_var("HOME"), - } - - // Verify: returns valid path that is NOT the fake HOME - if let Some(path) = result { - assert!(path.is_absolute(), "Path should be absolute: {:?}", path); - assert!(path.is_dir(), "Path should be a directory: {:?}", path); - assert_ne!( - path.to_string_lossy(), - "/tmp/fake_malicious_home", - "Should not use HOME env var" - ); - } - } - - /// Test shell_quote with normal strings - #[test] - fn test_shell_quote_normal() { - assert_eq!(shell_quote("hello"), "'hello'"); - assert_eq!(shell_quote("/home/user"), "'/home/user'"); - } - - /// Test shell_quote with spaces - #[test] - fn test_shell_quote_spaces() { - assert_eq!(shell_quote("/home/my user/file"), "'/home/my user/file'"); - assert_eq!(shell_quote("path with spaces"), "'path with spaces'"); - } - - /// Test shell_quote with single quotes (the tricky case) - #[test] - fn test_shell_quote_single_quotes() { - assert_eq!(shell_quote("it's"), "'it'\\''s'"); - assert_eq!(shell_quote("don't stop"), "'don'\\''t stop'"); - } - - /// Test shell_quote with shell metacharacters - #[test] - fn test_shell_quote_metacharacters() { - // These should all be safely quoted - assert_eq!(shell_quote("test;rm -rf /"), "'test;rm -rf /'"); - assert_eq!(shell_quote("$(whoami)"), "'$(whoami)'"); - assert_eq!(shell_quote("`id`"), "'`id`'"); - assert_eq!(shell_quote("a && b"), "'a && b'"); - assert_eq!(shell_quote("a | b"), "'a | b'"); - } -} diff --git a/src/platform/linux/wayland_probe.rs b/src/platform/linux/wayland_probe.rs deleted file mode 100644 index 7ed07218d..000000000 --- a/src/platform/linux/wayland_probe.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! Isolated Wayland display probe: enumerates a compositor over a runtime-directory socket when -//! the environment names no endpoint (a greeter's `--server` and the root service are given no -//! compositor variables). Gated behind the `wayland_probe` feature so the base Wayland path is -//! untouched — a consumer that does not build the DRM login-screen backend never compiles this, -//! and `get_wayland_displays` keeps its original behavior of returning the connect error. - -use super::{collect_wayland_displays, get_values_of_seat0_with_gdm_wayland, WaylandDisplayInfo}; -use crate::{bail, ResultType}; -use sctk::reexports::client::Connection; -use std::path::{Path, PathBuf}; - -const RUNTIME_DIR_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2); - -/// The argument the consumer binary must dispatch to `wayland_display_probe_child_main` before -/// any other startup work; see that function for why the probe is its own process. -pub const WAYLAND_DISPLAY_PROBE_ARG: &str = "--wayland-display-probe"; - -/// First stdout line of a probe child. A binary that does not dispatch the arg never prints it. -const WAYLAND_PROBE_MAGIC: &str = "wayland-display-probe-v1"; - -/// Latched on a failed handshake: a consumer that does not dispatch the probe arg runs its NORMAL -/// startup instead, and this path re-enters every enumeration cycle. -static PROBE_UNSUPPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); - -static RUNTIME_DIR_PROBE_BUSY: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -/// Clears the in-flight flag on every exit path of the parent, error arms included. -struct ProbeBusyGuard; - -impl Drop for ProbeBusyGuard { - fn drop(&mut self) { - RUNTIME_DIR_PROBE_BUSY.store(false, std::sync::atomic::Ordering::Release); - } -} - -/// Entry point of the isolated probe process. The consumer binary dispatches -/// `WAYLAND_DISPLAY_PROBE_ARG` here first, before config, logging or any other startup work. -/// -/// Its own process because the release profile builds with panic=abort: sctk panics on malformed -/// protocol bytes, and in-process that abort takes the whole server down. Here it takes down only -/// this child, which the parent reports as a failed probe. The seat0 lookup also runs in here, so -/// the parent's single deadline bounds the loginctl reads too. -pub fn wayland_display_probe_child_main() -> ! { - use std::io::Write; - // The handshake first, so the parent can tell this entry point ran and not a consumer binary - // that fell through to its normal startup. - println!("{WAYLAND_PROBE_MAGIC}"); - let _ = std::io::stdout().flush(); - let code = match seat0_runtime_dir() - .and_then(|dir| { - drop_to_dir_owner(&dir)?; - probe_runtime_dir(&dir) - }) - .and_then(|displays| serde_json::to_string(&displays).map_err(anyhow::Error::from)) - { - Ok(json) => { - println!("{json}"); - 0 - } - Err(err) => { - eprintln!("{err:#}"); - 1 - } - }; - let _ = std::io::stdout().flush(); - std::process::exit(code) -} - -static ENDPOINT_WAS_NAMED: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -/// Whether the environment ever named a wayland endpoint in this process. Empty is not a name. -/// -/// Read before `connect_to_env`, which removes `WAYLAND_SOCKET` from the environment on both its -/// success and its bad-fd path; and latched, so a consumed variable cannot turn a process that WAS -/// pointed at a compositor into one that is free to go looking for another. -pub(super) fn env_names_wayland_endpoint() -> bool { - use std::sync::atomic::Ordering; - let named = ["WAYLAND_DISPLAY", "WAYLAND_SOCKET"] - .iter() - .any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty())); - if named { - ENDPOINT_WAS_NAMED.store(true, Ordering::Release); - } - ENDPOINT_WAS_NAMED.load(Ordering::Acquire) -} - -/// The probe parses compositor-controlled protocol data; a root service must not do that as -/// root. Before touching the socket, become the runtime directory's owner — and refuse to probe -/// at all if the drop fails, since staying root is the one unacceptable outcome. -fn drop_to_dir_owner(dir: &Path) -> ResultType<()> { - if unsafe { libc::geteuid() } != 0 { - return Ok(()); - } - use std::os::unix::fs::MetadataExt; - let meta = std::fs::metadata(dir)?; - let (uid, gid) = (meta.uid(), meta.gid()); - if uid == 0 { - // Root's own session: there is no boundary to cross and nothing to drop to. - return Ok(()); - } - unsafe { - if libc::setgroups(0, std::ptr::null()) != 0 - || libc::setgid(gid) != 0 - || libc::setuid(uid) != 0 - || libc::setuid(0) == 0 - { - bail!("could not drop privileges for the socket probe"); - } - } - Ok(()) -} - -/// `/run/user/` of the active seat0 session, a greeter included. -/// -/// Derived from the uid rather than read from `XDG_RUNTIME_DIR`: the root service is given no such -/// variable, and `get_home_dir_trusted` refuses to trust the environment for the same reason. -fn seat0_runtime_dir() -> ResultType { - let uid = get_values_of_seat0_with_gdm_wayland(&[1]).remove(0); - if uid.is_empty() || !uid.bytes().all(|b| b.is_ascii_digit()) { - bail!("no active seat0 session to take a runtime directory from"); - } - Ok(PathBuf::from(format!("/run/user/{uid}"))) -} - -/// The wayland sockets present in `dir`, lowest display number first. -/// -/// Scanned rather than guessed: `wl_display_add_socket_auto` takes the first FREE name up to -/// `wayland-32`, and a greeter is where leftovers accumulate across compositor restarts. Only that -/// name pattern, because the same directory holds pipewire and dbus sockets. -fn wayland_sockets_in(dir: &Path) -> Vec { - use std::os::unix::fs::FileTypeExt; - let mut paths: Vec = match std::fs::read_dir(dir) { - Ok(entries) => entries - .flatten() - .filter(|entry| { - let name = entry.file_name(); - let name = name.to_string_lossy(); - name.starts_with("wayland-") - && !name.ends_with(".lock") - && entry.file_type().map(|t| t.is_socket()).unwrap_or(false) - }) - .map(|entry| entry.path()) - .collect(), - Err(_) => Vec::new(), - }; - paths.sort_by_key(|path| { - path.file_name() - .and_then(|name| name.to_str()) - .and_then(|name| name.strip_prefix("wayland-")) - .and_then(|number| number.parse::().ok()) - .unwrap_or(u32::MAX) - }); - paths -} - -/// Enumerate through a socket in the seat0 runtime directory, for the case where nothing named an -/// endpoint: a greeter's `--server` and the root service are given no compositor variables, so -/// nothing tells the enumerator where a compositor that IS running lives. An endpoint that WAS -/// named and failed must not silently reattach to a different compositor. -/// -/// In a subprocess and bounded, because the caller holds a process-wide lock across the call while -/// `connect(2)` parks on a full backlog and sctk's roundtrip polls without a deadline; and because -/// sctk panics on malformed output events, which the release profile's panic=abort turns into an -/// abort of the whole server. A child dies alone, and on the deadline it is killed instead of -/// leaking a thread. The seat0 lookup runs inside the child, under the same deadline. -pub(super) fn wayland_displays_from_runtime_dir( - named_endpoint: bool, -) -> ResultType> { - use std::sync::atomic::Ordering; - if named_endpoint { - bail!("an explicit wayland endpoint is set and did not connect"); - } - if PROBE_UNSUPPORTED.load(Ordering::Acquire) { - bail!("this binary does not dispatch {WAYLAND_DISPLAY_PROBE_ARG}"); - } - if RUNTIME_DIR_PROBE_BUSY.swap(true, Ordering::AcqRel) { - bail!("an earlier probe has not returned"); - } - let _busy = ProbeBusyGuard; - let exe = std::env::current_exe()?; - // Its own process group, so the deadline can kill loginctl descendants along with the child, - // and so no surviving descendant can hold the pipes open past the reads below. - use std::os::unix::process::CommandExt; - let mut child = std::process::Command::new(exe) - .arg(WAYLAND_DISPLAY_PROBE_ARG) - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .process_group(0) - .spawn()?; - let probe_pgid = child.id() as libc::pid_t; - let kill_probe_group = || unsafe { - let _ = libc::kill(-probe_pgid, libc::SIGKILL); - }; - let deadline = std::time::Instant::now() + RUNTIME_DIR_PROBE_TIMEOUT; - let status = loop { - match child.try_wait()? { - Some(status) => { - kill_probe_group(); - break status; - } - None if std::time::Instant::now() >= deadline => { - kill_probe_group(); - // The direct pid too, not only its group: if the child left the group its own - // kill would miss it, and the wait below would then block on a live child. A - // pid-targeted SIGKILL is uncatchable, so wait() is bounded either way. - let _ = child.kill(); - let _ = child.wait(); - // An unwired binary runs its normal startup, and a long-running one (the - // server itself) lands HERE rather than at the handshake check below — latch - // on this path too, or every enumeration cycle spawns a full consumer - // process. Judged by what the child already wrote: a real probe prints the - // magic line first and flushes, so its absence after a whole deadline means - // this is not a probe. Only buffered bytes are read — a blocking read could - // hang on a grandchild that inherited the write end. - match first_buffered_line(child.stdout.take()) { - // The pipe could not be inspected at all: no evidence, no latch. - None => { - bail!("the wayland socket probe timed out and its output was uninspectable") - } - Some(head) if head.as_deref() == Some(WAYLAND_PROBE_MAGIC) => { - bail!("the wayland socket probe did not answer and was killed"); - } - Some(_) => { - PROBE_UNSUPPORTED.store(true, Ordering::Release); - bail!("the wayland socket probe timed out without the handshake; probe disabled"); - } - } - } - None => std::thread::sleep(std::time::Duration::from_millis(25)), - } - }; - // Drained non-blocking, not read_to_string: the child exited so its output is already - // buffered, but a descendant that escaped the process group could still hold a write end open - // and an EOF-seeking read would then hang here forever. - let stdout = drain_nonblocking(child.stdout.take()).unwrap_or_default(); - let stderr = drain_nonblocking(child.stderr.take()).unwrap_or_default(); - let mut lines = stdout.lines(); - if lines.next() != Some(WAYLAND_PROBE_MAGIC) { - // Not a probe: the binary ran its normal startup. Latch, or this path would spawn one - // full consumer process per enumeration cycle. - PROBE_UNSUPPORTED.store(true, Ordering::Release); - bail!("this binary does not dispatch {WAYLAND_DISPLAY_PROBE_ARG}; probe disabled"); - } - if !status.success() { - let detail = stderr.trim(); - if detail.is_empty() { - // panic=abort or a signal leaves stderr empty; the status is then the only cause. - bail!("wayland socket probe failed: {status}"); - } - bail!("wayland socket probe failed ({status}): {detail}"); - } - let displays: Vec = - match serde_json::from_str(lines.next().unwrap_or_default()) { - Ok(displays) => displays, - Err(err) => bail!("wayland socket probe answered a malformed list: {err}"), - }; - // The child already refuses an empty list; refuse it here too, so a truncated pipe cannot - // become a cached-for-life empty enumeration. - if displays.is_empty() { - bail!("wayland socket probe returned no outputs"); - } - log::debug!( - "wayland: {} output(s) via the probe subprocess", - displays.len() - ); - Ok(displays) -} - -/// Everything already buffered in the pipe, read strictly non-blocking and capped: a descendant -/// that escaped the probe's process group can hold a write end open, so a blocking read (even -/// after the child exits) could hang the enumeration forever. `None` means the pipe could not be -/// INSPECTED (missing handle or fcntl failure) and must not be read as evidence of anything; -/// `Some` is whatever bytes were buffered, whether or not EOF arrived. -fn drain_nonblocking(pipe: Option) -> Option { - let mut pipe = pipe?; - let fd = pipe.as_raw_fd(); - unsafe { - let flags = libc::fcntl(fd, libc::F_GETFL); - if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 { - return None; - } - } - // Capped so a descendant that keeps writing cannot spin this read forever. - const CAP: usize = 64 * 1024; - let mut out = Vec::new(); - let mut buf = [0u8; 4096]; - loop { - match pipe.read(&mut buf) { - Ok(0) => break, // EOF: the write end is fully closed - Ok(n) => { - out.extend_from_slice(&buf[..n]); - if out.len() >= CAP { - break; - } - } - Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue, - // WouldBlock: what is buffered is drained (a descendant may still hold the writer). - // Any other error: stop with what we have. - Err(_) => break, - } - } - Some(String::from_utf8_lossy(&out).into_owned()) -} - -/// The first line the child buffered, for the timeout latch decision. `Some(None)` is an -/// inspected-but-empty buffer (genuine absence of the handshake); outer `None` is uninspectable. -fn first_buffered_line(pipe: Option) -> Option> { - drain_nonblocking(pipe).map(|s| s.lines().next().map(str::to_owned)) -} - -fn probe_runtime_dir(dir: &Path) -> ResultType> { - use std::os::unix::net::UnixStream; - let mut errs = Vec::new(); - for path in wayland_sockets_in(dir) { - match UnixStream::connect(&path) - .map_err(anyhow::Error::from) - .and_then(|s| Connection::from_socket(s).map_err(anyhow::Error::from)) - .and_then(|conn| collect_wayland_displays(&conn)) - { - // The caller caches an empty list as ground truth for the process lifetime, and a - // compositor still probing its monitors is exactly what this path connects to. - Ok(displays) if displays.is_empty() => { - errs.push(format!("{}: no outputs yet", path.display())) - } - Ok(displays) => { - // Which socket answered, when nothing in the environment named one. - log::debug!( - "wayland: {} output(s) from {}, found by scanning", - displays.len(), - path.display() - ); - return Ok(displays); - } - Err(err) => errs.push(format!("{}: {err}", path.display())), - } - } - bail!( - "no usable wayland socket in {} ({})", - dir.display(), - if errs.is_empty() { - "none present".to_owned() - } else { - errs.join("; ") - } - ) -} diff --git a/src/platform/macos.rs b/src/platform/macos.rs deleted file mode 100644 index dd83a8738..000000000 --- a/src/platform/macos.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::ResultType; -use osascript; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Serialize)] -struct AlertParams { - title: String, - message: String, - alert_type: String, - buttons: Vec, -} - -#[derive(Deserialize)] -struct AlertResult { - #[serde(rename = "buttonReturned")] - button: String, -} - -/// Firstly run the specified app, then alert a dialog. Return the clicked button value. -/// -/// # Arguments -/// -/// * `app` - The app to execute the script. -/// * `alert_type` - Alert type. . informational, warning, critical -/// * `title` - The alert title. -/// * `message` - The alert message. -/// * `buttons` - The buttons to show. -pub fn alert( - app: String, - alert_type: String, - title: String, - message: String, - buttons: Vec, -) -> ResultType { - let script = osascript::JavaScript::new(&format!( - " - var App = Application('{}'); - App.includeStandardAdditions = true; - return App.displayAlert($params.title, {{ - message: $params.message, - 'as': $params.alert_type, - buttons: $params.buttons, - }}); - ", - app - )); - - let result: AlertResult = script.execute_with_params(AlertParams { - title, - message, - alert_type, - buttons, - })?; - Ok(result.button) -} diff --git a/src/platform/mod.rs b/src/platform/mod.rs deleted file mode 100644 index 6818add40..000000000 --- a/src/platform/mod.rs +++ /dev/null @@ -1,82 +0,0 @@ -#[cfg(target_os = "linux")] -pub mod linux; - -#[cfg(target_os = "macos")] -pub mod macos; - -#[cfg(target_os = "windows")] -pub mod windows; - -#[cfg(not(debug_assertions))] -use crate::{config::Config, log}; -#[cfg(not(debug_assertions))] -use std::process::exit; - -#[cfg(not(debug_assertions))] -static mut GLOBAL_CALLBACK: Option> = None; - -#[cfg(not(debug_assertions))] -extern "C" fn breakdown_signal_handler(sig: i32) { - let mut stack = vec![]; - backtrace::trace(|frame| { - backtrace::resolve_frame(frame, |symbol| { - if let Some(name) = symbol.name() { - stack.push(name.to_string()); - } - }); - true // keep going to the next frame - }); - let mut info = String::default(); - if stack.iter().any(|s| { - s.contains(&"nouveau_pushbuf_kick") - || s.to_lowercase().contains("nvidia") - || s.contains("gdk_window_end_draw_frame") - || s.contains("glGetString") - }) { - Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); - info = "Always use software rendering will be set.".to_string(); - log::info!("{}", info); - } - if stack.iter().any(|s| { - s.to_lowercase().contains("nvidia") - || s.to_lowercase().contains("amf") - || s.to_lowercase().contains("mfx") - || s.contains("cuProfilerStop") - }) { - Config::set_option("enable-hwcodec".to_string(), "N".to_string()); - info = "Perhaps hwcodec causing the crash, disable it first".to_string(); - log::info!("{}", info); - } - log::error!( - "Got signal {} and exit. stack:\n{}", - sig, - stack.join("\n").to_string() - ); - if !info.is_empty() { - #[cfg(target_os = "linux")] - linux::system_message( - "RustDesk", - &format!("Got signal {} and exit.{}", sig, info), - true, - ) - .ok(); - } - unsafe { - #[allow(static_mut_refs)] - if let Some(callback) = &GLOBAL_CALLBACK { - callback() - } - } - exit(0); -} - -#[cfg(not(debug_assertions))] -pub fn register_breakdown_handler(callback: T) -where - T: Fn() + 'static, -{ - unsafe { - GLOBAL_CALLBACK = Some(Box::new(callback)); - libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); - } -} diff --git a/src/platform/windows.rs b/src/platform/windows.rs deleted file mode 100644 index 7481631ac..000000000 --- a/src/platform/windows.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::{ - collections::VecDeque, - sync::{Arc, Mutex}, - time::Instant, -}; -use winapi::{ - shared::minwindef::{DWORD, FALSE, TRUE}, - um::{ - handleapi::CloseHandle, - pdh::{ - PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx, - PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE, - PDH_HCOUNTER, PDH_HQUERY, - }, - synchapi::{CreateEventA, WaitForSingleObject}, - sysinfoapi::VerSetConditionMask, - winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0}, - winnt::{ - HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION, - VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR, - }, - }, -}; - -lazy_static::lazy_static! { - static ref CPU_USAGE_ONE_MINUTE: Arc>> = Arc::new(Mutex::new(None)); -} - -// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs -#[repr(transparent)] -pub struct RAIIHandle(pub HANDLE); - -impl Drop for RAIIHandle { - fn drop(&mut self) { - // This never gives problem except when running under a debugger. - unsafe { CloseHandle(self.0) }; - } -} - -#[repr(transparent)] -pub(self) struct RAIIPDHQuery(pub PDH_HQUERY); - -impl Drop for RAIIPDHQuery { - fn drop(&mut self) { - unsafe { PdhCloseQuery(self.0) }; - } -} - -pub fn start_cpu_performance_monitor() { - // Code from: - // https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data - // https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex - // Why value lower than taskManager: - // https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43 - // Therefore we should compare with Precess Explorer rather than taskManager - - let f = || unsafe { - // load avg or cpu usage, test with prime95. - // Prefer cpu usage because we can get accurate value from Precess Explorer. - // const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0"; - const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0"; - const SAMPLE_INTERVAL: DWORD = 2; // 2 second - - let mut ret; - let mut query: PDH_HQUERY = std::mem::zeroed(); - ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query); - if ret != 0 { - log::error!("PdhOpenQueryA failed: 0x{:X}", ret); - return; - } - let _query = RAIIPDHQuery(query); - let mut counter: PDH_HCOUNTER = std::mem::zeroed(); - ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter); - if ret != 0 { - log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret); - return; - } - ret = PdhCollectQueryData(query); - if ret != 0 { - log::error!("PdhCollectQueryData failed: 0x{:X}", ret); - return; - } - let mut _counter_type: DWORD = 0; - let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed(); - let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _); - if event.is_null() { - log::error!("CreateEventA failed"); - return; - } - let _event: RAIIHandle = RAIIHandle(event); - ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event); - if ret != 0 { - log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret); - return; - } - - let mut queue: VecDeque = VecDeque::new(); - let mut recent_valid: VecDeque = VecDeque::new(); - loop { - // latest one minute - if queue.len() == 31 { - queue.pop_front(); - } - if recent_valid.len() == 31 { - recent_valid.pop_front(); - } - // allow get value within one minute - if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 { - let sum: f64 = queue.iter().map(|f| f.to_owned()).sum(); - let avg = sum / (queue.len() as f64); - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now())); - } else { - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = None; - } - if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) { - recent_valid.push_back(false); - continue; - } - if PdhGetFormattedCounterValue( - counter, - PDH_FMT_DOUBLE, - &mut _counter_type, - &mut counter_value, - ) != 0 - || counter_value.CStatus != 0 - { - recent_valid.push_back(false); - continue; - } - queue.push_back(counter_value.u.doubleValue().clone()); - recent_valid.push_back(true); - } - }; - use std::sync::Once; - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - std::thread::spawn(f); - }); -} - -pub fn cpu_uage_one_minute() -> Option { - let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone(); - if let Some((v, instant)) = v { - if instant.elapsed().as_secs() < 30 { - return Some(v); - } - } - None -} - -pub fn sync_cpu_usage(cpu_usage: Option) { - let v = match cpu_usage { - Some(cpu_usage) => Some((cpu_usage, Instant::now())), - None => None, - }; - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = v; - log::info!("cpu usage synced: {:?}", cpu_usage); -} - -// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 -// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780 -pub fn is_windows_version_or_greater( - os_major: u32, - os_minor: u32, - build_number: u32, - service_pack_major: u32, - service_pack_minor: u32, -) -> bool { - let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() }; - osvi.dwOSVersionInfoSize = std::mem::size_of::() as DWORD; - osvi.dwMajorVersion = os_major as _; - osvi.dwMinorVersion = os_minor as _; - osvi.dwBuildNumber = build_number as _; - osvi.wServicePackMajor = service_pack_major as _; - osvi.wServicePackMinor = service_pack_minor as _; - - let result = unsafe { - let mut condition_mask = 0; - let op = VER_GREATER_EQUAL; - condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op); - condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op); - condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op); - condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op); - condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op); - - VerifyVersionInfoW( - &mut osvi as *mut OSVERSIONINFOEXW, - VER_MAJORVERSION - | VER_MINORVERSION - | VER_BUILDNUMBER - | VER_SERVICEPACKMAJOR - | VER_SERVICEPACKMINOR, - condition_mask, - ) - }; - - result == TRUE -} diff --git a/src/sh.rs b/src/sh.rs new file mode 100644 index 000000000..c6e4bb37b --- /dev/null +++ b/src/sh.rs @@ -0,0 +1,78 @@ +//! Shell and command-path helpers. +//! +//! These live here rather than with the rest of the platform code because +//! `config::patch()` resolves a home directory through them. + +use crate::ResultType; +use std::process::Command; + +// to-do: There seems to be some runtime issue that causes the audit logs to be generated. +// We may need to fix this and remove this workaround in the future. +// +// We use the pre-search method to find the command path to avoid the audit logs on some systems. +// No idea why the audit logs happen. +// Though the audit logs may disappear after rebooting. +// +// See https://github.com/rustdesk/rustdesk/discussions/11959 +// +// `ausearch -x /usr/share/rustdesk/rustdesk` will return +// ... +// time->Tue Jun 24 10:40:43 2025 +// type=PROCTITLE msg=audit(1750776043.446:192757): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365 +// type=PATH msg=audit(1750776043.446:192757): item=0 name="/usr/local/bin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 +// type=CWD msg=audit(1750776043.446:192757): cwd="/" +// type=SYSCALL msg=audit(1750776043.446:192757): arch=c000003e syscall=59 success=no exit=-2 a0=7fb7dbd22da0 a1=1d65f2c0 a2=7ffc25193360 a3=7ffc25194ec0 items=1 ppid=172208 pid=267565 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="rustdesk" exe="/usr/share/rustdesk/rustdesk" subj=unconfined key="processos_criados" +// ---- +// time->Tue Jun 24 10:40:43 2025 +// type=PROCTITLE msg=audit(1750776043.446:192758): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365 +// type=PATH msg=audit(1750776043.446:192758): item=0 name="/usr/sbin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 +// ... +lazy_static::lazy_static! { + pub static ref CMD_LOGINCTL: String = find_cmd_path("loginctl"); + pub static ref CMD_PS: String = find_cmd_path("ps"); + pub static ref CMD_SH: String = find_cmd_path("sh"); +} + +fn find_cmd_path(cmd: &'static str) -> String { + let test_cmd = format!("/bin/{}", cmd); + if std::path::Path::new(&test_cmd).exists() { + return test_cmd; + } + let test_cmd = format!("/usr/bin/{}", cmd); + if std::path::Path::new(&test_cmd).exists() { + return test_cmd; + } + if let Ok(output) = Command::new("which").arg(cmd).output() { + if output.status.success() { + return String::from_utf8_lossy(&output.stdout).trim().to_string(); + } + } + cmd.to_string() +} + +pub fn run_cmds_trim_newline(cmds: &str) -> ResultType { + let output = std::process::Command::new(CMD_SH.as_str()) + .args(vec!["-c", cmds]) + .output()?; + let out = String::from_utf8_lossy(&output.stdout); + Ok(if out.ends_with('\n') { + out[..out.len() - 1].to_string() + } else { + out.to_string() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + // `config::patch()` resolves the root home directory through this, so the + // trailing-newline strip has to keep working in this crate on its own. + #[test] + fn trims_one_trailing_newline() { + assert_eq!(run_cmds_trim_newline("printf 123").unwrap(), "123"); + assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123"); + assert_eq!(run_cmds_trim_newline("printf '1\\n\\n'").unwrap(), "1\n"); + assert_eq!(run_cmds_trim_newline("printf ''").unwrap(), ""); + } +}