From 1cfc9b61391ce7e7091288ebef12614df56e69ae Mon Sep 17 00:00:00 2001 From: redistay <165581775+redistay@users.noreply.github.com> Date: Tue, 2 Apr 2024 21:41:54 +0800 Subject: [PATCH 1/3] chore: fix some typos (#7585) Signed-off-by: redistay --- flutter/lib/desktop/widgets/tabbar_widget.dart | 2 +- flutter/lib/models/ab_model.dart | 2 +- libs/virtual_display/dylib/src/win10/IddController.h | 2 +- src/common.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 8d84de744f..76777b19f2 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -583,7 +583,7 @@ class WindowActionPanelState extends State mainWindowClose() async => await windowManager.hide(); notMainWindowClose(WindowController controller) async { if (widget.tabController.length != 0) { - debugPrint("close not emtpy multiwindow from taskbar"); + debugPrint("close not empty multiwindow from taskbar"); if (isWindows) { await controller.show(); await controller.focus(); diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 9840ded5d2..ba13c51435 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -854,7 +854,7 @@ class LegacyAb extends BaseAb { final resp = await http.get(Uri.parse(api), headers: authHeaders); statusCode = resp.statusCode; if (resp.body.toLowerCase() == "null") { - // normal reply, emtpy ab return null + // normal reply, empty ab return null tags.clear(); tagColors.clear(); peers.clear(); diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index 6e87a78df1..99c5dad49e 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -61,7 +61,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice); /** * @brief Create device and set the lifetime. * Only one device should be created. - * If device is installed ealier, this function returns FALSE. + * If device is installed earlier, this function returns FALSE. * * @param lifetime [in] The lifetime to set after creating the device. NULL means do not set the lifetime. * https://learn.microsoft.com/en-us/windows/win32/api/swdevice/nf-swdevice-swdevicesetlifetime diff --git a/src/common.rs b/src/common.rs index 86af0f8995..e57aefe5c4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1682,7 +1682,7 @@ mod tests { } } } - // No mutliple ticks in the `interval` time. + // No multiple ticks in the `interval` time. // Values in "times" are unique and are less than normal tokio interval. // See previous test (test_tokio_time_interval_sleep) for comparison. let times2: HashSet = HashSet::from_iter(times.clone()); From 74af7ef8b23c5b4697f74c953d3ea11a1d6e46ad Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 2 Apr 2024 21:59:17 +0800 Subject: [PATCH 2/3] Fix. Custom client, connection status (#7586) Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 136 ++++++++++-------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 408e0ee929..fe8e6c6427 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -64,76 +64,90 @@ class _OnlineStatusWidgetState extends State { @override Widget build(BuildContext context) { final isIncomingOnly = bind.isIncomingOnly(); - return Container( - height: height, - child: Obx(() => Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 8, - width: 8, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: _svcStopped.value || - stateGlobal.svcStatus.value == SvcStatus.connecting - ? kColorWarn - : (stateGlobal.svcStatus.value == SvcStatus.ready - ? Color.fromARGB(255, 50, 190, 166) - : Color.fromARGB(255, 224, 79, 95)), - ), - ).marginSymmetric(horizontal: em), - Container( - width: isIncomingOnly ? 226 : null, - child: _buildConnStatusMsg(), - ), - // stop - Offstage( - offstage: !_svcStopped.value, - child: InkWell( - onTap: () async { - await start_service(true); - }, - child: Text(translate("Start service"), - style: TextStyle( - decoration: TextDecoration.underline, - fontSize: em))) - .marginOnly(left: em), - ), - // ready && public - // No need to show the guide if is custom client. - if (!isIncomingOnly) + startServiceWidget() => Offstage( + offstage: !_svcStopped.value, + child: InkWell( + onTap: () async { + await start_service(true); + }, + child: Text(translate("Start service"), + style: TextStyle( + decoration: TextDecoration.underline, fontSize: em))) + .marginOnly(left: em), + ); + + setupServerWidget() => Flexible( + child: Offstage( + offstage: !(!_svcStopped.value && + stateGlobal.svcStatus.value == SvcStatus.ready && + _svcIsUsingPublicServer.value), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: TextStyle(fontSize: em)), Flexible( - child: Offstage( - offstage: !(!_svcStopped.value && - stateGlobal.svcStatus.value == SvcStatus.ready && - _svcIsUsingPublicServer.value), + child: InkWell( + onTap: onUsePublicServerGuide, child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(', ', style: TextStyle(fontSize: em)), Flexible( - child: InkWell( - onTap: onUsePublicServerGuide, - child: Row( - children: [ - Flexible( - child: Text( - translate('setup_server_tip'), - style: TextStyle( - decoration: TextDecoration.underline, - fontSize: em), - ), - ), - ], - ), + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em), ), - ) + ), ], ), ), ) - ], - )), + ], + ), + ), + ); + + basicWidget() => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: _svcStopped.value || + stateGlobal.svcStatus.value == SvcStatus.connecting + ? kColorWarn + : (stateGlobal.svcStatus.value == SvcStatus.ready + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), + ), + ).marginSymmetric(horizontal: em), + Container( + width: isIncomingOnly ? 226 : null, + child: _buildConnStatusMsg(), + ), + // stop + if (!isIncomingOnly) startServiceWidget(), + // ready && public + // No need to show the guide if is custom client. + if (!isIncomingOnly) setupServerWidget(), + ], + ); + + return Container( + height: height, + child: Obx(() => isIncomingOnly + ? Column( + children: [ + basicWidget(), + Align( + child: startServiceWidget(), + alignment: Alignment.centerLeft) + .marginOnly(top: 2.0, left: 22.0), + ], + ) + : basicWidget()), ).paddingOnly(right: isIncomingOnly ? 8 : 0); } From d7b47b49d23e41219465c5dc20f69fd2eee1ef3d Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 2 Apr 2024 22:08:47 +0800 Subject: [PATCH 3/3] opt password sync, opt ab widgets (#7582) * Opt sync conctrl with password source, add some comments * For sync from recent, legacy ab remove forceRelay, rdpPort, rdpUsername, because it's not used, personal ab add sync hash * Opt style of add Id dialog Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 153 ++++++++++------- flutter/lib/common/widgets/dialog.dart | 77 ++++++--- flutter/lib/common/widgets/peer_card.dart | 48 +++--- flutter/lib/models/ab_model.dart | 112 ++++++------ flutter/lib/models/model.dart | 8 +- flutter/lib/models/peer_model.dart | 28 +-- src/client.rs | 170 ++++++++++++++----- src/flutter.rs | 16 +- 8 files changed, 369 insertions(+), 243 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 8a8992a1ed..702b4eb79c 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -87,7 +87,10 @@ class _AddressBookState extends State { child: Column( children: [ _buildAbDropdown(), - _buildTagHeader().marginOnly(left: 8.0, right: 0), + _buildTagHeader().marginOnly( + left: 8.0, + right: gFFI.abModel.legacyMode.value ? 8.0 : 0, + top: gFFI.abModel.legacyMode.value ? 8.0 : 0), Expanded( child: Container( width: double.infinity, @@ -415,6 +418,7 @@ class _AddressBookState extends State { return; } var isInProgress = false; + var passwordVisible = false; IDTextEditingController idController = IDTextEditingController(text: ''); TextEditingController aliasController = TextEditingController(text: ''); TextEditingController passwordController = TextEditingController(text: ''); @@ -460,6 +464,24 @@ class _AddressBookState extends State { } double marginBottom = 4; + + row({required Widget lable, required Widget input}) { + return Row( + children: [ + !isMobile + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: lable.marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 200), + child: input), + ), + ], + ).marginOnly(bottom: !isMobile ? 8 : 0); + } + return CustomAlertDialog( title: Text(translate("Add ID")), content: Column( @@ -467,75 +489,90 @@ class _AddressBookState extends State { children: [ Column( children: [ - Align( - alignment: Alignment.centerLeft, - child: Row( - children: [ - Text( - '*', - style: TextStyle(color: Colors.red, fontSize: 14), - ), - Text( - 'ID', - style: style, - ), - ], - ), - ).marginOnly(bottom: marginBottom), - TextField( - controller: idController, - inputFormatters: [IDTextInputFormatter()], - decoration: - InputDecoration(errorText: errorMsg, errorMaxLines: 5), - ), - Align( - alignment: Alignment.centerLeft, - child: Text( + row( + lable: Row( + children: [ + Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 14), + ), + Text( + 'ID', + style: style, + ), + ], + ), + input: TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + labelText: !isMobile ? null : translate('ID'), + errorText: errorMsg, + errorMaxLines: 5), + )), + row( + lable: Text( translate('Alias'), style: style, ), - ).marginOnly(top: 8, bottom: marginBottom), - TextField( - controller: aliasController, + input: TextField( + controller: aliasController, + decoration: InputDecoration( + labelText: !isMobile ? null : translate('Alias'), + )), ), if (isCurrentAbShared) + row( + lable: Text( + translate('Password'), + style: style, + ), + input: TextField( + controller: passwordController, + obscureText: !passwordVisible, + decoration: InputDecoration( + labelText: !isMobile ? null : translate('Password'), + suffixIcon: IconButton( + icon: Icon( + passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + }, + ), + ), + )), + if (gFFI.abModel.currentAbTags.isNotEmpty) Align( alignment: Alignment.centerLeft, child: Text( - translate('Password'), + translate('Tags'), style: style, ), ).marginOnly(top: 8, bottom: marginBottom), - if (isCurrentAbShared) - TextField( - controller: passwordController, - obscureText: true, - ), - Align( - alignment: Alignment.centerLeft, - child: Text( - translate('Tags'), - style: style, - ), - ).marginOnly(top: 8, bottom: marginBottom), - Align( - alignment: Alignment.centerLeft, - child: Wrap( - children: tags - .map((e) => AddressBookTag( - name: e, - tags: selectedTag, - onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - }, - showActionMenu: false)) - .toList(growable: false), + if (gFFI.abModel.currentAbTags.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), ), - ), ], ), const SizedBox( diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index be1df65397..b98b2b2488 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1918,11 +1918,9 @@ void addPeersToAbDialog( Future addTo(String abname) async { final mapList = peers.map((e) { var json = e.toJson(); - // remove shared password when add to other address book + // remove password when add to another address book to avoid re-share json.remove('password'); - if (gFFI.abModel.addressbooks[abname]?.isPersonal() != true) { - json.remove('hash'); - } + json.remove('hash'); return json; }).toList(); final errMsg = await gFFI.abModel.addPeersTo(mapList, abname); @@ -1986,6 +1984,7 @@ void addPeersToAbDialog( content: Obx(() => Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ + // https://github.com/flutter/flutter/issues/145081 DropdownMenu( initialSelection: currentName.value, onSelected: (value) { @@ -2026,18 +2025,23 @@ void addPeersToAbDialog( } void setSharedAbPasswordDialog(String abName, Peer peer) { - TextEditingController controller = TextEditingController(text: peer.password); + TextEditingController controller = TextEditingController(text: ''); RxBool isInProgress = false.obs; + RxBool isInputEmpty = true.obs; + bool passwordVisible = false; + controller.addListener(() { + isInputEmpty.value = controller.text.isEmpty; + }); gFFI.dialogManager.show((setState, close, context) { - submit() async { + change(String password) async { isInProgress.value = true; - bool res = await gFFI.abModel - .changeSharedPassword(abName, peer.id, controller.text); - close(); + bool res = + await gFFI.abModel.changeSharedPassword(abName, peer.id, password); isInProgress.value = false; if (res) { showToast(translate('Successful')); } + close(); } cancel() { @@ -2049,22 +2053,38 @@ void setSharedAbPasswordDialog(String abName, Peer peer) { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.key, color: MyTheme.accent), - Text(translate('Set shared password')).paddingOnly(left: 10), + Text(translate(peer.password.isEmpty + ? 'Set shared password' + : 'Change Password')) + .paddingOnly(left: 10), ], ), content: Obx(() => Column(children: [ TextField( controller: controller, - obscureText: true, autofocus: true, + obscureText: !passwordVisible, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + }, + ), + ), ), - Row(children: [ - Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), - Text( - translate('share_warning_tip'), - style: TextStyle(fontSize: 12), - ) - ]).marginSymmetric(vertical: 10), + if (!gFFI.abModel.current.isPersonal()) + Row(children: [ + Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), + Text( + translate('share_warning_tip'), + style: TextStyle(fontSize: 12), + ) + ]).marginSymmetric(vertical: 10), // NOT use Offstage to wrap LinearProgressIndicator isInProgress.value ? const LinearProgressIndicator() : Offstage() ])), @@ -2075,13 +2095,22 @@ void setSharedAbPasswordDialog(String abName, Peer peer) { onPressed: cancel, isOutline: true, ), - dialogButton( - "OK", - icon: Icon(Icons.done_rounded), - onPressed: submit, - ), + if (peer.password.isNotEmpty) + dialogButton( + "Remove", + icon: Icon(Icons.delete_outline_rounded), + onPressed: () => change(''), + buttonStyle: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red)), + ), + Obx(() => dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: + isInputEmpty.value ? null : () => change(controller.text), + )), ], - onSubmit: submit, + onSubmit: isInputEmpty.value ? null : () => change(controller.text), onCancel: cancel, ); }); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 3efebf4c79..d1aede0b6f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -139,21 +139,30 @@ class _PeerCardState extends State<_PeerCard> mainAxisSize: MainAxisSize.max, children: [ Container( - decoration: BoxDecoration( - color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: isMobile - ? BorderRadius.circular(_tileRadius) - : BorderRadius.only( - topLeft: Radius.circular(_tileRadius), - bottomLeft: Radius.circular(_tileRadius), + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: isMobile + ? BorderRadius.circular(_tileRadius) + : BorderRadius.only( + topLeft: Radius.circular(_tileRadius), + bottomLeft: Radius.circular(_tileRadius), + ), + ), + alignment: Alignment.center, + width: isMobile ? 50 : 42, + height: isMobile ? 50 : null, + child: Stack( + children: [ + getPlatformImage(peer.platform, size: isMobile ? 38 : 30) + .paddingAll(6), + if (_shouldBuildPasswordIcon(peer)) + Positioned( + top: 1, + left: 1, + child: Icon(Icons.key, size: 6, color: Colors.white), ), - ), - alignment: Alignment.center, - width: isMobile ? 50 : 42, - height: isMobile ? 50 : null, - child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30) - .paddingAll(6), - ), + ], + )), Expanded( child: Container( decoration: BoxDecoration( @@ -216,12 +225,6 @@ class _PeerCardState extends State<_PeerCard> child: child, ), ), - if (_shouldBuildPasswordIcon(peer)) - Positioned( - top: 2, - left: isMobile ? 60 : 50, - child: Icon(Icons.key, size: 12), - ), if (colors.isNotEmpty) Positioned( top: 2, @@ -329,7 +332,7 @@ class _PeerCardState extends State<_PeerCard> Positioned( top: 4, left: 12, - child: Icon(Icons.key, size: 12), + child: Icon(Icons.key, size: 12, color: Colors.white), ), if (colors.isNotEmpty) Positioned( @@ -1102,7 +1105,8 @@ class AddressBookPeerCard extends BasePeerCard { MenuEntryBase _changeSharedAbPassword() { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( - translate('Set shared password'), + translate( + peer.password.isEmpty ? 'Set shared password' : 'Change Password'), style: style, ), proc: () { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index ba13c51435..4f3a04a14c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -320,7 +320,7 @@ class AbModel { peer['password'] = password; } final ret = await addPeersTo([peer], _currentName.value); - _timerCounter = 0; + _syncAllFromRecent = true; return ret; } @@ -364,7 +364,7 @@ class AbModel { final personalAb = addressbooks[_personalAddressBookName]; if (personalAb != null) { ret = await personalAb.changePersonalHashPassword(id, hash); - await pullNonLegacyAfterChange(); + await personalAb.pullAb(quiet: true); } else { final legacyAb = addressbooks[_legacyAddressBookName]; if (legacyAb != null) { @@ -377,9 +377,10 @@ class AbModel { Future changeSharedPassword( String abName, String id, String password) async { - final ret = - await addressbooks[abName]?.changeSharedPassword(id, password) ?? false; - await pullNonLegacyAfterChange(); + final ab = addressbooks[abName]; + if (ab == null) return false; + final ret = await ab.changeSharedPassword(id, password); + await ab.pullAb(quiet: true); return ret; } @@ -538,9 +539,7 @@ class AbModel { "name": key, "tags": value.tags, "peers": value.peers - .map((e) => value.isPersonal() - ? e.toPersonalAbUploadJson(true) - : e.toSharedAbCacheJson()) + .map((e) => e.toCustomJson(includingHash: value.isPersonal())) .toList(), "tag_colors": jsonEncode(value.tagColors) }); @@ -745,6 +744,10 @@ abstract class BaseAb { name() == _legacyAddressBookName; } + bool isLegacy() { + return name() == _legacyAddressBookName; + } + Future pullAb({quiet = false}) async { debugPrint("pull ab \"${name()}\""); if (abLoading.value) return; @@ -1049,9 +1052,6 @@ class LegacyAb extends BaseAb { p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname; p.platform = r.platform.isEmpty ? p.platform : r.platform; p.alias = p.alias.isEmpty ? r.alias : p.alias; - p.forceAlwaysRelay = r.forceAlwaysRelay; - p.rdpPort = r.rdpPort; - p.rdpUsername = r.rdpUsername; } @override @@ -1151,7 +1151,7 @@ class LegacyAb extends BaseAb { Map _serialize() { final peersJsonData = - peers.map((e) => e.toPersonalAbUploadJson(true)).toList(); + peers.map((e) => e.toCustomJson(includingHash: true)).toList(); for (var e in tags) { if (tagColors[e] == null) { tagColors[e] = str2color2(e, existing: tagColors.values.toList()).value; @@ -1491,38 +1491,55 @@ class Ab extends BaseAb { Future changePersonalHashPassword(String id, String hash) async { if (!personal) return false; if (!peers.any((e) => e.id == id)) return false; - return _setPassword({"id": id, "hash": hash}); + return await _setPassword({"id": id, "hash": hash}); } @override Future changeSharedPassword(String id, String password) async { if (personal) return false; - return _setPassword({"id": id, "password": password}); + return await _setPassword({"id": id, "password": password}); } @override Future syncFromRecent(List recents) async { bool uiUpdate = false; - bool peerSyncEqual(Peer a, Peer b) { - return a.username == b.username && - a.platform == b.platform && - a.hostname == b.hostname; - } - - Future syncOnePeer(Peer p, Peer r) async { - p.username = r.username; - p.hostname = r.hostname; - p.platform = r.platform; - final api = - "${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}"; - var headers = getHttpHeaders(); - headers['Content-Type'] = "application/json"; - final body = jsonEncode({ - "id": p.id, - "username": r.username, - "hostname": r.hostname, - "platform": r.platform - }); + bool saveCache = false; + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + + Future trySyncOnePeer(Peer p, Peer r) async { + var map = Map.fromEntries([]); + if (p.sameServer != true && + r.username.isNotEmpty && + p.username != r.username) { + p.username = r.username; + map['username'] = r.username; + } + if (p.sameServer != true && + r.hostname.isNotEmpty && + p.hostname != r.hostname) { + p.hostname = r.hostname; + map['hostname'] = r.hostname; + } + if (p.sameServer != true && + r.platform.isNotEmpty && + p.platform != r.platform) { + p.platform = r.platform; + map['platform'] = r.platform; + } + if (personal && r.hash.isNotEmpty && p.hash != r.hash) { + p.hash = r.hash; + map['hash'] = r.hash; + saveCache = true; + } + if (map.isEmpty) { + // no need to sync + return false; + } + map['id'] = p.id; + final body = jsonEncode(map); final resp = await http.put(Uri.parse(api), headers: headers, body: body); final errMsg = _jsonDecodeActionResp(resp); if (errMsg.isNotEmpty) { @@ -1534,35 +1551,20 @@ class Ab extends BaseAb { } try { - /* Remove this because IDs that are not on the server can't be synced, then sync will happen every startup. - // Try add new peers to personal ab - if (personal) { - for (var r in recents) { - if (peers.length < gFFI.abModel._maxPeerOneAb) { - if (!peers.any((e) => e.id == r.id)) { - var err = await addPeers([r.toPersonalAbUploadJson(true)]); - if (err == null) { - peers.add(r); - uiUpdate = true; - } - } - } - } - } - */ - final syncPeers = peers.where((p0) => p0.sameServer != true); - for (var p in syncPeers) { + // Not add new peers because IDs that are not on the server can't be synced, then sync will happen every startup. + for (var p in peers) { Peer? r = recents.firstWhereOrNull((e) => e.id == p.id); if (r != null) { - if (!peerSyncEqual(p, r)) { - await syncOnePeer(p, r); - } + await trySyncOnePeer(p, r); } } // Pull cannot be used for sync to avoid cyclic sync. if (uiUpdate && gFFI.abModel.currentName.value == profile.name) { peers.refresh(); } + if (saveCache) { + gFFI.abModel._saveCache(); + } } catch (err) { debugPrint('syncFromRecent err: ${err.toString()}'); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3f316ef467..0a58aa0235 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -352,13 +352,13 @@ class FfiModel with ChangeNotifier { handleReloading(evt); } else if (name == 'plugin_option') { handleOption(evt); - } else if (name == "sync_peer_password_to_ab") { + } else if (name == "sync_peer_hash_password_to_personal_ab") { if (desktopType == DesktopType.main) { final id = evt['id']; - final password = evt['password']; - if (id != null && password != null) { + final hash = evt['hash']; + if (id != null && hash != null) { gFFI.abModel - .changePersonalHashPassword(id.toString(), password.toString()); + .changePersonalHashPassword(id.toString(), hash.toString()); } } } else if (name == "cm_file_transfer_log") { diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 8b853b3fec..188dd4e0bd 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -61,7 +61,7 @@ class Peer { }; } - Map toPersonalAbUploadJson(bool includingHash) { + Map toCustomJson({required bool includingHash}) { var res = { "id": id, "username": username, @@ -76,32 +76,6 @@ class Peer { return res; } - Map toSharedAbUploadJson(bool includingPassword) { - var res = { - "id": id, - "username": username, - "hostname": hostname, - "platform": platform, - "alias": alias, - "tags": tags, - }; - if (includingPassword) { - res['password'] = password; - } - return res; - } - - Map toSharedAbCacheJson() { - return { - "id": id, - "username": username, - "hostname": hostname, - "platform": platform, - "alias": alias, - "tags": tags, - }; - } - Map toGroupCacheJson() { return { "id": id, diff --git a/src/client.rs b/src/client.rs index b323310726..0ff275bc4d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1129,6 +1129,53 @@ impl VideoHandler { } } +// The source of sent password +#[derive(Debug, Clone, PartialEq, Eq)] +enum PasswordSource { + PersonalAb(Vec), + SharedAb(String), + Undefined, +} + +impl Default for PasswordSource { + fn default() -> Self { + PasswordSource::Undefined + } +} + +impl PasswordSource { + // Whether the password is personal ab password + pub fn is_personal_ab(&self, password: &[u8]) -> bool { + if password.is_empty() { + return false; + } + match self { + PasswordSource::PersonalAb(p) => p == password, + _ => false, + } + } + + // Whether the password is shared ab password + pub fn is_shared_ab(&self, password: &[u8], hash: &Hash) -> bool { + if password.is_empty() { + return false; + } + match self { + PasswordSource::SharedAb(p) => Self::equal(p, password, hash), + _ => false, + } + } + + // Whether the password equals to the connected password + fn equal(password: &str, connected_password: &[u8], hash: &Hash) -> bool { + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&hash.salt); + let res = hasher.finalize(); + connected_password[..] == res[..] + } +} + /// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { @@ -1155,7 +1202,8 @@ pub struct LoginConfigHandler { pub mark_unsupported: Vec, pub selected_windows_session_id: Option, pub peer_info: Option, - shared_password: Option, // used to distinguish whether it is connected with a shared password + password_source: PasswordSource, // where the sent password comes from + shared_password: Option, // Store the shared password } impl Deref for LoginConfigHandler { @@ -1829,20 +1877,25 @@ impl LoginConfigHandler { platform: pi.platform.clone(), }; let mut config = self.load_config(); - let connected_with_shared_password = self.is_connected_with_shared_password(); - let old_config_password = config.password.clone(); config.info = serde; let password = self.password.clone(); let password0 = config.password.clone(); let remember = self.remember; + let hash = self.hash.clone(); if remember { - if !password.is_empty() && password != password0 { - config.password = password; + // remember is true: use PeerConfig password or ui login + // not sync shared password to recent + if !password.is_empty() + && password != password0 + && !self.password_source.is_shared_ab(&password, &hash) + { + config.password = password.clone(); log::debug!("remember password of {}", self.id); } } else { - if self.save_ab_password_to_recent { - config.password = password; + if self.password_source.is_personal_ab(&password) { + // sync personal ab password to recent automatically + config.password = password.clone(); log::debug!("save ab password of {} to recent", self.id); } else if !password0.is_empty() { config.password = Default::default(); @@ -1863,13 +1916,16 @@ impl LoginConfigHandler { } #[cfg(feature = "flutter")] { - if !connected_with_shared_password && remember && !config.password.is_empty() { - // sync ab password with PeerConfig password - let password = base64::encode(config.password.clone(), base64::Variant::Original); + // sync connected password to personal ab automatically if it is not shared password + if !config.password.is_empty() + && !self.password_source.is_shared_ab(&password, &hash) + && !self.password_source.is_personal_ab(&password) + { + let hash = base64::encode(config.password.clone(), base64::Variant::Original); let evt: HashMap<&str, String> = HashMap::from([ - ("name", "sync_peer_password_to_ab".to_string()), + ("name", "sync_peer_hash_password_to_personal_ab".to_string()), ("id", self.id.clone()), - ("password", password), + ("hash", hash), ]); let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); @@ -1893,27 +1949,12 @@ impl LoginConfigHandler { config.keyboard_mode = KeyboardMode::Legacy.to_string(); } } - // keep hash password unchanged if connected with shared password - if connected_with_shared_password { - config.password = old_config_password; - } // no matter if change, for update file time self.save_config(config); self.supported_encoding = pi.encoding.clone().unwrap_or_default(); log::info!("peer info supported_encoding:{:?}", self.supported_encoding); } - fn is_connected_with_shared_password(&self) -> bool { - if let Some(shared_password) = self.shared_password.as_ref() { - let mut hasher = Sha256::new(); - hasher.update(shared_password); - hasher.update(&self.hash.salt); - let res = hasher.finalize(); - return self.password.clone()[..] == res[..]; - } - false - } - pub fn get_remote_dir(&self) -> String { serde_json::from_str::>(&self.get_option("remote_dir")) .unwrap_or_default() @@ -2565,7 +2606,6 @@ pub fn handle_login_error( err: &str, interface: &impl Interface, ) -> bool { - lc.write().unwrap().save_ab_password_to_recent = false; if err == LOGIN_MSG_PASSWORD_EMPTY { lc.write().unwrap().password = Default::default(); interface.msgbox("input-password", "Password Required", "", ""); @@ -2617,14 +2657,20 @@ pub async fn handle_hash( peer: &mut Stream, ) { lc.write().unwrap().hash = hash.clone(); + // Take care of password application order + + // switch_uuid let uuid = lc.write().unwrap().switch_uuid.take(); if let Some(uuid) = uuid { if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { send_switch_login_request(lc.clone(), peer, uuid).await; + lc.write().unwrap().password_source = Default::default(); return; } } + // last password let mut password = lc.read().unwrap().password.clone(); + // preset password if password.is_empty() { if !password_preset.is_empty() { let mut hasher = Sha256::new(); @@ -2632,31 +2678,32 @@ pub async fn handle_hash( hasher.update(&hash.salt); let res = hasher.finalize(); password = res[..].into(); + lc.write().unwrap().password_source = Default::default(); + } + } + // shared password + // Currently it's used only when click shared ab peer card + let shared_password = lc.write().unwrap().shared_password.take(); + if let Some(shared_password) = shared_password { + if !shared_password.is_empty() { + let mut hasher = Sha256::new(); + hasher.update(shared_password.clone()); + hasher.update(&hash.salt); + let res = hasher.finalize(); + password = res[..].into(); + lc.write().unwrap().password_source = PasswordSource::SharedAb(shared_password); } } + // peer config password if password.is_empty() { password = lc.read().unwrap().config.password.clone(); + if !password.is_empty() { + lc.write().unwrap().password_source = Default::default(); + } } + // personal ab password if password.is_empty() { - let access_token = LocalConfig::get_option("access_token"); - let ab = hbb_common::config::Ab::load(); - if !access_token.is_empty() && access_token == ab.access_token { - let id = lc.read().unwrap().id.clone(); - if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) { - if let Some(p) = ab - .peers - .iter() - .find_map(|p| if p.id == id { Some(p) } else { None }) - { - if let Ok(hash) = base64::decode(p.hash.clone(), base64::Variant::Original) { - if !hash.is_empty() { - password = hash; - lc.write().unwrap().save_ab_password_to_recent = true; - } - } - } - } - } + try_get_password_from_personal_ab(lc.clone(), &mut password); } lc.write().unwrap().password = password.clone(); let password = if password.is_empty() { @@ -2677,6 +2724,31 @@ pub async fn handle_hash( lc.write().unwrap().hash = hash; } +#[inline] +fn try_get_password_from_personal_ab(lc: Arc>, password: &mut Vec) { + let access_token = LocalConfig::get_option("access_token"); + let ab = hbb_common::config::Ab::load(); + if !access_token.is_empty() && access_token == ab.access_token { + let id = lc.read().unwrap().id.clone(); + if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) { + if let Some(p) = ab + .peers + .iter() + .find_map(|p| if p.id == id { Some(p) } else { None }) + { + if let Ok(hash_password) = base64::decode(p.hash.clone(), base64::Variant::Original) + { + if !hash_password.is_empty() { + *password = hash_password.clone(); + lc.write().unwrap().password_source = + PasswordSource::PersonalAb(hash_password); + } + } + } + } + } +} + /// Send login message to peer. /// /// # Arguments @@ -2722,9 +2794,13 @@ pub async fn handle_login_from_ui( let mut password2 = lc.read().unwrap().password.clone(); if password2.is_empty() { password2 = lc.read().unwrap().config.password.clone(); + if !password2.is_empty() { + lc.write().unwrap().password_source = Default::default(); + } } password2 } else { + lc.write().unwrap().password_source = Default::default(); let mut hasher = Sha256::new(); hasher.update(password); hasher.update(&lc.read().unwrap().hash.salt); diff --git a/src/flutter.rs b/src/flutter.rs index af150c3f51..ca4057d36c 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1050,8 +1050,17 @@ pub fn session_add( LocalConfig::set_remote_id(&id); + let mut preset_password = password.clone(); + let shared_password = if is_shared_password { + // To achieve a flexible password application order, we dont' treat shared password as a preset password. + preset_password = Default::default(); + Some(password) + } else { + None + }; + let session: Session = Session { - password: password.clone(), + password: preset_password, server_keyboard_enabled: Arc::new(RwLock::new(true)), server_file_transfer_enabled: Arc::new(RwLock::new(true)), server_clipboard_enabled: Arc::new(RwLock::new(true)), @@ -1069,11 +1078,6 @@ pub fn session_add( #[cfg(not(feature = "gpucodec"))] let adapter_luid = None; - let shared_password = if is_shared_password { - Some(password) - } else { - None - }; session.lc.write().unwrap().initialize( id.to_owned(), conn_type,