diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9363d8c3f..f17efb828 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -295,25 +295,26 @@ class _RemotePageState extends State { : Offstage(), ], )), - body: getRawPointerAndKeyBody(Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - return Container( + body: Obx( + () => getRawPointerAndKeyBody(Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( color: Colors.black, child: isWebDesktop ? getBodyForDesktopWithListener(keyboard) - : SafeArea(child: - OrientationBuilder(builder: (ctx, orientation) { - if (_currentOrientation != orientation) { - Timer(const Duration(milliseconds: 200), () { - gFFI.dialogManager - .resetMobileActionsOverlay(ffi: gFFI); - _currentOrientation = orientation; - gFFI.canvasModel.updateViewStyle(); - }); - } - return Obx( - () => Container( + : SafeArea( + child: + OrientationBuilder(builder: (ctx, orientation) { + if (_currentOrientation != orientation) { + Timer(const Duration(milliseconds: 200), () { + gFFI.dialogManager + .resetMobileActionsOverlay(ffi: gFFI); + _currentOrientation = orientation; + gFFI.canvasModel.updateViewStyle(); + }); + } + return Container( color: MyTheme.canvasColor, child: inputModel.isPhysicalMouse.value ? getBodyForMobile() @@ -321,12 +322,14 @@ class _RemotePageState extends State { child: getBodyForMobile(), ffi: gFFI, ), - ), - ); - }))); - }) - ], - ))), + ); + }), + ), + ); + }) + ], + )), + )), ); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 2440ee25b..dd745bbaf 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -134,7 +134,7 @@ class RustdeskImpl { Future sessionSend2Fa( {required UuidValue sessionId, required String code, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['send_2fa', code])); } Future sessionClose({required UuidValue sessionId, dynamic hint}) { @@ -143,7 +143,7 @@ class RustdeskImpl { Future sessionRefresh( {required UuidValue sessionId, required int display, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['refresh'])); } Future sessionRecordScreen( @@ -168,7 +168,8 @@ class RustdeskImpl { Future sessionToggleOption( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['toggle_option', value])); } Future sessionTogglePrivacyMode( @@ -176,7 +177,10 @@ class RustdeskImpl { required String implKey, required bool on, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'toggle_option', + jsonEncode({implKey, on}) + ])); } Future sessionGetFlutterOption( @@ -360,11 +364,11 @@ class RustdeskImpl { } Future sessionLockScreen({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['lock_screen'])); } Future sessionCtrlAltDel({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['ctrl_alt_del'])); } Future sessionSwitchDisplay( @@ -372,7 +376,14 @@ class RustdeskImpl { required UuidValue sessionId, required Int32List value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'switch_display', + jsonEncode({ + isDesktop: isDesktop, + sessionId: sessionId.toString(), + value: value + }) + ])); } Future sessionHandleFlutterKeyEvent( @@ -383,6 +394,7 @@ class RustdeskImpl { required int lockModes, required bool downOrUp, dynamic hint}) { + // TODO: map mode throw UnimplementedError(); } @@ -401,12 +413,24 @@ class RustdeskImpl { required bool shift, required bool command, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'input_key', + jsonEncode({ + 'name': name, + if (down) 'down': 'true', + if (press) 'press': 'true', + if (alt) 'alt': 'true', + if (ctrl) 'ctrl': 'true', + if (shift) 'shift': 'true', + if (command) 'command': 'true' + }) + ])); } Future sessionInputString( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['input_string', value])); } Future sessionSendChat( @@ -559,7 +583,10 @@ class RustdeskImpl { required String username, required String password, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'elevate_with_logon', + jsonEncode({username, password}) + ])); } Future sessionSwitchSides( @@ -573,6 +600,7 @@ class RustdeskImpl { required int width, required int height, dynamic hint}) { + // note: restore on disconnected throw UnimplementedError(); } @@ -683,7 +711,7 @@ class RustdeskImpl { } Future mainGetVersion({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['version'])); } Future> mainGetFav({dynamic hint}) { @@ -691,10 +719,12 @@ class RustdeskImpl { } Future mainStoreFav({required List favs, dynamic hint}) { + // TODO: throw UnimplementedError(); } String mainGetPeerSync({required String id, dynamic hint}) { + // TODO: throw UnimplementedError(); } @@ -713,7 +743,8 @@ class RustdeskImpl { } Future mainIsUsingPublicServer({dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ["is_using_public_server"])); } Future mainDiscover({dynamic hint}) { @@ -749,12 +780,16 @@ class RustdeskImpl { } String mainGetInputSource({dynamic hint}) { - throw UnimplementedError(); + // // rdev grab mode + // const CONFIG_INPUT_SOURCE_1 = "Input source 1"; + // // flutter grab mode + // const CONFIG_INPUT_SOURCE_2 = "Input source 2"; + return 'Input source 2'; } Future mainSetInputSource( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future.value(); } Future mainGetMyId({dynamic hint}) { @@ -767,17 +802,17 @@ class RustdeskImpl { Future mainGetPeerOption( {required String id, required String key, dynamic hint}) { - throw UnimplementedError(); + return Future(() => mainGetPeerOptionSync(id: id, key: key, hint: hint)); } String mainGetPeerOptionSync( {required String id, required String key, dynamic hint}) { - throw UnimplementedError(); + return js.context.callMethod('getByName', ['option:peer', key]); } String mainGetPeerFlutterOptionSync( {required String id, required String k, dynamic hint}) { - throw UnimplementedError(); + return js.context.callMethod('getByName', ['option:flutter:peer', k]); } void mainSetPeerFlutterOptionSync( @@ -785,7 +820,10 @@ class RustdeskImpl { required String k, required String v, dynamic hint}) { - throw UnimplementedError(); + js.context.callMethod('setByName', [ + 'option:flutter:peer', + jsonEncode({'name': k, 'value': v}) + ]); } Future mainSetPeerOption( @@ -793,7 +831,8 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + mainSetPeerOptionSync(id: id, key: key, value: value, hint: hint); + return Future.value(); } bool mainSetPeerOptionSync( @@ -801,12 +840,17 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': key, 'value': value}) + ]); + return true; } Future mainSetPeerAlias( {required String id, required String alias, dynamic hint}) { - throw UnimplementedError(); + mainSetPeerOptionSync(id: id, key: 'alias', value: alias, hint: hint); + return Future.value(); } Future mainGetNewStoredPeers({dynamic hint}) { @@ -814,15 +858,17 @@ class RustdeskImpl { } Future mainForgetPassword({required String id, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['forget'])); } Future mainPeerHasPassword({required String id, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('getByName', ['peer_has_password', id])); } Future mainPeerExists({required String id, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('getByName', ['peer_exists', id])); } Future mainLoadRecentPeers({dynamic hint}) { @@ -870,6 +916,7 @@ class RustdeskImpl { Future mainSetUserDefaultOption( {required String key, required String value, dynamic hint}) { + // TODO: do we need the default option? throw UnimplementedError(); } @@ -976,15 +1023,17 @@ class RustdeskImpl { } Future mainDeviceId({required String id, dynamic hint}) { + // TODO: ? throw UnimplementedError(); } Future mainDeviceName({required String name, dynamic hint}) { + // TODO: ? throw UnimplementedError(); } Future mainRemovePeer({required String id, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['remove', id])); } bool mainHasHwcodec({dynamic hint}) { @@ -1048,7 +1097,7 @@ class RustdeskImpl { Future sessionRestartRemoteDevice( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['restart'])); } String sessionGetAuditServerSync( @@ -1081,6 +1130,7 @@ class RustdeskImpl { required int index, required bool on, dynamic hint}) { + // TODO throw UnimplementedError(); } @@ -1126,6 +1176,7 @@ class RustdeskImpl { } Future mainCreateShortcut({required String id, dynamic hint}) { + // TODO: throw UnimplementedError(); } @@ -1181,6 +1232,7 @@ class RustdeskImpl { } Future mainGetBuildDate({dynamic hint}) { + // TODO throw UnimplementedError(); } @@ -1213,6 +1265,7 @@ class RustdeskImpl { dynamic hint}) {} Future queryOnlines({required List ids, dynamic hint}) { + // TODO: throw UnimplementedError(); } @@ -1336,11 +1389,11 @@ class RustdeskImpl { } bool mainCurrentIsWayland({dynamic hint}) { - throw UnimplementedError(); + return false; } bool mainIsLoginWayland({dynamic hint}) { - throw UnimplementedError(); + return false; } Future mainStartPa({dynamic hint}) { @@ -1494,7 +1547,7 @@ class RustdeskImpl { } bool isSelinuxEnforcing({dynamic hint}) { - throw UnimplementedError(); + return false; } String mainDefaultPrivacyModeImpl({dynamic hint}) { @@ -1506,7 +1559,7 @@ class RustdeskImpl { } String mainSupportedInputSource({dynamic hint}) { - throw UnimplementedError(); + return jsonEncode(['Input source 2', 'input_source_2_tip']); } Future mainGenerate2Fa({dynamic hint}) { @@ -1525,7 +1578,5 @@ class RustdeskImpl { throw UnimplementedError(); } - void dispose() { - throw UnimplementedError(); - } + void dispose() {} } diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index 7ae40d627..c41e32d00 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -4,6 +4,7 @@ import * as rendezvous from "./rendezvous.js"; import { loadVp9 } from "./codec"; import * as sha256 from "fast-sha256"; import * as globals from "./globals"; +import * as consts from "./consts"; import { decompress, mapKey, sleep } from "./common"; export const PORT = 21116; @@ -247,21 +248,7 @@ export default class Connection { this._ws?.sendMessage({ test_delay }); } } else if (msg?.login_response) { - const r = msg?.login_response; - if (r.error) { - if (r.error == "Wrong Password") { - this._password = undefined; - this.msgbox( - "re-input-password", - r.error, - "Do you want to enter again?" - ); - } else { - this.msgbox("error", "Login Error", r.error); - } - } else if (r.peer_info) { - this.handlePeerInfo(r.peer_info); - } + this.handleLoginResponse(msg?.login_response); } else if (msg?.video_frame) { this.handleVideoFrame(msg?.video_frame!); } else if (msg?.clipboard) { @@ -318,6 +305,103 @@ export default class Connection { } } + handleLoginResponse(response: message.LoginResponse) { + const loginErrorMap: Record = { + [consts.LOGIN_SCREEN_WAYLAND]: { + msgtype: "error", + title: "Login Error", + text: "Login screen using Wayland is not supported", + link: "https://rustdesk.com/docs/en/manual/linux/#login-screen", + try_again: true, + }, + [consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY]: { + msgtype: "session-login", + title: "", + text: "", + link: "", + try_again: true, + }, + [consts.LOGIN_MSG_DESKTOP_XSESSION_FAILED]: { + msgtype: "session-re-login", + title: "", + text: "", + link: "", + try_again: true, + }, + [consts.LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER]: { + msgtype: "info-nocancel", + title: "another_user_login_title_tip", + text: "another_user_login_text_tip", + link: "", + try_again: false, + }, + [consts.LOGIN_MSG_DESKTOP_XORG_NOT_FOUND]: { + msgtype: "info-nocancel", + title: "xorg_not_found_title_tip", + text: "xorg_not_found_text_tip", + link: "https://rustdesk.com/docs/en/manual/linux/#login-screen", + try_again: true, + }, + [consts.LOGIN_MSG_DESKTOP_NO_DESKTOP]: { + msgtype: "info-nocancel", + title: "no_desktop_title_tip", + text: "no_desktop_text_tip", + link: "https://rustdesk.com/docs/en/manual/linux/#login-screen", + try_again: true, + }, + [consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY]: { + msgtype: "session-login-password", + title: "", + text: "", + link: "", + try_again: true, + }, + [consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG]: { + msgtype: "session-login-re-password", + title: "", + text: "", + link: "", + try_again: true, + }, + [consts.LOGIN_MSG_NO_PASSWORD_ACCESS]: { + msgtype: "wait-remote-accept-nook", + title: "Prompt", + text: "Please wait for the remote side to accept your session request...", + link: "", + try_again: true, + }, + }; + + const err = response.error; + if (err) { + if (err == consts.LOGIN_MSG_PASSWORD_EMPTY) { + this._password = undefined; + this.msgbox("input-password", "Password Required", "", ""); + } + if (err == consts.LOGIN_MSG_PASSWORD_WRONG) { + this._password = undefined; + this.msgbox( + "re-input-password", + err, + "Do you want to enter again?" + ); + } else if (err == consts.LOGIN_MSG_2FA_WRONG || err == consts.REQUIRE_2FA) { + this.msgbox("input-2fa", err, ""); + } else if (err in loginErrorMap) { + const m = loginErrorMap[err]; + this.msgbox(m.msgtype, m.title, m.text, m.link); + } else { + if (err.includes(consts.SCRAP_X11_REQUIRED)) { + this.msgbox("error", "Login Error", err, consts.SCRAP_X11_REF_URL); + } else { + this.msgbox("error", "Login Error", err); + } + } + } else if (response.peer_info) { + this.handlePeerInfo(response.peer_info); + } + } + msgbox(type_: string, title: string, text: string, link: string = '') { this._msgbox?.(type_, title, text, link); } @@ -620,17 +704,70 @@ export default class Connection { this._ws?.sendMessage({ key_event }); } + restart() { + const misc = message.Misc.fromPartial({}); + misc.restart_remote_device = true; + this._ws?.sendMessage({ misc }); + } + inputString(seq: string) { const key_event = message.KeyEvent.fromPartial({ seq }); this._ws?.sendMessage({ key_event }); } - switchDisplay(display: number) { - const switch_display = message.SwitchDisplay.fromPartial({ display }); - const misc = message.Misc.fromPartial({ switch_display }); + send2fa(code: string) { + const auth_2fa = message.Auth2FA.fromPartial({ code }); + this._ws?.sendMessage({ auth_2fa }); + } + + _captureDisplays({ add, sub, set }: { + add?: number[], sub?: number[], set?: number[] + }) { + const capture_displays = message.CaptureDisplays.fromPartial({ add, sub, set }); + const misc = message.Misc.fromPartial({ capture_displays }); this._ws?.sendMessage({ misc }); } + switchDisplay(v: string) { + try { + const obj = JSON.parse(v); + const value = obj.value; + const isDesktop = obj.isDesktop; + if (value.length == 1) { + const switch_display = message.SwitchDisplay.fromPartial({ display: value[0] }); + const misc = message.Misc.fromPartial({ switch_display }); + this._ws?.sendMessage({ misc }); + + if (!isDesktop) { + this._captureDisplays({ set: value }); + } else { + // If support merging images, check_remove_unused_displays() in ui_session_interface.rs + } + } else { + this._captureDisplays({ set: value }); + } + } + catch (e) { + console.log('Failed to switch display, invalid param "' + v + '"'); + } + } + + elevateWithLogon(value: string) { + try { + const obj = JSON.parse(value); + const logon = message.ElevationRequestWithLogon.fromPartial({ + username: obj.username, + password: obj.password + }); + const elevation_request = message.ElevationRequest.fromPartial({ logon }); + const misc = message.Misc.fromPartial({ elevation_request }); + this._ws?.sendMessage({ misc }); + } + catch (e) { + console.log('Failed to elevate with logon, invalid param "' + value + '"'); + } + } + async inputOsPassword(seq: string) { this.inputMouse(); await sleep(50); @@ -714,6 +851,20 @@ export default class Connection { this._ws?.sendMessage({ misc }); } + togglePrivacyMode(value: string) { + try { + const obj = JSON.parse(value); + const toggle_privacy_mode = message.TogglePrivacyMode.fromPartial({ + impl_key: obj.impl_key, + on: obj.on, + }); + const misc = message.Misc.fromPartial({ toggle_privacy_mode }); + this._ws?.sendMessage({ misc }); + } catch (e) { + console.log('Failed to toggle privacy mode, invalid param "' + value + '"') + } + } + getImageQuality() { return this.getOption("image-quality"); } diff --git a/flutter/web/js/src/consts.ts b/flutter/web/js/src/consts.ts new file mode 100644 index 000000000..fe7d534e5 --- /dev/null +++ b/flutter/web/js/src/consts.ts @@ -0,0 +1,19 @@ +export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY = 'Desktop session not ready'; +export const LOGIN_MSG_DESKTOP_XSESSION_FAILED = 'Desktop xsession failed'; +export const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER = 'Desktop session another user login'; +export const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND = 'Desktop xorg not found'; +// ls /usr/share/xsessions/ +export const LOGIN_MSG_DESKTOP_NO_DESKTOP = 'Desktop none'; +export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY = + 'Desktop session not ready, password empty'; +export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG = + 'Desktop session not ready, password wrong'; +export const LOGIN_MSG_PASSWORD_EMPTY = 'Empty Password'; +export const LOGIN_MSG_PASSWORD_WRONG = 'Wrong Password'; +export const LOGIN_MSG_2FA_WRONG = 'Wrong 2FA Code'; +export const REQUIRE_2FA = '2FA Required'; +export const LOGIN_MSG_NO_PASSWORD_ACCESS = 'No Password Access'; +export const LOGIN_MSG_OFFLINE = 'Offline'; +export const LOGIN_SCREEN_WAYLAND = 'Wayland login screen is not supported'; +export const SCRAP_X11_REQUIRED = 'x11 expected'; +export const SCRAP_X11_REF_URL = 'https://rustdesk.com/docs/en/manual/linux/#x11-required'; diff --git a/flutter/web/js/src/globals.js b/flutter/web/js/src/globals.js index d6f4080aa..5830a7beb 100644 --- a/flutter/web/js/src/globals.js +++ b/flutter/web/js/src/globals.js @@ -63,7 +63,7 @@ let testSpeed = [0, 0]; export function draw(display, frame) { if (yuvWorker) { // frame's (y/u/v).bytes already detached, can not transferrable any more. - yuvWorker.postMessage({display, frame}); + yuvWorker.postMessage({ display, frame }); } else { var tm0 = new Date().getTime(); yuvCanvas.drawFrame(frame); @@ -216,6 +216,9 @@ window.setByName = (name, value) => { case 'toggle_option': curConn.toggleOption(value); break; + case 'toggle_privacy_mode': + curConn.togglePrivacyMode(value); + break; case 'image_quality': curConn.setImageQuality(value); break; @@ -266,6 +269,9 @@ window.setByName = (name, value) => { } curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); break; + case 'send_2fa': + curConn.send2fa(value); + break; case 'option': value = JSON.parse(value); localStorage.setItem(value.name, value.value); @@ -306,6 +312,20 @@ window.setByName = (name, value) => { case 'session_close': sessionClose(value); break; + case 'elevate_with_logon': + curConn.elevateWithLogon(value); + break; + case 'forget': + curConn.setRemember(false); + break; + case 'peer_has_password': + const options = getPeers()[value] || {}; + return (options['password'] ?? '') !== ''; + case 'peer_exists': + return !(!getPeers()[value]); + case 'restart': + curConn.restart(); + break; default: break; } @@ -402,6 +422,8 @@ function _getByName(name, arg) { return localStorage.getItem('peers-lan') ?? '{}'; case 'api_server': return getApiServer(); + case 'is_using_public_server': + return !localStorage.getItem('custom-rendezvous-server'); } return ''; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2a886376d..b60be6bff 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -874,6 +874,7 @@ pub fn main_get_api_server() -> String { get_api_server() } +// This function doesn't seem to be used. pub fn main_post_request(url: String, body: String, header: String) { post_request(url, body, header) }