mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-12 02:05:14 +03:00
Fix/session scope permission audit (#15469)
* fix: enforce session-scoped permissions Restrict non-remote sessions to their allowed message types, filter out-of-scope login options, and audit rejected or filtered messages. Hide screenshot controls outside default remote sessions. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: typo Signed-off-by: fufesou <linlong1266@gmail.com> * fix: prevent privacy mode in view-camera sessions Signed-off-by: fufesou <linlong1266@gmail.com> * fix: switch display, check non-view-camera Signed-off-by: fufesou <linlong1266@gmail.com> * fix: avoid sending unsupported messages Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, add option to control close/alarm Signed-off-by: fufesou <linlong1266@gmail.com> * Fix: scoped session handling for view-camera compatibility - Skip view-camera auto-login and display-management side effects - Allow harmless render broadcasts without affecting non-video sessions - Keep legacy view-camera management messages compatible as no-ops - Preserve stricter scope violations for non-video session types Signed-off-by: fufesou <linlong1266@gmail.com> * update libs/hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> * fix: ignore repeated login request Signed-off-by: fufesou <linlong1266@gmail.com> * fix: view camera, support "Take screenshot" Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scoped messages, check update options Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, check portforward before conn type voolations Signed-off-by: fufesou <linlong1266@gmail.com> * fix: scoped messages, reduce changes. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix: keep scoped sessions compatible with render broadcasts Allow legacy render-broadcast no-op messages for file transfer and terminal sessions while keeping port forward and mixed options scoped. Also avoid sending new render updates to non-video Flutter sessions. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: scope screenshot requests by video source Key screenshot requests by video source and display index so camera and monitor sessions cannot consume each other's requests. Deduplicate the Flutter render-target predicate while keeping render updates limited to video sessions. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: Harden scoped session message handling Filter option updates by authenticated connection type, keep legacy no-op messages compatible, and avoid noisy repeated scope violation alarms. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix: Send close reason for scoped session violations Signed-off-by: fufesou <linlong1266@gmail.com> * fix: Enforce scoped session message filtering - filter out-of-scope messages for limited session types - scope option updates by authenticated connection type - keep render-broadcast no-op compatibility for non-video scoped sessions - restore view-camera screenshot handling - improve session scope violation audit labels - avoid cloning option messages on the remote hot path Signed-off-by: fufesou <linlong1266@gmail.com> * Fix scoped session clipboard broadcast compatibility Treat text clipboard broadcasts as no-op compatibility messages for FileTransfer and Terminal sessions, matching existing handler behavior and preventing optional scope-violation close from disconnecting those sessions. Keep ViewCamera and PortForward clipboard messages subject to normal scope enforcement. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: log warn Signed-off-by: fufesou <linlong1266@gmail.com> * fix: restrict Flutter clipboard sync to default sessions Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, comments and tests Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, reset sessions in login handle Signed-off-by: fufesou <linlong1266@gmail.com> * fix: session scope, view camera, allow clipboard noop Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -606,7 +606,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
// to-do:
|
// to-do:
|
||||||
// 1. Web desktop
|
// 1. Web desktop
|
||||||
// 2. Mobile, copy the image to the clipboard
|
// 2. Mobile, copy the image to the clipboard
|
||||||
if (isDesktop) {
|
if ((isDefaultConn || ffi.connType == ConnType.viewCamera) && isDesktop) {
|
||||||
final isScreenshotSupported = bind.sessionGetCommonSync(
|
final isScreenshotSupported = bind.sessionGetCommonSync(
|
||||||
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
||||||
if ('true' == isScreenshotSupported) {
|
if ('true' == isScreenshotSupported) {
|
||||||
|
|||||||
Submodule libs/hbb_common updated: a920d00945...7e1c392c62
@@ -1090,6 +1090,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
|
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
|
||||||
|
if self.handler.is_view_camera() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if !self.peer_info.is_support_virtual_display() {
|
if !self.peer_info.is_support_virtual_display() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1111,6 +1114,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) {
|
async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) {
|
||||||
|
if self.handler.is_view_camera() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let lc = self.handler.lc.read().unwrap();
|
let lc = self.handler.lc.read().unwrap();
|
||||||
if lc.version >= hbb_common::get_version_number("1.2.4")
|
if lc.version >= hbb_common::get_version_number("1.2.4")
|
||||||
&& lc.get_toggle_option("privacy-mode")
|
&& lc.get_toggle_option("privacy-mode")
|
||||||
|
|||||||
@@ -1437,7 +1437,7 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
|
|||||||
pub fn update_text_clipboard_required() {
|
pub fn update_text_clipboard_required() {
|
||||||
let is_required = sessions::get_sessions()
|
let is_required = sessions::get_sessions()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| s.is_text_clipboard_required());
|
.any(|s| s.is_default() && s.is_text_clipboard_required());
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
|
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
|
||||||
Client::set_is_text_clipboard_required(is_required);
|
Client::set_is_text_clipboard_required(is_required);
|
||||||
@@ -1447,13 +1447,16 @@ pub fn update_text_clipboard_required() {
|
|||||||
pub fn update_file_clipboard_required() {
|
pub fn update_file_clipboard_required() {
|
||||||
let is_required = sessions::get_sessions()
|
let is_required = sessions::get_sessions()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| s.is_file_clipboard_required());
|
.any(|s| s.is_default() && s.is_file_clipboard_required());
|
||||||
Client::set_is_file_clipboard_required(is_required);
|
Client::set_is_file_clipboard_required(is_required);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
||||||
for s in sessions::get_sessions() {
|
for s in sessions::get_sessions() {
|
||||||
|
if !s.is_default() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
if _is_file {
|
if _is_file {
|
||||||
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
|
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
|
||||||
|
|||||||
@@ -1224,9 +1224,14 @@ pub fn main_set_local_option(key: String, value: String) {
|
|||||||
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
||||||
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
||||||
set_local_option(key, value.clone());
|
set_local_option(key, value.clone());
|
||||||
|
let is_render_target =
|
||||||
|
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
||||||
if is_texture_render_key {
|
if is_texture_render_key {
|
||||||
let session_event = [("v", &value)];
|
let session_event = [("v", &value)];
|
||||||
for session in sessions::get_sessions() {
|
for session in sessions::get_sessions() {
|
||||||
|
if !is_render_target(&session) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
session.push_event("use_texture_render", &session_event, &[]);
|
session.push_event("use_texture_render", &session_event, &[]);
|
||||||
session.use_texture_render_changed();
|
session.use_texture_render_changed();
|
||||||
session.ui_handler.update_use_texture_render();
|
session.ui_handler.update_use_texture_render();
|
||||||
@@ -1234,6 +1239,9 @@ pub fn main_set_local_option(key: String, value: String) {
|
|||||||
}
|
}
|
||||||
if is_d3d_render_key {
|
if is_d3d_render_key {
|
||||||
for session in sessions::get_sessions() {
|
for session in sessions::get_sessions() {
|
||||||
|
if !is_render_target(&session) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
session.update_supported_decodings();
|
session.update_supported_decodings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,18 @@ pub enum AuthConnType {
|
|||||||
Terminal,
|
Terminal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthConnType {
|
||||||
|
fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
AuthConnType::Remote => "remote",
|
||||||
|
AuthConnType::FileTransfer => "file_transfer",
|
||||||
|
AuthConnType::PortForward => "port_forward",
|
||||||
|
AuthConnType::ViewCamera => "view_camera",
|
||||||
|
AuthConnType::Terminal => "terminal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
#[repr(i64)]
|
#[repr(i64)]
|
||||||
enum ConnAuditPrimaryAuth {
|
enum ConnAuditPrimaryAuth {
|
||||||
@@ -384,6 +396,8 @@ pub struct Connection {
|
|||||||
cm_read_job_ids: HashSet<i32>,
|
cm_read_job_ids: HashSet<i32>,
|
||||||
terminal_service_id: String,
|
terminal_service_id: String,
|
||||||
terminal_persistent: bool,
|
terminal_persistent: bool,
|
||||||
|
// Used to avoid too many repeated scope violation warnings.
|
||||||
|
scope_violation_messages: HashSet<&'static str>,
|
||||||
// The user token must be set when terminal is enabled.
|
// The user token must be set when terminal is enabled.
|
||||||
// 0 indicates SYSTEM user
|
// 0 indicates SYSTEM user
|
||||||
// other values indicate current user
|
// other values indicate current user
|
||||||
@@ -571,6 +585,7 @@ impl Connection {
|
|||||||
cm_read_job_ids: HashSet::new(),
|
cm_read_job_ids: HashSet::new(),
|
||||||
terminal_service_id: "".to_owned(),
|
terminal_service_id: "".to_owned(),
|
||||||
terminal_persistent: false,
|
terminal_persistent: false,
|
||||||
|
scope_violation_messages: HashSet::new(),
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
terminal_user_token: None,
|
terminal_user_token: None,
|
||||||
terminal_generic_service: None,
|
terminal_generic_service: None,
|
||||||
@@ -1501,6 +1516,23 @@ impl Connection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn post_session_scope_violation_alarm(&self, message: &'static str) {
|
||||||
|
let conn_type = self
|
||||||
|
.authed_conn_type()
|
||||||
|
.map(AuthConnType::as_str)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
self.post_alarm_audit(
|
||||||
|
AlarmAuditType::SessionScopeViolation,
|
||||||
|
json!({
|
||||||
|
"id": self.lr.my_id.clone(),
|
||||||
|
"name": self.lr.my_name.clone(),
|
||||||
|
"ip": &self.ip,
|
||||||
|
"conn_type": conn_type,
|
||||||
|
"message": message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
async fn post_audit_async(url: String, v: Value) -> ResultType<String> {
|
async fn post_audit_async(url: String, v: Value) -> ResultType<String> {
|
||||||
crate::post_request(url, v.to_string(), "").await
|
crate::post_request(url, v.to_string(), "").await
|
||||||
@@ -1897,10 +1929,9 @@ impl Connection {
|
|||||||
let mut msg_out = Message::new();
|
let mut msg_out = Message::new();
|
||||||
msg_out.set_login_response(res);
|
msg_out.set_login_response(res);
|
||||||
self.send(msg_out).await;
|
self.send(msg_out).await;
|
||||||
if let Some(o) = self.options_in_login.take() {
|
self.update_scoped_login_options().await;
|
||||||
self.update_options(&o).await;
|
|
||||||
}
|
|
||||||
if let Some((dir, show_hidden)) = self.file_transfer.clone() {
|
if let Some((dir, show_hidden)) = self.file_transfer.clone() {
|
||||||
|
self.keyboard = false;
|
||||||
let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() {
|
let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() {
|
||||||
&dir
|
&dir
|
||||||
} else {
|
} else {
|
||||||
@@ -2407,6 +2438,14 @@ impl Connection {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_session_scope_for_login(&mut self) {
|
||||||
|
self.file_transfer = None;
|
||||||
|
self.view_camera = false;
|
||||||
|
self.terminal = false;
|
||||||
|
self.port_forward_address.clear();
|
||||||
|
self.terminal_persistent = false;
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
|
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
|
||||||
self.lr = lr.clone();
|
self.lr = lr.clone();
|
||||||
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
|
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
|
||||||
@@ -2474,12 +2513,21 @@ impl Connection {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.authorized {
|
||||||
|
if matches!(msg.union.as_ref(), Some(message::Union::LoginRequest(_))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if let Some(message) = self.authorized_scope_violation(&msg) {
|
||||||
|
return self.handle_authorized_scope_violation(message).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
// After handling CloseReason messages, proceed to process other message types
|
// After handling CloseReason messages, proceed to process other message types
|
||||||
if let Some(message::Union::LoginRequest(lr)) = msg.union {
|
if let Some(message::Union::LoginRequest(lr)) = msg.union {
|
||||||
self.handle_login_request_without_validation(&lr).await;
|
self.handle_login_request_without_validation(&lr).await;
|
||||||
if self.authorized {
|
if self.authorized {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
self.reset_session_scope_for_login();
|
||||||
match lr.union {
|
match lr.union {
|
||||||
Some(login_request::Union::FileTransfer(ft)) => {
|
Some(login_request::Union::FileTransfer(ft)) => {
|
||||||
if !Self::permission(
|
if !Self::permission(
|
||||||
@@ -2989,7 +3037,7 @@ impl Connection {
|
|||||||
self.update_auto_disconnect_timer();
|
self.update_auto_disconnect_timer();
|
||||||
}
|
}
|
||||||
Some(message::Union::Clipboard(cb)) => {
|
Some(message::Union::Clipboard(cb)) => {
|
||||||
if self.clipboard_enabled() {
|
if self.should_handle_text_clipboard_message() && self.clipboard_enabled() {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(vec![cb], ClipboardSide::Host);
|
update_clipboard(vec![cb], ClipboardSide::Host);
|
||||||
// ios as the controlled side is actually not supported for now.
|
// ios as the controlled side is actually not supported for now.
|
||||||
@@ -3017,7 +3065,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(message::Union::MultiClipboards(_mcb)) => {
|
Some(message::Union::MultiClipboards(_mcb)) => {
|
||||||
if self.clipboard_enabled() {
|
if self.should_handle_text_clipboard_message() && self.clipboard_enabled() {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(_mcb.clipboards, ClipboardSide::Host);
|
update_clipboard(_mcb.clipboards, ClipboardSide::Host);
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
@@ -3404,10 +3452,14 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
Some(misc::Union::ToggleVirtualDisplay(t)) => {
|
Some(misc::Union::ToggleVirtualDisplay(t)) => {
|
||||||
self.toggle_virtual_display(t).await;
|
if !self.view_camera {
|
||||||
|
self.toggle_virtual_display(t).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(misc::Union::TogglePrivacyMode(t)) => {
|
Some(misc::Union::TogglePrivacyMode(t)) => {
|
||||||
self.toggle_privacy_mode(t).await;
|
if !self.view_camera {
|
||||||
|
self.toggle_privacy_mode(t).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(misc::Union::ChatMessage(c)) => {
|
Some(misc::Union::ChatMessage(c)) => {
|
||||||
self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
|
self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
|
||||||
@@ -3415,19 +3467,27 @@ impl Connection {
|
|||||||
self.update_auto_disconnect_timer();
|
self.update_auto_disconnect_timer();
|
||||||
}
|
}
|
||||||
Some(misc::Union::Option(o)) => {
|
Some(misc::Union::Option(o)) => {
|
||||||
self.update_options(&o).await;
|
if self.authed_conn_type() == Some(AuthConnType::Remote) {
|
||||||
|
self.update_options(&o).await;
|
||||||
|
} else if let Some(option) = self.scoped_update_option_message(&o) {
|
||||||
|
self.update_options(&option).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(misc::Union::RefreshVideo(r)) => {
|
Some(misc::Union::RefreshVideo(r)) => {
|
||||||
if r {
|
if self.should_handle_render_broadcast_message() {
|
||||||
// Refresh all videos.
|
if r {
|
||||||
// Compatibility with old versions and sciter(remote).
|
// Refresh all videos.
|
||||||
self.refresh_video_display(None);
|
// Compatibility with old versions and sciter(remote).
|
||||||
|
self.refresh_video_display(None);
|
||||||
|
}
|
||||||
|
self.update_auto_disconnect_timer();
|
||||||
}
|
}
|
||||||
self.update_auto_disconnect_timer();
|
|
||||||
}
|
}
|
||||||
Some(misc::Union::RefreshVideoDisplay(display)) => {
|
Some(misc::Union::RefreshVideoDisplay(display)) => {
|
||||||
self.refresh_video_display(Some(display as usize));
|
if self.should_handle_render_broadcast_message() {
|
||||||
self.update_auto_disconnect_timer();
|
self.refresh_video_display(Some(display as usize));
|
||||||
|
self.update_auto_disconnect_timer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(misc::Union::VideoReceived(_)) => {
|
Some(misc::Union::VideoReceived(_)) => {
|
||||||
video_service::notify_video_frame_fetched_by_conn_id(
|
video_service::notify_video_frame_fetched_by_conn_id(
|
||||||
@@ -3495,10 +3555,16 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Some(misc::Union::ChangeResolution(r)) => self.change_resolution(None, &r),
|
Some(misc::Union::ChangeResolution(r)) => {
|
||||||
|
if !self.view_camera {
|
||||||
|
self.change_resolution(None, &r);
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Some(misc::Union::ChangeDisplayResolution(dr)) => {
|
Some(misc::Union::ChangeDisplayResolution(dr)) => {
|
||||||
self.change_resolution(Some(dr.display as _), &dr.resolution)
|
if !self.view_camera {
|
||||||
|
self.change_resolution(Some(dr.display as _), &dr.resolution);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
@@ -3584,6 +3650,7 @@ impl Connection {
|
|||||||
Some(message::Union::ScreenshotRequest(request)) => {
|
Some(message::Union::ScreenshotRequest(request)) => {
|
||||||
if let Some(tx) = self.inner.tx.clone() {
|
if let Some(tx) = self.inner.tx.clone() {
|
||||||
crate::video_service::set_take_screenshot(
|
crate::video_service::set_take_screenshot(
|
||||||
|
self.video_source(),
|
||||||
request.display as _,
|
request.display as _,
|
||||||
request.sid.clone(),
|
request.sid.clone(),
|
||||||
tx,
|
tx,
|
||||||
@@ -4080,7 +4147,7 @@ impl Connection {
|
|||||||
self.switch_display_to(display_idx, server.clone());
|
self.switch_display_to(display_idx, server.clone());
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if s.width != 0 && s.height != 0 {
|
if !self.view_camera && s.width != 0 && s.height != 0 {
|
||||||
self.change_resolution(
|
self.change_resolution(
|
||||||
None,
|
None,
|
||||||
&Resolution {
|
&Resolution {
|
||||||
@@ -5204,6 +5271,398 @@ impl Connection {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_handle_render_broadcast_message(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.authed_conn_type(),
|
||||||
|
Some(AuthConnType::Remote | AuthConnType::ViewCamera)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_handle_text_clipboard_message(&self) -> bool {
|
||||||
|
matches!(self.authed_conn_type(), Some(AuthConnType::Remote))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scoped_update_option_message(&self, option: &OptionMessage) -> Option<OptionMessage> {
|
||||||
|
match self.authed_conn_type() {
|
||||||
|
Some(AuthConnType::ViewCamera) => Self::scoped_view_camera_option(option).0,
|
||||||
|
Some(AuthConnType::Terminal) => Self::scoped_terminal_login_option(option).0,
|
||||||
|
Some(AuthConnType::Remote | AuthConnType::FileTransfer | AuthConnType::PortForward)
|
||||||
|
| None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authed_conn_type(&self) -> Option<AuthConnType> {
|
||||||
|
self.authed_conn_id.as_ref().map(|id| id.conn_type())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_authorized_scope_violation(&mut self, message: &'static str) -> bool {
|
||||||
|
let conn_type = self
|
||||||
|
.authed_conn_type()
|
||||||
|
.map(AuthConnType::as_str)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let is_first = self.scope_violation_messages.insert(message);
|
||||||
|
if is_first {
|
||||||
|
log::warn!(
|
||||||
|
"Received out-of-scope message in {} session: {}",
|
||||||
|
conn_type,
|
||||||
|
message
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Received repeated out-of-scope message in {} session: {}",
|
||||||
|
conn_type,
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if is_first && Config::get_bool_option(keys::OPTION_ALLOW_SCOPE_VIOLATION_ALARM) {
|
||||||
|
self.post_session_scope_violation_alarm(message);
|
||||||
|
}
|
||||||
|
if Config::get_bool_option(keys::OPTION_ALLOW_SCOPE_VIOLATION_CLOSE) {
|
||||||
|
self.send_close_reason_no_retry("Connection not allowed")
|
||||||
|
.await;
|
||||||
|
self.on_close("Session scope violation", true).await;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorized_scope_violation(&self, msg: &Message) -> Option<&'static str> {
|
||||||
|
let Some(conn_type) = self.authed_conn_type() else {
|
||||||
|
return (!Self::is_connection_housekeeping_message(msg)).then_some("session.auth_type");
|
||||||
|
};
|
||||||
|
Self::authorized_message_scope_violation(conn_type, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_scoped_login_options(&mut self) {
|
||||||
|
let Some(option) = self.options_in_login.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(conn_type) = self.authed_conn_type() else {
|
||||||
|
// Unreachable, but just in case, we drop the options if the connection type is unknown.
|
||||||
|
log::warn!(
|
||||||
|
"Dropping scoped login options because authorized connection type is unknown"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (scoped, violation) = Self::scoped_login_option(conn_type, &option);
|
||||||
|
if let Some(message) = violation {
|
||||||
|
log::debug!(
|
||||||
|
"Filtering {} session login options outside scope: {}",
|
||||||
|
conn_type.as_str(),
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(option) = scoped {
|
||||||
|
self.update_options(&option).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scoped_login_option(
|
||||||
|
conn_type: AuthConnType,
|
||||||
|
option: &OptionMessage,
|
||||||
|
) -> (Option<OptionMessage>, Option<&'static str>) {
|
||||||
|
match conn_type {
|
||||||
|
AuthConnType::Remote => (Some(option.clone()), None),
|
||||||
|
AuthConnType::ViewCamera => Self::scoped_view_camera_option(option),
|
||||||
|
AuthConnType::Terminal => Self::scoped_terminal_login_option(option),
|
||||||
|
AuthConnType::FileTransfer | AuthConnType::PortForward => {
|
||||||
|
let violation = Self::option_has_any_field(option).then_some("login.option");
|
||||||
|
(None, violation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scoped_terminal_login_option(
|
||||||
|
option: &OptionMessage,
|
||||||
|
) -> (Option<OptionMessage>, Option<&'static str>) {
|
||||||
|
let mut scoped = OptionMessage::new();
|
||||||
|
let mut violation = false;
|
||||||
|
match option.terminal_persistent.enum_value() {
|
||||||
|
Ok(value) => scoped.terminal_persistent = value.into(),
|
||||||
|
Err(_) => violation = true,
|
||||||
|
}
|
||||||
|
if Self::option_has_non_terminal_login_field(option) {
|
||||||
|
violation = true;
|
||||||
|
}
|
||||||
|
let scoped = Self::option_has_any_field(&scoped).then_some(scoped);
|
||||||
|
(scoped, violation.then_some("login.option"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorized_message_scope_violation(
|
||||||
|
conn_type: AuthConnType,
|
||||||
|
msg: &Message,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
if Self::is_connection_housekeeping_message(msg) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Legacy clients can broadcast render-refresh messages to all opened sessions.
|
||||||
|
// Clipboard messages may also be broadcast to FileTransfer/Terminal sessions while
|
||||||
|
// the client still considers text clipboard sync required, and handlers ignore them.
|
||||||
|
let noop_compat = match conn_type {
|
||||||
|
AuthConnType::FileTransfer | AuthConnType::Terminal => {
|
||||||
|
Self::is_render_broadcast_noop_compat_message(msg)
|
||||||
|
|| Self::is_text_clipboard_noop_compat_message(msg)
|
||||||
|
}
|
||||||
|
AuthConnType::PortForward => Self::is_render_broadcast_noop_compat_message(msg),
|
||||||
|
AuthConnType::ViewCamera => Self::is_text_clipboard_noop_compat_message(msg),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if noop_compat {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let allowed = match conn_type {
|
||||||
|
AuthConnType::Remote => true,
|
||||||
|
AuthConnType::FileTransfer => Self::is_file_transfer_scoped_message(msg),
|
||||||
|
AuthConnType::PortForward => false,
|
||||||
|
AuthConnType::ViewCamera => Self::is_view_camera_scoped_message(msg),
|
||||||
|
AuthConnType::Terminal => Self::is_terminal_scoped_message(msg),
|
||||||
|
};
|
||||||
|
(!allowed).then(|| Self::message_family(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_render_broadcast_noop_compat_message(msg: &Message) -> bool {
|
||||||
|
let Some(message::Union::Misc(misc)) = msg.union.as_ref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match misc.union.as_ref() {
|
||||||
|
Some(misc::Union::RefreshVideo(_)) | Some(misc::Union::RefreshVideoDisplay(_)) => true,
|
||||||
|
Some(misc::Union::Option(option)) => Self::is_supported_decoding_only_option(option),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_text_clipboard_noop_compat_message(msg: &Message) -> bool {
|
||||||
|
matches!(
|
||||||
|
msg.union.as_ref(),
|
||||||
|
Some(message::Union::Clipboard(_)) | Some(message::Union::MultiClipboards(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported_decoding_only_option(option: &OptionMessage) -> bool {
|
||||||
|
option.supported_decoding.is_some()
|
||||||
|
&& option.image_quality.enum_value() == Ok(ImageQuality::NotSet)
|
||||||
|
&& option.custom_image_quality == 0
|
||||||
|
&& option.custom_fps == 0
|
||||||
|
&& Self::is_bool_option_not_set(option.lock_after_session_end)
|
||||||
|
&& Self::is_bool_option_not_set(option.show_remote_cursor)
|
||||||
|
&& Self::is_bool_option_not_set(option.privacy_mode)
|
||||||
|
&& Self::is_bool_option_not_set(option.block_input)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_audio)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_clipboard)
|
||||||
|
&& Self::is_bool_option_not_set(option.enable_file_transfer)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_keyboard)
|
||||||
|
&& Self::is_bool_option_not_set(option.follow_remote_cursor)
|
||||||
|
&& Self::is_bool_option_not_set(option.follow_remote_window)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_camera)
|
||||||
|
&& Self::is_bool_option_not_set(option.terminal_persistent)
|
||||||
|
&& Self::is_bool_option_not_set(option.show_my_cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_connection_housekeeping_message(msg: &Message) -> bool {
|
||||||
|
match msg.union.as_ref() {
|
||||||
|
Some(message::Union::LoginRequest(_)) => true,
|
||||||
|
Some(message::Union::TestDelay(_)) => true,
|
||||||
|
Some(message::Union::Misc(misc)) => {
|
||||||
|
matches!(misc.union.as_ref(), Some(misc::Union::CloseReason(_)))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file_transfer_scoped_message(msg: &Message) -> bool {
|
||||||
|
match msg.union.as_ref() {
|
||||||
|
Some(message::Union::FileAction(_)) | Some(message::Union::FileResponse(_)) => true,
|
||||||
|
Some(message::Union::Misc(misc)) => Self::is_file_transfer_scoped_misc(misc),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file_transfer_scoped_misc(misc: &Misc) -> bool {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if matches!(misc.union.as_ref(), Some(misc::Union::SelectedSid(_))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let _ = misc;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_terminal_scoped_message(msg: &Message) -> bool {
|
||||||
|
match msg.union.as_ref() {
|
||||||
|
Some(message::Union::TerminalAction(_)) => true,
|
||||||
|
Some(message::Union::Misc(misc)) => Self::is_terminal_scoped_misc(misc),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_terminal_scoped_misc(misc: &Misc) -> bool {
|
||||||
|
match misc.union.as_ref() {
|
||||||
|
Some(misc::Union::ChatMessage(_)) => true,
|
||||||
|
Some(misc::Union::Option(option)) => Self::is_terminal_scoped_option(option),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_terminal_scoped_option(option: &OptionMessage) -> bool {
|
||||||
|
Self::scoped_terminal_login_option(option).1.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_view_camera_scoped_message(msg: &Message) -> bool {
|
||||||
|
match msg.union.as_ref() {
|
||||||
|
Some(message::Union::ScreenshotRequest(_)) => true,
|
||||||
|
Some(message::Union::Misc(misc)) => Self::is_view_camera_scoped_misc(misc),
|
||||||
|
// Legacy clients may send auto-login input during view-camera connect.
|
||||||
|
// The handlers intentionally ignore these messages for view-camera sessions.
|
||||||
|
Some(message::Union::MouseEvent(_))
|
||||||
|
| Some(message::Union::PointerDeviceEvent(_))
|
||||||
|
| Some(message::Union::KeyEvent(_)) => true,
|
||||||
|
Some(message::Union::AudioFrame(_))
|
||||||
|
| Some(message::Union::VoiceCallRequest(_))
|
||||||
|
| Some(message::Union::VoiceCallResponse(_)) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_view_camera_scoped_misc(misc: &Misc) -> bool {
|
||||||
|
match misc.union.as_ref() {
|
||||||
|
Some(misc::Union::SwitchDisplay(_))
|
||||||
|
| Some(misc::Union::CaptureDisplays(_))
|
||||||
|
| Some(misc::Union::RefreshVideo(_))
|
||||||
|
| Some(misc::Union::RefreshVideoDisplay(_))
|
||||||
|
| Some(misc::Union::VideoReceived(_))
|
||||||
|
| Some(misc::Union::ChatMessage(_))
|
||||||
|
| Some(misc::Union::AudioFormat(_))
|
||||||
|
| Some(misc::Union::ClientRecordStatus(_))
|
||||||
|
// Though these messages are not expected in normal view-camera sessions,
|
||||||
|
// keep them allowed to avoid breaking existing clients that may send them.
|
||||||
|
| Some(misc::Union::MessageQuery(_))
|
||||||
|
| Some(misc::Union::TogglePrivacyMode(_))
|
||||||
|
| Some(misc::Union::ToggleVirtualDisplay(_))
|
||||||
|
| Some(misc::Union::ChangeResolution(_))
|
||||||
|
| Some(misc::Union::ChangeDisplayResolution(_)) => true,
|
||||||
|
Some(misc::Union::Option(option)) => Self::is_view_camera_scoped_option(option),
|
||||||
|
#[cfg(windows)]
|
||||||
|
Some(misc::Union::SelectedSid(_)) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_view_camera_scoped_option(option: &OptionMessage) -> bool {
|
||||||
|
Self::scoped_view_camera_option(option).1.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep these OptionMessage field lists in sync with message.proto and update_options().
|
||||||
|
// New fields must be classified here before limited session types can receive them.
|
||||||
|
fn scoped_view_camera_option(
|
||||||
|
option: &OptionMessage,
|
||||||
|
) -> (Option<OptionMessage>, Option<&'static str>) {
|
||||||
|
let mut scoped = OptionMessage::new();
|
||||||
|
let mut violation = false;
|
||||||
|
if option.image_quality.enum_value().is_ok() {
|
||||||
|
scoped.image_quality = option.image_quality;
|
||||||
|
}
|
||||||
|
if option.custom_image_quality >= 0 {
|
||||||
|
scoped.custom_image_quality = option.custom_image_quality;
|
||||||
|
}
|
||||||
|
if option.custom_fps >= 0 {
|
||||||
|
scoped.custom_fps = option.custom_fps;
|
||||||
|
}
|
||||||
|
scoped.supported_decoding = option.supported_decoding.clone();
|
||||||
|
if let Ok(value) = option.disable_audio.enum_value() {
|
||||||
|
scoped.disable_audio = value.into();
|
||||||
|
}
|
||||||
|
if Self::option_has_non_view_camera_login_field(option) {
|
||||||
|
violation = true;
|
||||||
|
}
|
||||||
|
let scoped = Self::option_has_any_field(&scoped).then_some(scoped);
|
||||||
|
(scoped, violation.then_some("login.option"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_has_non_view_camera_login_field(option: &OptionMessage) -> bool {
|
||||||
|
!(Self::is_bool_option_not_set(option.lock_after_session_end)
|
||||||
|
&& Self::is_bool_option_not_set(option.show_remote_cursor)
|
||||||
|
&& Self::is_bool_option_not_set(option.privacy_mode)
|
||||||
|
&& Self::is_bool_option_not_set(option.block_input)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_clipboard)
|
||||||
|
&& Self::is_bool_option_not_set(option.enable_file_transfer)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_keyboard)
|
||||||
|
&& Self::is_bool_option_not_set(option.follow_remote_cursor)
|
||||||
|
&& Self::is_bool_option_not_set(option.follow_remote_window)
|
||||||
|
&& Self::is_bool_option_not_set(option.disable_camera)
|
||||||
|
&& Self::is_bool_option_not_set(option.terminal_persistent)
|
||||||
|
&& Self::is_bool_option_not_set(option.show_my_cursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_has_non_terminal_login_field(option: &OptionMessage) -> bool {
|
||||||
|
option.image_quality.enum_value() != Ok(ImageQuality::NotSet)
|
||||||
|
|| option.custom_image_quality != 0
|
||||||
|
|| option.custom_fps != 0
|
||||||
|
|| option.supported_decoding.is_some()
|
||||||
|
|| !Self::is_bool_option_not_set(option.lock_after_session_end)
|
||||||
|
|| !Self::is_bool_option_not_set(option.show_remote_cursor)
|
||||||
|
|| !Self::is_bool_option_not_set(option.privacy_mode)
|
||||||
|
|| !Self::is_bool_option_not_set(option.block_input)
|
||||||
|
|| !Self::is_bool_option_not_set(option.disable_audio)
|
||||||
|
|| !Self::is_bool_option_not_set(option.disable_clipboard)
|
||||||
|
|| !Self::is_bool_option_not_set(option.enable_file_transfer)
|
||||||
|
|| !Self::is_bool_option_not_set(option.disable_keyboard)
|
||||||
|
|| !Self::is_bool_option_not_set(option.follow_remote_cursor)
|
||||||
|
|| !Self::is_bool_option_not_set(option.follow_remote_window)
|
||||||
|
|| !Self::is_bool_option_not_set(option.disable_camera)
|
||||||
|
|| !Self::is_bool_option_not_set(option.show_my_cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_has_any_field(option: &OptionMessage) -> bool {
|
||||||
|
Self::option_has_non_terminal_login_field(option)
|
||||||
|
|| !Self::is_bool_option_not_set(option.terminal_persistent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_bool_option_not_set(option: hbb_common::protobuf::EnumOrUnknown<BoolOption>) -> bool {
|
||||||
|
option.enum_value() == Ok(BoolOption::NotSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_family(msg: &Message) -> &'static str {
|
||||||
|
match msg.union.as_ref() {
|
||||||
|
Some(message::Union::MouseEvent(_)) => "mouse_event",
|
||||||
|
Some(message::Union::AudioFrame(_)) => "audio_frame",
|
||||||
|
Some(message::Union::PointerDeviceEvent(_)) => "pointer_device_event",
|
||||||
|
Some(message::Union::KeyEvent(_)) => "key_event",
|
||||||
|
Some(message::Union::Clipboard(_)) => "clipboard",
|
||||||
|
Some(message::Union::FileAction(_)) => "file_action",
|
||||||
|
Some(message::Union::FileResponse(_)) => "file_response",
|
||||||
|
Some(message::Union::VoiceCallRequest(_)) => "voice_call_request",
|
||||||
|
Some(message::Union::VoiceCallResponse(_)) => "voice_call_response",
|
||||||
|
Some(message::Union::MultiClipboards(_)) => "multi_clipboards",
|
||||||
|
Some(message::Union::ScreenshotRequest(_)) => "screenshot_request",
|
||||||
|
Some(message::Union::ScreenshotResponse(_)) => "screenshot_response",
|
||||||
|
Some(message::Union::TerminalAction(_)) => "terminal_action",
|
||||||
|
Some(message::Union::TerminalResponse(_)) => "terminal_response",
|
||||||
|
Some(message::Union::Misc(misc)) => Self::misc_message_family(misc),
|
||||||
|
Some(_) => "message.other",
|
||||||
|
None => "empty",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn misc_message_family(misc: &Misc) -> &'static str {
|
||||||
|
match misc.union.as_ref() {
|
||||||
|
Some(misc::Union::ChatMessage(_)) => "misc.chat_message",
|
||||||
|
Some(misc::Union::SwitchDisplay(_)) => "misc.switch_display",
|
||||||
|
Some(misc::Union::Option(_)) => "misc.option",
|
||||||
|
Some(misc::Union::AudioFormat(_)) => "misc.audio_format",
|
||||||
|
Some(misc::Union::CaptureDisplays(_)) => "misc.capture_displays",
|
||||||
|
Some(misc::Union::ClientRecordStatus(_)) => "misc.client_record_status",
|
||||||
|
Some(misc::Union::TogglePrivacyMode(_)) => "misc.toggle_privacy_mode",
|
||||||
|
Some(misc::Union::ToggleVirtualDisplay(_)) => "misc.toggle_virtual_display",
|
||||||
|
Some(misc::Union::SelectedSid(_)) => "misc.selected_sid",
|
||||||
|
Some(misc::Union::ChangeResolution(_)) => "misc.change_resolution",
|
||||||
|
Some(misc::Union::ChangeDisplayResolution(_)) => "misc.change_display_resolution",
|
||||||
|
Some(misc::Union::MessageQuery(_)) => "misc.message_query",
|
||||||
|
Some(misc::Union::FollowCurrentDisplay(_)) => "misc.follow_current_display",
|
||||||
|
Some(_) => "misc.other",
|
||||||
|
None => "misc.empty",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) {
|
async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) {
|
||||||
let is_stopping_allowed = clip.is_stopping_allowed();
|
let is_stopping_allowed = clip.is_stopping_allowed();
|
||||||
@@ -5599,6 +6058,7 @@ pub enum AlarmAuditType {
|
|||||||
ExceedIPv6PrefixAttempts = 6,
|
ExceedIPv6PrefixAttempts = 6,
|
||||||
TerminalOsLoginBackoff = 7,
|
TerminalOsLoginBackoff = 7,
|
||||||
TerminalOsLoginConcurrency = 8,
|
TerminalOsLoginConcurrency = 8,
|
||||||
|
SessionScopeViolation = 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum FileAuditType {
|
pub enum FileAuditType {
|
||||||
@@ -6216,6 +6676,7 @@ mod raii {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -6258,4 +6719,325 @@ mod test {
|
|||||||
assert!(Ipv6Addr::from_str("127.0.0.1").is_err());
|
assert!(Ipv6Addr::from_str("127.0.0.1").is_err());
|
||||||
assert!(Ipv6Addr::from_str("0").is_err());
|
assert!(Ipv6Addr::from_str("0").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn msg(set: impl FnOnce(&mut Message)) -> Message {
|
||||||
|
let mut msg = Message::new();
|
||||||
|
set(&mut msg);
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
fn misc_msg(set: impl FnOnce(&mut Misc)) -> Message {
|
||||||
|
msg(|msg| {
|
||||||
|
let mut misc = Misc::new();
|
||||||
|
set(&mut misc);
|
||||||
|
msg.set_misc(misc);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_msg(set: impl FnOnce(&mut OptionMessage)) -> Message {
|
||||||
|
misc_msg(|misc| {
|
||||||
|
let mut option = OptionMessage::new();
|
||||||
|
set(&mut option);
|
||||||
|
misc.set_option(option);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_supported_decoding(option: &mut OptionMessage) {
|
||||||
|
option.supported_decoding = hbb_common::protobuf::MessageField::some(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_scopes(
|
||||||
|
conn_type: AuthConnType,
|
||||||
|
cases: impl IntoIterator<Item = (Message, Option<&'static str>)>,
|
||||||
|
) {
|
||||||
|
for (msg, expected) in cases {
|
||||||
|
assert_eq!(
|
||||||
|
Connection::authorized_message_scope_violation(conn_type, &msg),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_scope_allows_only_messages_for_authenticated_session_type() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
AuthConnType::FileTransfer,
|
||||||
|
vec![
|
||||||
|
(msg(|m| m.set_file_action(FileAction::new())), None),
|
||||||
|
(msg(|m| m.set_file_response(FileResponse::new())), None),
|
||||||
|
(msg(|m| m.set_login_request(LoginRequest::new())), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_screenshot_request(ScreenshotRequest::new())),
|
||||||
|
Some("screenshot_request"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_capture_displays(CaptureDisplays::new())),
|
||||||
|
Some("misc.capture_displays"),
|
||||||
|
),
|
||||||
|
(msg(|m| m.set_clipboard(Clipboard::new())), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_multi_clipboards(MultiClipboards::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(misc_msg(|m| m.set_refresh_video(true)), None),
|
||||||
|
(misc_msg(|m| m.set_refresh_video_display(0)), None),
|
||||||
|
(
|
||||||
|
option_msg(|o| {
|
||||||
|
o.supported_decoding =
|
||||||
|
hbb_common::protobuf::MessageField::some(Default::default())
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
option_msg(|o| {
|
||||||
|
o.supported_decoding =
|
||||||
|
hbb_common::protobuf::MessageField::some(Default::default());
|
||||||
|
o.disable_audio = BoolOption::Yes.into();
|
||||||
|
}),
|
||||||
|
Some("misc.option"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AuthConnType::Terminal,
|
||||||
|
vec![
|
||||||
|
(msg(|m| m.set_terminal_action(TerminalAction::new())), None),
|
||||||
|
(
|
||||||
|
option_msg(|o| o.terminal_persistent = BoolOption::Yes.into()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_screenshot_request(ScreenshotRequest::new())),
|
||||||
|
Some("screenshot_request"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_file_action(FileAction::new())),
|
||||||
|
Some("file_action"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_toggle_privacy_mode(TogglePrivacyMode::new())),
|
||||||
|
Some("misc.toggle_privacy_mode"),
|
||||||
|
),
|
||||||
|
(misc_msg(|m| m.set_chat_message(ChatMessage::new())), None),
|
||||||
|
(msg(|m| m.set_clipboard(Clipboard::new())), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_multi_clipboards(MultiClipboards::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_toggle_virtual_display(ToggleVirtualDisplay::new())),
|
||||||
|
Some("misc.toggle_virtual_display"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_change_resolution(Resolution::new())),
|
||||||
|
Some("misc.change_resolution"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_change_display_resolution(DisplayResolution::new())),
|
||||||
|
Some("misc.change_display_resolution"),
|
||||||
|
),
|
||||||
|
(misc_msg(|m| m.set_refresh_video(true)), None),
|
||||||
|
(misc_msg(|m| m.set_refresh_video_display(0)), None),
|
||||||
|
(
|
||||||
|
option_msg(|o| {
|
||||||
|
o.supported_decoding =
|
||||||
|
hbb_common::protobuf::MessageField::some(Default::default())
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
option_msg(|o| {
|
||||||
|
o.supported_decoding =
|
||||||
|
hbb_common::protobuf::MessageField::some(Default::default());
|
||||||
|
o.disable_audio = BoolOption::Yes.into();
|
||||||
|
}),
|
||||||
|
Some("misc.option"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AuthConnType::ViewCamera,
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_switch_display(SwitchDisplay::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(misc_msg(|m| m.set_chat_message(ChatMessage::new())), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_voice_call_request(VoiceCallRequest::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(msg(|m| m.set_audio_frame(AudioFrame::new())), None),
|
||||||
|
(
|
||||||
|
option_msg(|o| o.image_quality = ImageQuality::Balanced.into()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_toggle_privacy_mode(TogglePrivacyMode::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_toggle_virtual_display(ToggleVirtualDisplay::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_change_resolution(Resolution::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
misc_msg(|m| m.set_change_display_resolution(DisplayResolution::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(msg(|m| m.set_mouse_event(MouseEvent::new())), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_pointer_device_event(PointerDeviceEvent::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(msg(|m| m.set_key_event(KeyEvent::new())), None),
|
||||||
|
(misc_msg(|m| m.set_client_record_status(true)), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_file_response(FileResponse::new())),
|
||||||
|
Some("file_response"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_terminal_action(TerminalAction::new())),
|
||||||
|
Some("terminal_action"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AuthConnType::Remote,
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
msg(|m| m.set_screenshot_request(ScreenshotRequest::new())),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(msg(|m| m.set_terminal_action(TerminalAction::new())), None),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AuthConnType::PortForward,
|
||||||
|
vec![
|
||||||
|
(msg(|m| m.set_test_delay(TestDelay::new())), None),
|
||||||
|
(misc_msg(|m| m.set_close_reason("closed".to_owned())), None),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_file_action(FileAction::new())),
|
||||||
|
Some("file_action"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_terminal_action(TerminalAction::new())),
|
||||||
|
Some("terminal_action"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
msg(|m| m.set_screenshot_request(ScreenshotRequest::new())),
|
||||||
|
Some("screenshot_request"),
|
||||||
|
),
|
||||||
|
(misc_msg(|m| m.set_refresh_video(true)), None),
|
||||||
|
(misc_msg(|m| m.set_refresh_video_display(0)), None),
|
||||||
|
(
|
||||||
|
option_msg(|o| {
|
||||||
|
o.supported_decoding =
|
||||||
|
hbb_common::protobuf::MessageField::some(Default::default())
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (conn_type, messages) in cases {
|
||||||
|
assert_scopes(conn_type, messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_scope_login_options_are_limited_to_authenticated_session_type() {
|
||||||
|
let mut option = OptionMessage::new();
|
||||||
|
option.image_quality = ImageQuality::Balanced.into();
|
||||||
|
option.disable_audio = BoolOption::Yes.into();
|
||||||
|
option.block_input = BoolOption::Yes.into();
|
||||||
|
option.privacy_mode = BoolOption::Yes.into();
|
||||||
|
|
||||||
|
let (scoped, violation) =
|
||||||
|
Connection::scoped_login_option(AuthConnType::ViewCamera, &option);
|
||||||
|
let scoped = scoped.unwrap();
|
||||||
|
assert_eq!(violation, Some("login.option"));
|
||||||
|
assert_eq!(
|
||||||
|
scoped.image_quality.enum_value(),
|
||||||
|
Ok(ImageQuality::Balanced)
|
||||||
|
);
|
||||||
|
assert_eq!(scoped.disable_audio.enum_value(), Ok(BoolOption::Yes));
|
||||||
|
assert_eq!(scoped.block_input.enum_value(), Ok(BoolOption::NotSet));
|
||||||
|
assert_eq!(scoped.privacy_mode.enum_value(), Ok(BoolOption::NotSet));
|
||||||
|
|
||||||
|
let (scoped, violation) =
|
||||||
|
Connection::scoped_login_option(AuthConnType::FileTransfer, &option);
|
||||||
|
assert!(scoped.is_none());
|
||||||
|
assert_eq!(violation, Some("login.option"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_scope_limited_render_noop_options_reject_mixed_fields() {
|
||||||
|
for conn_type in [
|
||||||
|
AuthConnType::FileTransfer,
|
||||||
|
AuthConnType::Terminal,
|
||||||
|
AuthConnType::PortForward,
|
||||||
|
] {
|
||||||
|
let supported_decoding_only = option_msg(set_supported_decoding);
|
||||||
|
assert_eq!(
|
||||||
|
Connection::authorized_message_scope_violation(conn_type, &supported_decoding_only),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let mixed_option = option_msg(|o| {
|
||||||
|
set_supported_decoding(o);
|
||||||
|
o.disable_audio = BoolOption::Yes.into();
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
Connection::authorized_message_scope_violation(conn_type, &mixed_option),
|
||||||
|
Some("misc.option")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_scope_view_camera_options_keep_only_camera_fields() {
|
||||||
|
let mut option = OptionMessage::new();
|
||||||
|
option.image_quality = ImageQuality::Balanced.into();
|
||||||
|
option.custom_image_quality = 80;
|
||||||
|
option.custom_fps = 24;
|
||||||
|
set_supported_decoding(&mut option);
|
||||||
|
option.disable_audio = BoolOption::Yes.into();
|
||||||
|
option.block_input = BoolOption::Yes.into();
|
||||||
|
option.disable_clipboard = BoolOption::Yes.into();
|
||||||
|
option.enable_file_transfer = BoolOption::Yes.into();
|
||||||
|
option.terminal_persistent = BoolOption::Yes.into();
|
||||||
|
|
||||||
|
let (scoped, violation) =
|
||||||
|
Connection::scoped_login_option(AuthConnType::ViewCamera, &option);
|
||||||
|
let scoped = scoped.unwrap();
|
||||||
|
assert_eq!(violation, Some("login.option"));
|
||||||
|
assert_eq!(
|
||||||
|
scoped.image_quality.enum_value(),
|
||||||
|
Ok(ImageQuality::Balanced)
|
||||||
|
);
|
||||||
|
assert_eq!(scoped.custom_image_quality, 80);
|
||||||
|
assert_eq!(scoped.custom_fps, 24);
|
||||||
|
assert!(scoped.supported_decoding.is_some());
|
||||||
|
assert_eq!(scoped.disable_audio.enum_value(), Ok(BoolOption::Yes));
|
||||||
|
assert_eq!(scoped.block_input.enum_value(), Ok(BoolOption::NotSet));
|
||||||
|
assert_eq!(
|
||||||
|
scoped.disable_clipboard.enum_value(),
|
||||||
|
Ok(BoolOption::NotSet)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scoped.enable_file_transfer.enum_value(),
|
||||||
|
Ok(BoolOption::NotSet)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scoped.terminal_persistent.enum_value(),
|
||||||
|
Ok(BoolOption::NotSet)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ lazy_static::lazy_static! {
|
|||||||
pub static ref VIDEO_QOS: Arc<Mutex<VideoQoS>> = Default::default();
|
pub static ref VIDEO_QOS: Arc<Mutex<VideoQoS>> = Default::default();
|
||||||
pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default();
|
pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default();
|
||||||
pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default();
|
pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default();
|
||||||
static ref SCREENSHOTS: Mutex<HashMap<usize, Screenshot>> = Default::default();
|
static ref SCREENSHOTS: Mutex<HashMap<(VideoSource, usize), Screenshot>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Screenshot {
|
struct Screenshot {
|
||||||
@@ -192,7 +192,7 @@ impl VideoFrameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum VideoSource {
|
pub enum VideoSource {
|
||||||
Monitor,
|
Monitor,
|
||||||
Camera,
|
Camera,
|
||||||
@@ -725,7 +725,8 @@ fn run(vs: VideoService) -> ResultType<()> {
|
|||||||
Ok(frame) => {
|
Ok(frame) => {
|
||||||
repeat_encode_counter = 0;
|
repeat_encode_counter = 0;
|
||||||
if frame.valid() {
|
if frame.valid() {
|
||||||
let screenshot = SCREENSHOTS.lock().unwrap().remove(&display_idx);
|
let screenshot_key = (vs.source, display_idx);
|
||||||
|
let screenshot = SCREENSHOTS.lock().unwrap().remove(&screenshot_key);
|
||||||
if let Some(mut screenshot) = screenshot {
|
if let Some(mut screenshot) = screenshot {
|
||||||
let restore_vram = screenshot.restore_vram;
|
let restore_vram = screenshot.restore_vram;
|
||||||
let (msg, w, h, data) = match &frame {
|
let (msg, w, h, data) = match &frame {
|
||||||
@@ -754,7 +755,10 @@ fn run(vs: VideoService) -> ResultType<()> {
|
|||||||
#[cfg(all(windows, feature = "vram"))]
|
#[cfg(all(windows, feature = "vram"))]
|
||||||
VRamEncoder::set_not_use(sp.name(), true);
|
VRamEncoder::set_not_use(sp.name(), true);
|
||||||
screenshot.restore_vram = true;
|
screenshot.restore_vram = true;
|
||||||
SCREENSHOTS.lock().unwrap().insert(display_idx, screenshot);
|
SCREENSHOTS
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(screenshot_key, screenshot);
|
||||||
_raii.try_vram = false;
|
_raii.try_vram = false;
|
||||||
bail!("SWITCH");
|
bail!("SWITCH");
|
||||||
}
|
}
|
||||||
@@ -1348,9 +1352,9 @@ fn check_qos(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_take_screenshot(display_idx: usize, sid: String, tx: Sender) {
|
pub fn set_take_screenshot(source: VideoSource, display_idx: usize, sid: String, tx: Sender) {
|
||||||
SCREENSHOTS.lock().unwrap().insert(
|
SCREENSHOTS.lock().unwrap().insert(
|
||||||
display_idx,
|
(source, display_idx),
|
||||||
Screenshot {
|
Screenshot {
|
||||||
sid,
|
sid,
|
||||||
tx,
|
tx,
|
||||||
|
|||||||
@@ -1809,10 +1809,12 @@ impl<T: InvokeUiSession> Interface for Session<T> {
|
|||||||
self.msgbox("error", "Error", msg, "");
|
self.msgbox("error", "Error", msg, "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.try_change_init_resolution(pi.current_display);
|
if !self.is_view_camera() {
|
||||||
let p = self.lc.read().unwrap().should_auto_login();
|
self.try_change_init_resolution(pi.current_display);
|
||||||
if !p.is_empty() {
|
let p = self.lc.read().unwrap().should_auto_login();
|
||||||
input_os_password(p, true, self.clone());
|
if !p.is_empty() {
|
||||||
|
input_os_password(p, true, self.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let current = &pi.displays[pi.current_display as usize];
|
let current = &pi.displays[pi.current_display as usize];
|
||||||
self.set_display(
|
self.set_display(
|
||||||
|
|||||||
Reference in New Issue
Block a user