mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-06 07:56:46 +08: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:
|
||||
// 1. Web desktop
|
||||
// 2. Mobile, copy the image to the clipboard
|
||||
if (isDesktop) {
|
||||
if ((isDefaultConn || ffi.connType == ConnType.viewCamera) && isDesktop) {
|
||||
final isScreenshotSupported = bind.sessionGetCommonSync(
|
||||
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
||||
if ('true' == isScreenshotSupported) {
|
||||
|
||||
+1
-1
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) {
|
||||
if self.handler.is_view_camera() {
|
||||
return;
|
||||
}
|
||||
if !self.peer_info.is_support_virtual_display() {
|
||||
return;
|
||||
}
|
||||
@@ -1111,6 +1114,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
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();
|
||||
if lc.version >= hbb_common::get_version_number("1.2.4")
|
||||
&& lc.get_toggle_option("privacy-mode")
|
||||
|
||||
+5
-2
@@ -1437,7 +1437,7 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
|
||||
pub fn update_text_clipboard_required() {
|
||||
let is_required = sessions::get_sessions()
|
||||
.iter()
|
||||
.any(|s| s.is_text_clipboard_required());
|
||||
.any(|s| s.is_default() && s.is_text_clipboard_required());
|
||||
#[cfg(target_os = "android")]
|
||||
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(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() {
|
||||
let is_required = sessions::get_sessions()
|
||||
.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);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
||||
for s in sessions::get_sessions() {
|
||||
if !s.is_default() {
|
||||
continue;
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if _is_file {
|
||||
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_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
||||
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 {
|
||||
let session_event = [("v", &value)];
|
||||
for session in sessions::get_sessions() {
|
||||
if !is_render_target(&session) {
|
||||
continue;
|
||||
}
|
||||
session.push_event("use_texture_render", &session_event, &[]);
|
||||
session.use_texture_render_changed();
|
||||
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 {
|
||||
for session in sessions::get_sessions() {
|
||||
if !is_render_target(&session) {
|
||||
continue;
|
||||
}
|
||||
session.update_supported_decodings();
|
||||
}
|
||||
}
|
||||
|
||||
+800
-18
@@ -240,6 +240,18 @@ pub enum AuthConnType {
|
||||
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)]
|
||||
#[repr(i64)]
|
||||
enum ConnAuditPrimaryAuth {
|
||||
@@ -384,6 +396,8 @@ pub struct Connection {
|
||||
cm_read_job_ids: HashSet<i32>,
|
||||
terminal_service_id: String,
|
||||
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.
|
||||
// 0 indicates SYSTEM user
|
||||
// other values indicate current user
|
||||
@@ -571,6 +585,7 @@ impl Connection {
|
||||
cm_read_job_ids: HashSet::new(),
|
||||
terminal_service_id: "".to_owned(),
|
||||
terminal_persistent: false,
|
||||
scope_violation_messages: HashSet::new(),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
terminal_user_token: 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]
|
||||
async fn post_audit_async(url: String, v: Value) -> ResultType<String> {
|
||||
crate::post_request(url, v.to_string(), "").await
|
||||
@@ -1897,10 +1929,9 @@ impl Connection {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
if let Some(o) = self.options_in_login.take() {
|
||||
self.update_options(&o).await;
|
||||
}
|
||||
self.update_scoped_login_options().await;
|
||||
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() {
|
||||
&dir
|
||||
} 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) {
|
||||
self.lr = lr.clone();
|
||||
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
|
||||
@@ -2474,12 +2513,21 @@ impl Connection {
|
||||
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
|
||||
if let Some(message::Union::LoginRequest(lr)) = msg.union {
|
||||
self.handle_login_request_without_validation(&lr).await;
|
||||
if self.authorized {
|
||||
return true;
|
||||
}
|
||||
self.reset_session_scope_for_login();
|
||||
match lr.union {
|
||||
Some(login_request::Union::FileTransfer(ft)) => {
|
||||
if !Self::permission(
|
||||
@@ -2989,7 +3037,7 @@ impl Connection {
|
||||
self.update_auto_disconnect_timer();
|
||||
}
|
||||
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")))]
|
||||
update_clipboard(vec![cb], ClipboardSide::Host);
|
||||
// ios as the controlled side is actually not supported for now.
|
||||
@@ -3017,7 +3065,7 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
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")))]
|
||||
update_clipboard(_mcb.clipboards, ClipboardSide::Host);
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -3404,10 +3452,14 @@ impl Connection {
|
||||
}
|
||||
#[cfg(windows)]
|
||||
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)) => {
|
||||
self.toggle_privacy_mode(t).await;
|
||||
if !self.view_camera {
|
||||
self.toggle_privacy_mode(t).await;
|
||||
}
|
||||
}
|
||||
Some(misc::Union::ChatMessage(c)) => {
|
||||
self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
|
||||
@@ -3415,19 +3467,27 @@ impl Connection {
|
||||
self.update_auto_disconnect_timer();
|
||||
}
|
||||
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)) => {
|
||||
if r {
|
||||
// Refresh all videos.
|
||||
// Compatibility with old versions and sciter(remote).
|
||||
self.refresh_video_display(None);
|
||||
if self.should_handle_render_broadcast_message() {
|
||||
if r {
|
||||
// Refresh all videos.
|
||||
// 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)) => {
|
||||
self.refresh_video_display(Some(display as usize));
|
||||
self.update_auto_disconnect_timer();
|
||||
if self.should_handle_render_broadcast_message() {
|
||||
self.refresh_video_display(Some(display as usize));
|
||||
self.update_auto_disconnect_timer();
|
||||
}
|
||||
}
|
||||
Some(misc::Union::VideoReceived(_)) => {
|
||||
video_service::notify_video_frame_fetched_by_conn_id(
|
||||
@@ -3495,10 +3555,16 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
#[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")))]
|
||||
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(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -3584,6 +3650,7 @@ impl Connection {
|
||||
Some(message::Union::ScreenshotRequest(request)) => {
|
||||
if let Some(tx) = self.inner.tx.clone() {
|
||||
crate::video_service::set_take_screenshot(
|
||||
self.video_source(),
|
||||
request.display as _,
|
||||
request.sid.clone(),
|
||||
tx,
|
||||
@@ -4080,7 +4147,7 @@ impl Connection {
|
||||
self.switch_display_to(display_idx, server.clone());
|
||||
|
||||
#[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(
|
||||
None,
|
||||
&Resolution {
|
||||
@@ -5204,6 +5271,398 @@ impl Connection {
|
||||
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")]
|
||||
async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) {
|
||||
let is_stopping_allowed = clip.is_stopping_allowed();
|
||||
@@ -5599,6 +6058,7 @@ pub enum AlarmAuditType {
|
||||
ExceedIPv6PrefixAttempts = 6,
|
||||
TerminalOsLoginBackoff = 7,
|
||||
TerminalOsLoginConcurrency = 8,
|
||||
SessionScopeViolation = 9,
|
||||
}
|
||||
|
||||
pub enum FileAuditType {
|
||||
@@ -6216,6 +6676,7 @@ mod raii {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[allow(unused)]
|
||||
use super::*;
|
||||
@@ -6258,4 +6719,325 @@ mod test {
|
||||
assert!(Ipv6Addr::from_str("127.0.0.1").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 IS_UAC_RUNNING: 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 {
|
||||
@@ -192,7 +192,7 @@ impl VideoFrameController {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum VideoSource {
|
||||
Monitor,
|
||||
Camera,
|
||||
@@ -725,7 +725,8 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
Ok(frame) => {
|
||||
repeat_encode_counter = 0;
|
||||
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 {
|
||||
let restore_vram = screenshot.restore_vram;
|
||||
let (msg, w, h, data) = match &frame {
|
||||
@@ -754,7 +755,10 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
#[cfg(all(windows, feature = "vram"))]
|
||||
VRamEncoder::set_not_use(sp.name(), true);
|
||||
screenshot.restore_vram = true;
|
||||
SCREENSHOTS.lock().unwrap().insert(display_idx, screenshot);
|
||||
SCREENSHOTS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(screenshot_key, screenshot);
|
||||
_raii.try_vram = false;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
@@ -1348,9 +1352,9 @@ fn check_qos(
|
||||
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(
|
||||
display_idx,
|
||||
(source, display_idx),
|
||||
Screenshot {
|
||||
sid,
|
||||
tx,
|
||||
|
||||
@@ -1809,10 +1809,12 @@ impl<T: InvokeUiSession> Interface for Session<T> {
|
||||
self.msgbox("error", "Error", msg, "");
|
||||
return;
|
||||
}
|
||||
self.try_change_init_resolution(pi.current_display);
|
||||
let p = self.lc.read().unwrap().should_auto_login();
|
||||
if !p.is_empty() {
|
||||
input_os_password(p, true, self.clone());
|
||||
if !self.is_view_camera() {
|
||||
self.try_change_init_resolution(pi.current_display);
|
||||
let p = self.lc.read().unwrap().should_auto_login();
|
||||
if !p.is_empty() {
|
||||
input_os_password(p, true, self.clone());
|
||||
}
|
||||
}
|
||||
let current = &pi.displays[pi.current_display as usize];
|
||||
self.set_display(
|
||||
|
||||
Reference in New Issue
Block a user