Compare commits

..

5 Commits

Author SHA1 Message Date
21pages
1abc897c45 fix avatar fallback (#14458)
* fix avatar fallback

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(ui): improve avatar fallback handling and layout consistency

  - Always show spacing in account section regardless of avatar presence
  - Handle null return from buildAvatarWidget with proper fallback
  - Adjust mobile settings avatar size to 28

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-05 12:30:40 +08:00
RustDesk
ab64a32f30 avatar (#14440)
* avatar

* refactor avatar display: unify rendering and resolve at use time

  - Extract buildAvatarWidget() in common.dart to share avatar rendering
    logic across desktop settings, desktop CM and mobile CM
  - Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
    to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
  - Store avatar as-is in local config, only resolve when displaying
    (settings page) or sending (LoginRequest)
  - Resolve avatar in LoginRequest before sending to remote peer
  - Add error handling for network image load failures
  - Guard against empty client.name[0] crash
  - Show avatar in mobile settings page account tile

Signed-off-by: 21pages <sunboeasy@gmail.com>

* web: implement mainResolveAvatarUrl via js getByName

Signed-off-by: 21pages <sunboeasy@gmail.com>

* increase ipc Data enum size limit to 120 bytes

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-03-04 21:43:19 +08:00
RustDesk
52b66e71d1 Move port mapping afterwards (#14448)
* move port mapping after auth in port forwarding

* fix(port-forward): try connect after 2fa

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(security): gate port-forward connect on full auth and clarify login flow semantics

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(port-forward): comments and logs

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-03-04 15:48:42 +08:00
fufesou
41ab5bbdd8 fix(update): macos, test before update (#14446)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-03 10:47:32 +08:00
fufesou
732b250815 fix(keyboard): legacy mode (#14435)
* fix(keyboard): legacy mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* Simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): legacy mode, chr to seq

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): legacy mode, early return if (!hotkey)&down

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): legacy mode, pair down/up

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-02 19:07:09 +08:00
17 changed files with 244 additions and 165 deletions

View File

@@ -4118,3 +4118,43 @@ String mouseButtonsToPeer(int buttons) {
return ''; return '';
} }
} }
/// Build an avatar widget from an avatar URL or data URI string.
/// Returns [fallback] if avatar is empty or cannot be decoded.
/// [borderRadius] defaults to [size]/2 (circle).
Widget? buildAvatarWidget({
required String avatar,
required double size,
double? borderRadius,
Widget? fallback,
}) {
final trimmed = avatar.trim();
if (trimmed.isEmpty) return fallback;
ImageProvider? imageProvider;
if (trimmed.startsWith('data:image/')) {
final comma = trimmed.indexOf(',');
if (comma > 0) {
try {
imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1)));
} catch (_) {}
}
} else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
imageProvider = NetworkImage(trimmed);
}
if (imageProvider == null) return fallback;
final radius = borderRadius ?? size / 2;
return ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image(
image: imageProvider,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
fallback ?? SizedBox.shrink(),
),
);
}

View File

@@ -2039,7 +2039,7 @@ class _AccountState extends State<_Account> {
return Row( return Row(
children: [ children: [
if (avatarWidget != null) avatarWidget, if (avatarWidget != null) avatarWidget,
if (avatarWidget != null) const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -2061,7 +2061,8 @@ class _AccountState extends State<_Account> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Theme.of(context).textTheme.bodySmall?.color, color:
Theme.of(context).textTheme.bodySmall?.color,
), ),
), ),
), ),
@@ -2076,28 +2077,13 @@ class _AccountState extends State<_Account> {
} }
Widget? _buildUserAvatar() { Widget? _buildUserAvatar() {
final avatar = gFFI.userModel.avatar.value.trim(); // Resolve relative avatar path at display time
if (avatar.isEmpty) return null; final avatar =
const radius = 22.0; bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
if (avatar.startsWith('data:image/')) { return buildAvatarWidget(
final comma = avatar.indexOf(','); avatar: avatar,
if (comma > 0) { size: 44,
try {
return CircleAvatar(
radius: radius,
backgroundImage: MemoryImage(base64Decode(avatar.substring(comma + 1))),
); );
} catch (_) {
return null;
}
}
} else if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
return CircleAvatar(
radius: radius,
backgroundImage: NetworkImage(avatar),
);
}
return null;
} }
} }

View File

@@ -1,7 +1,6 @@
// original cm window in Sciter version. // original cm window in Sciter version.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -569,37 +568,13 @@ class _CmHeaderState extends State<_CmHeader>
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
Widget _buildClientAvatar() { Widget _buildClientAvatar() {
const borderRadius = BorderRadius.all(Radius.circular(15.0)); return buildAvatarWidget(
final avatar = client.avatar.trim(); avatar: client.avatar,
if (avatar.startsWith('data:image/')) { size: 70,
final comma = avatar.indexOf(','); borderRadius: 15,
if (comma > 0) { fallback: _buildInitialAvatar(),
try { ) ??
final bytes = base64Decode(avatar.substring(comma + 1)); _buildInitialAvatar();
return ClipRRect(
borderRadius: borderRadius,
child: Image.memory(
bytes,
width: 70,
height: 70,
fit: BoxFit.cover,
),
);
} catch (_) {}
}
} else if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
return ClipRRect(
borderRadius: borderRadius,
child: Image.network(
avatar,
width: 70,
height: 70,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildInitialAvatar(),
),
);
}
return _buildInitialAvatar();
} }
Widget _buildInitialAvatar() { Widget _buildInitialAvatar() {
@@ -612,7 +587,7 @@ class _CmHeaderState extends State<_CmHeader>
borderRadius: BorderRadius.circular(15.0), borderRadius: BorderRadius.circular(15.0),
), ),
child: Text( child: Text(
client.name[0], client.name.isNotEmpty ? client.name[0] : '?',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -857,28 +856,17 @@ class ClientInfo extends StatelessWidget {
} }
Widget _buildAvatar(BuildContext context) { Widget _buildAvatar(BuildContext context) {
final avatar = client.avatar.trim(); final fallback = CircleAvatar(
if (avatar.isNotEmpty) { backgroundColor: str2color(client.name,
if (avatar.startsWith('data:image/')) {
final comma = avatar.indexOf(',');
if (comma > 0) {
try {
return CircleAvatar(
backgroundImage: MemoryImage(base64Decode(avatar.substring(comma + 1))),
);
} catch (_) {}
}
} else if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
return CircleAvatar(backgroundImage: NetworkImage(avatar));
}
}
// Show character as before if no avatar
return CircleAvatar(
backgroundColor: str2color(
client.name,
Theme.of(context).brightness == Brightness.light ? 255 : 150), Theme.of(context).brightness == Brightness.light ? 255 : 150),
child: Text(client.name[0]), child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
); );
return buildAvatarWidget(
avatar: client.avatar,
size: 40,
fallback: fallback,
) ??
fallback;
} }
} }

View File

@@ -689,7 +689,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login') ? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')), : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
leading: Icon(Icons.person), leading: Obx(() {
final avatar = bind.mainResolveAvatarUrl(
avatar: gFFI.userModel.avatar.value);
return buildAvatarWidget(
avatar: avatar,
size: 28,
borderRadius: null,
fallback: Icon(Icons.person),
) ??
Icon(Icons.person);
}),
onPressed: (context) { onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
loginDialog(); loginDialog();
@@ -829,10 +839,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
), ),
if (!incomingOnly) if (!incomingOnly)
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('keep-awake-during-outgoing-sessions-label')), title:
Text(translate('keep-awake-during-outgoing-sessions-label')),
initialValue: _preventSleepWhileConnected, initialValue: _preventSleepWhileConnected,
onToggle: (v) async { onToggle: (v) async {
await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v); await mainSetLocalBoolOption(
kOptionKeepAwakeDuringOutgoingSessions, v);
setState(() { setState(() {
_preventSleepWhileConnected = v; _preventSleepWhileConnected = v;
}); });

View File

@@ -34,6 +34,7 @@ class UserModel {
} }
return '$preferred (@$username)'; return '$preferred (@$username)';
} }
WeakReference<FFI> parent; WeakReference<FFI> parent;
UserModel(this.parent) { UserModel(this.parent) {
@@ -88,7 +89,6 @@ class UserModel {
} }
final user = UserPayload.fromJson(data); final user = UserPayload.fromJson(data);
user.avatar = _resolveAvatar(user.avatar, url);
_parseAndUpdateUser(user); _parseAndUpdateUser(user);
} catch (e) { } catch (e) {
debugPrint('Failed to refreshCurrentUser: $e'); debugPrint('Failed to refreshCurrentUser: $e');
@@ -138,7 +138,6 @@ class UserModel {
avatar.value = user.avatar; avatar.value = user.avatar;
isAdmin.value = user.isAdmin; isAdmin.value = user.isAdmin;
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user)); bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
_updateLocalUserInfo();
if (isWeb) { if (isWeb) {
// ugly here, tmp solution // ugly here, tmp solution
bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? ''); bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? '');

View File

@@ -2034,5 +2034,9 @@ class RustdeskImpl {
return false; return false;
} }
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
}
void dispose() {} void dispose() {}
} }

View File

@@ -269,7 +269,7 @@ impl KeyboardControllable for Enigo {
for pos in 0..mod_len { for pos in 0..mod_len {
let rpos = mod_len - 1 - pos; let rpos = mod_len - 1 - pos;
if flag & (0x0001 << rpos) != 0 { if flag & (0x0001 << rpos) != 0 {
self.key_up(modifiers[pos]); self.key_up(modifiers[rpos]);
} }
} }
@@ -298,8 +298,19 @@ impl KeyboardControllable for Enigo {
} }
fn key_up(&mut self, key: Key) { fn key_up(&mut self, key: Key) {
match key {
Key::Layout(c) => {
let code = self.get_layoutdependent_keycode(c);
if code as u16 != 0xFFFF {
let vk = code & 0x00FF;
keybd_event(KEYEVENTF_KEYUP, vk, 0);
}
}
_ => {
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
} }
}
}
fn get_key_state(&mut self, key: Key) -> bool { fn get_key_state(&mut self, key: Key) -> bool {
let keycode = self.key_to_keycode(key); let keycode = self.key_to_keycode(key);

View File

@@ -33,7 +33,7 @@ use crate::{
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
kcp_stream::KcpStream, kcp_stream::KcpStream,
secure_tcp, secure_tcp,
ui_interface::{get_builtin_option, use_texture_render}, ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
ui_session_interface::{InvokeUiSession, Session}, ui_session_interface::{InvokeUiSession, Session},
}; };
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
@@ -2638,6 +2638,7 @@ impl LoginConfigHandler {
}) })
.unwrap_or_default(); .unwrap_or_default();
} }
avatar = resolve_avatar_url(avatar);
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME); let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
if display_name.is_empty() { if display_name.is_empty() {
display_name = display_name =

View File

@@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String {
get_api_server() get_api_server()
} }
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
SyncReturn(resolve_avatar_url(avatar))
}
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) { pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
http_request(url, method, body, header) http_request(url, method, body, header)
} }

View File

@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
const QUERY_INTERVAL_SECS: f32 = 1.0; const QUERY_INTERVAL_SECS: f32 = 1.0;
const QUERY_TIMEOUT_SECS: u64 = 60 * 3; const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";

View File

@@ -1584,6 +1584,6 @@ mod test {
#[test] #[test]
fn verify_ffi_enum_data_size() { fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>()); println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() <= 96); assert!(std::mem::size_of::<Data>() <= 120);
} }
} }

View File

@@ -859,9 +859,10 @@ on run {app_name, cur_pid, app_dir, user_name}
set app_dir_q to quoted form of app_dir set app_dir_q to quoted form of app_dir
set user_name_q to quoted form of user_name set user_name_q to quoted form of user_name
set check_source to "test -d " & app_dir_q & " || exit 1;"
set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;" set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;"
set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);" set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);"
set sh to "set -e;" & kill_others & copy_files set sh to "set -e;" & check_source & kill_others & copy_files
do shell script sh with prompt app_name & " wants to update itself" with administrator privileges do shell script sh with prompt app_name & " wants to update itself" with administrator privileges
end run end run

View File

@@ -4,6 +4,7 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir}
set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist" set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist"
set app_bundle to "/Applications/RustDesk.app" set app_bundle to "/Applications/RustDesk.app"
set check_source to "test -d " & quoted form of source_dir & " || exit 1;"
set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);" set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);"
set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;" set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;"
set unload_service to "launchctl unload -w " & daemon_plist & " || true;" set unload_service to "launchctl unload -w " & daemon_plist & " || true;"
@@ -19,7 +20,7 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir}
set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;" set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;"
set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent
set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent set sh to "set -e;" & check_source & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent
do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges
end run end run

View File

@@ -560,7 +560,9 @@ impl Connection {
match data { match data {
ipc::Data::Authorize => { ipc::Data::Authorize => {
conn.require_2fa.take(); conn.require_2fa.take();
conn.send_logon_response().await; if !conn.send_logon_response_and_keep_alive().await {
break;
}
if conn.port_forward_socket.is_some() { if conn.port_forward_socket.is_some() {
break; break;
} }
@@ -1338,9 +1340,66 @@ impl Connection {
crate::post_request(url, v.to_string(), "").await crate::post_request(url, v.to_string(), "").await
} }
async fn send_logon_response(&mut self) { fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
let mut is_rdp = false;
if pf.host == "RDP" && pf.port == 0 {
pf.host = "localhost".to_owned();
pf.port = 3389;
is_rdp = true;
}
if pf.host.is_empty() {
pf.host = "localhost".to_owned();
}
(format!("{}:{}", pf.host, pf.port), is_rdp)
}
async fn connect_port_forward_if_needed(&mut self) -> bool {
if self.port_forward_socket.is_some() {
return true;
}
let Some(login_request::Union::PortForward(pf)) = self.lr.union.as_ref() else {
return true;
};
let mut pf = pf.clone();
let (mut addr, is_rdp) = Self::normalize_port_forward_target(&mut pf);
self.port_forward_address = addr.clone();
match timeout(3000, TcpStream::connect(&addr)).await {
Ok(Ok(sock)) => {
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
true
}
Ok(Err(e)) => {
log::warn!("Port forward connect failed for {}: {}", addr, e);
if is_rdp {
addr = "RDP".to_owned();
}
self.send_login_error(format!(
"Failed to access remote {}. Please make sure it is reachable/open.",
addr
))
.await;
false
}
Err(e) => {
log::warn!("Port forward connect timed out for {}: {}", addr, e);
if is_rdp {
addr = "RDP".to_owned();
}
self.send_login_error(format!(
"Failed to access remote {}. Please make sure it is reachable/open.",
addr
))
.await;
false
}
}
}
// Returns whether this connection should be kept alive.
// `true` does not necessarily mean authorization succeeded (e.g. REQUIRE_2FA case).
async fn send_logon_response_and_keep_alive(&mut self) -> bool {
if self.authorized { if self.authorized {
return; return true;
} }
if self.require_2fa.is_some() && !self.is_recent_session(true) && !self.from_switch { if self.require_2fa.is_some() && !self.is_recent_session(true) && !self.from_switch {
self.require_2fa.as_ref().map(|totp| { self.require_2fa.as_ref().map(|totp| {
@@ -1371,7 +1430,11 @@ impl Connection {
} }
}); });
self.send_login_error(crate::client::REQUIRE_2FA).await; self.send_login_error(crate::client::REQUIRE_2FA).await;
return; // Keep the connection alive so the client can continue with 2FA.
return true;
}
if !self.connect_port_forward_if_needed().await {
return false;
} }
self.authorized = true; self.authorized = true;
let (conn_type, auth_conn_type) = if self.file_transfer.is_some() { let (conn_type, auth_conn_type) = if self.file_transfer.is_some() {
@@ -1494,7 +1557,7 @@ impl Connection {
res.set_peer_info(pi); res.set_peer_info(pi);
msg_out.set_login_response(res); msg_out.set_login_response(res);
self.send(msg_out).await; self.send(msg_out).await;
return; return true;
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if self.is_remote() { if self.is_remote() {
@@ -1517,7 +1580,7 @@ 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;
return; return true;
} }
} }
#[allow(unused_mut)] #[allow(unused_mut)]
@@ -1671,6 +1734,7 @@ impl Connection {
self.try_sub_monitor_services(); self.try_sub_monitor_services();
} }
} }
true
} }
fn try_sub_camera_displays(&mut self) { fn try_sub_camera_displays(&mut self) {
@@ -2179,33 +2243,8 @@ impl Connection {
sleep(1.).await; sleep(1.).await;
return false; return false;
} }
let mut is_rdp = false; let (addr, _is_rdp) = Self::normalize_port_forward_target(&mut pf);
if pf.host == "RDP" && pf.port == 0 { self.port_forward_address = addr;
pf.host = "localhost".to_owned();
pf.port = 3389;
is_rdp = true;
}
if pf.host.is_empty() {
pf.host = "localhost".to_owned();
}
let mut addr = format!("{}:{}", pf.host, pf.port);
self.port_forward_address = addr.clone();
match timeout(3000, TcpStream::connect(&addr)).await {
Ok(Ok(sock)) => {
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
}
_ => {
if is_rdp {
addr = "RDP".to_owned();
}
self.send_login_error(format!(
"Failed to access remote {}, please make sure if it is open",
addr
))
.await;
return false;
}
}
} }
_ => { _ => {
if !self.check_privacy_mode_on().await { if !self.check_privacy_mode_on().await {
@@ -2236,9 +2275,7 @@ impl Connection {
// `is_logon_ui()` is a fallback for logon UI detection on Windows. // `is_logon_ui()` is a fallback for logon UI detection on Windows.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let is_logon = || { let is_logon = || {
crate::platform::is_prelogin() crate::platform::is_prelogin() || crate::platform::is_locked() || {
|| crate::platform::is_locked()
|| {
match crate::platform::is_logon_ui() { match crate::platform::is_logon_ui() {
Ok(result) => result, Ok(result) => result,
Err(e) => { Err(e) => {
@@ -2277,7 +2314,9 @@ impl Connection {
if err_msg.is_empty() { if err_msg.is_empty() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await; self.linux_headless_handle.wait_desktop_cm_ready().await;
self.send_logon_response().await; if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized); self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized);
} else { } else {
self.send_login_error(err_msg).await; self.send_login_error(err_msg).await;
@@ -2313,7 +2352,9 @@ impl Connection {
if err_msg.is_empty() { if err_msg.is_empty() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await; self.linux_headless_handle.wait_desktop_cm_ready().await;
self.send_logon_response().await; if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(lr.my_id, lr.my_name, self.authorized); self.try_start_cm(lr.my_id, lr.my_name, self.authorized);
} else { } else {
self.send_login_error(err_msg).await; self.send_login_error(err_msg).await;
@@ -2331,7 +2372,9 @@ impl Connection {
self.update_failure(failure, true, 1); self.update_failure(failure, true, 1);
self.require_2fa.take(); self.require_2fa.take();
raii::AuthedConnID::set_session_2fa(self.session_key()); raii::AuthedConnID::set_session_2fa(self.session_key());
self.send_logon_response().await; if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm( self.try_start_cm(
self.lr.my_id.to_owned(), self.lr.my_id.to_owned(),
self.lr.my_name.to_owned(), self.lr.my_name.to_owned(),
@@ -2382,7 +2425,9 @@ impl Connection {
if let Some((_instant, uuid_old)) = uuid_old { if let Some((_instant, uuid_old)) = uuid_old {
if uuid == uuid_old { if uuid == uuid_old {
self.from_switch = true; self.from_switch = true;
self.send_logon_response().await; if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm( self.try_start_cm(
lr.my_id.clone(), lr.my_id.clone(),
lr.my_name.clone(), lr.my_name.clone(),
@@ -5348,9 +5393,8 @@ mod raii {
} }
pub fn check_wake_lock_on_setting_changed() { pub fn check_wake_lock_on_setting_changed() {
let current = config::Config::get_bool_option( let current =
keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, config::Config::get_bool_option(keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS);
);
let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap(); let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap();
if cached != Some(current) { if cached != Some(current) {
Self::check_wake_lock(); Self::check_wake_lock();

View File

@@ -809,7 +809,7 @@ fn record_key_is_control_key(record_key: u64) -> bool {
#[inline] #[inline]
fn record_key_is_chr(record_key: u64) -> bool { fn record_key_is_chr(record_key: u64) -> bool {
record_key < KEY_CHAR_START record_key >= KEY_CHAR_START
} }
#[inline] #[inline]
@@ -1513,6 +1513,27 @@ fn get_control_key_value(key_event: &KeyEvent) -> i32 {
} }
} }
#[inline]
fn has_hotkey_modifiers(key_event: &KeyEvent) -> bool {
key_event.modifiers.iter().any(|ck| {
let v = ck.value();
v == ControlKey::Control.value()
|| v == ControlKey::RControl.value()
|| v == ControlKey::Meta.value()
|| v == ControlKey::RWin.value()
|| {
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
v == ControlKey::Alt.value() || v == ControlKey::RAlt.value()
}
#[cfg(target_os = "macos")]
{
false
}
}
})
}
fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) {
let ck_value = get_control_key_value(key_event); let ck_value = get_control_key_value(key_event);
fix_modifiers(&key_event.modifiers[..], en, ck_value); fix_modifiers(&key_event.modifiers[..], en, ck_value);
@@ -1572,7 +1593,7 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en)
} }
fn process_chr(en: &mut Enigo, chr: u32, down: bool) { fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
// On Wayland with uinput mode, use clipboard for character input // On Wayland with uinput mode, use clipboard for character input
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() { if !crate::platform::linux::is_x11() && wayland_use_uinput() {
@@ -1587,6 +1608,16 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
} }
} }
#[cfg(any(target_os = "macos", target_os = "windows"))]
if !_hotkey {
if down {
if let Ok(chr) = char::try_from(chr) {
en.key_sequence(&chr.to_string());
}
}
return;
}
let key = char_value_to_key(chr); let key = char_value_to_key(chr);
if down { if down {
@@ -1856,7 +1887,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) {
let record_key = chr as u64 + KEY_CHAR_START; let record_key = chr as u64 + KEY_CHAR_START;
record_pressed_key(KeysDown::EnigoKey(record_key), down); record_pressed_key(KeysDown::EnigoKey(record_key), down);
process_chr(&mut en, chr, down) process_chr(&mut en, chr, down, has_hotkey_modifiers(evt))
} }
Some(key_event::Union::Unicode(chr)) => { Some(key_event::Union::Unicode(chr)) => {
// Same as Chr: release Shift for Unicode input // Same as Chr: release Shift for Unicode input

View File

@@ -245,39 +245,20 @@ pub fn get_builtin_option(key: &str) -> String {
#[inline] #[inline]
pub fn set_local_option(key: String, value: String) { pub fn set_local_option(key: String, value: String) {
let value = normalize_local_option_value(&key, value);
LocalConfig::set_option(key.clone(), value); LocalConfig::set_option(key.clone(), value);
} }
fn normalize_local_option_value(key: &str, value: String) -> String { /// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
if key != "user_info" || value.is_empty() { /// by prepending the API server address.
return value; pub fn resolve_avatar_url(avatar: String) -> String {
} let avatar = avatar.trim().to_owned();
let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&value) else { if avatar.starts_with('/') {
return value;
};
let Some(obj) = v.as_object_mut() else {
return value;
};
let Some(avatar) = obj
.get("avatar")
.and_then(|x| x.as_str())
.map(|x| x.trim().to_owned())
else {
return value;
};
if !avatar.starts_with('/') {
return value;
}
let api_server = get_api_server(); let api_server = get_api_server();
if api_server.is_empty() { if !api_server.is_empty() {
return value; return format!("{}{}", api_server.trim_end_matches('/'), avatar);
} }
obj.insert( }
"avatar".to_owned(), avatar
serde_json::Value::String(format!("{}{}", api_server.trim_end_matches('/'), avatar)),
);
serde_json::to_string(&v).unwrap_or(value)
} }
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]