diff --git a/Cargo.toml b/Cargo.toml index fa22dcd7b..e0f78c29a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ screencapturekit = ["cpal/screencapturekit"] [dependencies] async-trait = "0.1" scrap = { path = "libs/scrap", features = ["wayland"] } -hbb_common = { path = "libs/hbb_common" } +hbb_common = { path = "libs/hbb_common", features = ["webrtc"] } serde_derive = "1.0" serde = "1.0" serde_json = "1.0" diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index adf7b1d45..832b96d24 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -142,10 +142,6 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse"; const String kOptionCodecPreference = "codec-preference"; const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left"; const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right"; -const String kOptionRemoteMenubarEdge = "remote-menubar-edge"; -const String kOptionRemoteMenubarFraction = "remote-menubar-frac"; -const String kOptionAllowMultiEdgeToolbarDock = - "allow-multi-edge-toolbar-dock"; const String kOptionHideAbTagsPanel = "hideAbTagsPanel"; const String kOptionRemoteMenubarState = "remoteMenubarState"; const String kOptionPeerSorting = "peer-sorting"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d1d620014..2841c1d27 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -488,16 +488,6 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), - if (!bind.isIncomingOnly()) - _OptionCheckBox( - context, - 'allow-remote-toolbar-docking-any-edge', - kOptionAllowMultiEdgeToolbarDock, - isServer: false, - update: (_) { - reloadAllWindows(); - }, - ), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), if (!isWeb) wallpaper(), if (!isWeb && !bind.isIncomingOnly()) ...[ diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 44a2dc1c7..645cbe1cb 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -28,220 +28,6 @@ import './kb_layout_type_chooser.dart'; import 'package:flutter_hbb/utils/scale.dart'; import 'package:flutter_hbb/common/widgets/custom_scale_base.dart'; -enum _ToolbarEdge { top, right, bottom, left } - -_ToolbarEdge _parseToolbarEdge(String? s) { - switch (s) { - case 'right': - return _ToolbarEdge.right; - case 'bottom': - return _ToolbarEdge.bottom; - case 'left': - return _ToolbarEdge.left; - default: - return _ToolbarEdge.top; - } -} - -String _toolbarEdgeToString(_ToolbarEdge e) { - switch (e) { - case _ToolbarEdge.top: - return 'top'; - case _ToolbarEdge.right: - return 'right'; - case _ToolbarEdge.bottom: - return 'bottom'; - case _ToolbarEdge.left: - return 'left'; - } -} - -bool _isHorizontalEdge(_ToolbarEdge e) => - e == _ToolbarEdge.top || e == _ToolbarEdge.bottom; - -const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x'; - -double _clampToolbarFraction(double fraction, double left, double right) { - if (fraction < left) fraction = left; - if (fraction > right) fraction = right; - return fraction; -} - -Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) { - final isHorizontal = _isHorizontalEdge(edge); - final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360); - final size = measured ?? fallback; - final long = size.longestSide; - final short = size.shortestSide; - return Size(isHorizontal ? long : short, isHorizontal ? short : long); -} - -Offset _toolbarOffsetForEdge({ - required _ToolbarEdge edge, - required double fraction, - required Size parentSize, - required Size toolbarSize, -}) { - final xTravel = parentSize.width - toolbarSize.width; - final yTravel = parentSize.height - toolbarSize.height; - switch (edge) { - case _ToolbarEdge.top: - return Offset(xTravel * fraction, 0); - case _ToolbarEdge.bottom: - return Offset(xTravel * fraction, yTravel); - case _ToolbarEdge.left: - return Offset(0, yTravel * fraction); - case _ToolbarEdge.right: - return Offset(xTravel, yTravel * fraction); - } -} - -double _fractionForAlignedDrag({ - required double cursor, - required double grabOffset, - required double parentExtent, - required double toolbarExtent, - required double left, - required double right, -}) { - final travelExtent = parentExtent - toolbarExtent; - if (travelExtent <= 0) { - return _clampToolbarFraction(0.5, left, right); - } - return _clampToolbarFraction( - (cursor - grabOffset) / travelExtent, left, right); -} - -({double left, double right}) _fractionBoundsForEdge( - _ToolbarEdge edge, - double left, - double right, -) { - return _isHorizontalEdge(edge) - ? (left: left, right: right) - : (left: 0, right: 1); -} - -String _toolbarRawFraction({ - required bool multiEdgeEnabled, - required _ToolbarEdge edge, - required String? savedFraction, - required String? legacyFraction, -}) { - if (!multiEdgeEnabled) { - return (legacyFraction != null && legacyFraction.isNotEmpty) - ? legacyFraction - : '0.5'; - } - if (savedFraction != null && savedFraction.isNotEmpty) { - return savedFraction; - } - if (edge == _ToolbarEdge.top && - legacyFraction != null && - legacyFraction.isNotEmpty) { - return legacyFraction; - } - return '0.5'; -} - -// Returns the alignment for the wrapper Align that positions the entire -// toolbar against the given edge at the given fraction along that edge. -// Alignment uses [-1, 1] coordinates (0 = center). -Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) { - final f = fraction * 2 - 1; - switch (edge) { - case _ToolbarEdge.top: - return Alignment(f, -1); - case _ToolbarEdge.bottom: - return Alignment(f, 1); - case _ToolbarEdge.left: - return Alignment(-1, f); - case _ToolbarEdge.right: - return Alignment(1, f); - } -} - -// The drag handle hangs off the side of the toolbar facing away from the -// docked edge, so the icons themselves sit flush against that edge. -BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) { - const r = Radius.circular(5); - switch (edge) { - case _ToolbarEdge.top: - return const BorderRadius.vertical(bottom: r); - case _ToolbarEdge.bottom: - return const BorderRadius.vertical(top: r); - case _ToolbarEdge.left: - return const BorderRadius.horizontal(right: r); - case _ToolbarEdge.right: - return const BorderRadius.horizontal(left: r); - } -} - -int _monitorMenuQuarterTurns(_ToolbarEdge edge) { - switch (edge) { - case _ToolbarEdge.left: - return 1; - case _ToolbarEdge.right: - return 3; - case _ToolbarEdge.top: - case _ToolbarEdge.bottom: - return 0; - } -} - -IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) { - switch (edge) { - case _ToolbarEdge.top: - return isCollapsed ? Icons.expand_more : Icons.expand_less; - case _ToolbarEdge.bottom: - return isCollapsed ? Icons.expand_less : Icons.expand_more; - case _ToolbarEdge.left: - return isCollapsed ? Icons.chevron_right : Icons.chevron_left; - case _ToolbarEdge.right: - return isCollapsed ? Icons.chevron_left : Icons.chevron_right; - } -} - -class _ToolbarDockingOptions { - _ToolbarDockingOptions({ - required this.edge, - required this.fraction, - required this.multiEdgeEnabled, - }); - - _ToolbarEdge edge; - double fraction; - bool multiEdgeEnabled; -} - -final _toolbarDockingOptionsBySession = {}; - -String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString(); - -_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) => - _toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)]; - -void _cacheToolbarDockingOptions({ - required SessionID sessionId, - required _ToolbarEdge edge, - required double fraction, - required bool multiEdgeEnabled, -}) { - final key = _toolbarDockingCacheKey(sessionId); - final cached = _toolbarDockingOptionsBySession[key]; - if (cached == null) { - _toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions( - edge: edge, - fraction: fraction, - multiEdgeEnabled: multiEdgeEnabled, - ); - return; - } - cached.edge = edge; - cached.fraction = fraction; - cached.multiEdgeEnabled = multiEdgeEnabled; -} - class ToolbarState { late RxBool _pin; @@ -464,26 +250,8 @@ class RemoteToolbar extends StatefulWidget { class _RemoteToolbarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - final _fraction = 0.5.obs; - final _edge = _ToolbarEdge.top.obs; + final _fractionX = 0.5.obs; final _dragging = false.obs; - // Live drag preview: where the toolbar would dock if the user dropped now. - final _previewEdge = Rxn<_ToolbarEdge>(); - final _previewFraction = Rxn(); - // Measured size of the live toolbar, so the preview ghost matches reality - // (collapsed handle vs expanded toolbar). Updated after every layout pass. - final _toolbarSize = Rxn(); - final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root'); - // When false (default), the toolbar stays on the top edge and the drag - // handle just slides it horizontally — preserving long-standing UX while - // still fixing the bug where dragging only moved the handle. When true, - // the user has opted into multi-edge docking with nearest-edge snap. - // Kept in sync after settings-triggered rebuilds. - final _multiEdgeEnabled = false.obs; - final _dockingOptionsInitialized = false.obs; - bool _pendingDockingOptionSync = false; - int _dockingOptionSyncSerial = 0; - int _dragEpoch = 0; int get windowId => stateGlobal.windowId; @@ -505,144 +273,16 @@ class _RemoteToolbarState extends State { void _minimize() async => await WindowController.fromWindowId(windowId).minimize(); - Future _syncDockingOptions({required bool force}) async { - final syncSerial = ++_dockingOptionSyncSerial; - if (_dragging.isTrue) { - _deferDockingOptionsSync(); - return; - } - final dragEpoch = _dragEpoch; - - // Use the canonical helper so the option's documented default semantics - // apply (allow-* prefix => default false). Keeping it raw-string would - // diverge from how _OptionCheckBox displays the same key. - final multiEdgeEnabled = - mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); - final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); - if (cached == null && pi.isSet.isFalse) { - return; - } - final hadDockingOptions = cached != null; - final wasMultiEdgeEnabled = - cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value; - if (!force && - hadDockingOptions && - wasMultiEdgeEnabled == multiEdgeEnabled) { - _pendingDockingOptionSync = false; - return; - } - - final savedFraction = await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction); - // Backward compat: legacy horizontal-only position. - final legacyFraction = await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX); - if (!mounted || syncSerial != _dockingOptionSyncSerial) return; - - var nextEdge = _edge.value; - var savedFractionForNextEdge = savedFraction; - var keepCurrentPosition = false; - if (!multiEdgeEnabled) { - nextEdge = _ToolbarEdge.top; - } else if (force || wasMultiEdgeEnabled || cached == null) { - final edgeStr = await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge); - if (!mounted || syncSerial != _dockingOptionSyncSerial) return; - nextEdge = _parseToolbarEdge(edgeStr); - } else { - // The setting changed from top-only to multi-edge while this toolbar is - // already visible. Keep its current position instead of jumping to the - // last saved multi-edge dock. - nextEdge = cached.edge; - savedFractionForNextEdge = cached.fraction.toString(); - keepCurrentPosition = true; - } - - final rawFraction = _toolbarRawFraction( - multiEdgeEnabled: multiEdgeEnabled, - edge: nextEdge, - savedFraction: savedFractionForNextEdge, - legacyFraction: legacyFraction, - ); - // Clamp to the saved drag-bound contract so a corrupted or out-of-range - // saved value can't bypass it until the user drags again. - final dragLeft = double.tryParse( - bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ?? - 0.0; - final dragRight = double.tryParse( - bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ?? - 1.0; - final fractionBounds = - _fractionBoundsForEdge(nextEdge, dragLeft, dragRight); - final nextFraction = (double.tryParse(rawFraction) ?? 0.5) - .clamp(fractionBounds.left, fractionBounds.right) - .toDouble(); - if (!mounted || syncSerial != _dockingOptionSyncSerial) return; - if (_dragging.isTrue || dragEpoch != _dragEpoch) { - _deferDockingOptionsSync(); - return; - } - _edge.value = nextEdge; - _fraction.value = nextFraction; - _multiEdgeEnabled.value = multiEdgeEnabled; - _dockingOptionsInitialized.value = true; - _cacheToolbarDockingOptions( - sessionId: widget.ffi.sessionId, - edge: nextEdge, - fraction: nextFraction, - multiEdgeEnabled: multiEdgeEnabled, - ); - _pendingDockingOptionSync = false; - if (!multiEdgeEnabled || keepCurrentPosition) { - bind.sessionPeerOption( - sessionId: widget.ffi.sessionId, - name: kOptionRemoteMenubarEdge, - value: _toolbarEdgeToString(nextEdge), - ); - bind.sessionPeerOption( - sessionId: widget.ffi.sessionId, - name: kOptionRemoteMenubarFraction, - value: nextFraction.toString(), - ); - } - } - - void _deferDockingOptionsSync() { - _pendingDockingOptionSync = true; - if (_dragging.isFalse) { - _syncDockingOptionsAfterDragIfNeeded(); - } - } - - void _markToolbarDragEpoch() { - ++_dragEpoch; - } - - void _syncDockingOptionsAfterDragIfNeeded() { - if (!_pendingDockingOptionSync) return; - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: false); - }); - } - @override initState() { super.initState(); - final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); - final multiEdgeEnabled = - mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); - final shouldResetToTop = - cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled; - if (cached != null && !shouldResetToTop) { - _edge.value = cached.edge; - _fraction.value = cached.fraction; - _multiEdgeEnabled.value = multiEdgeEnabled; - _dockingOptionsInitialized.value = true; - } - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: cached == null || shouldResetToTop); + _fractionX.value = double.tryParse(await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, + arg: 'remote-menubar-drag-x') ?? + '0.5') ?? + 0.5; // Initialize toolbar states (collapse, hide) from session options widget.state.init(widget.ffi.sessionId); }); @@ -663,14 +303,6 @@ class _RemoteToolbarState extends State { }); } - @override - void didUpdateWidget(covariant RemoteToolbar oldWidget) { - super.didUpdateWidget(oldWidget); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: false); - }); - } - _debouncerHideProc(int v) { if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) { collapse.value = true; @@ -679,130 +311,64 @@ class _RemoteToolbarState extends State { @override dispose() { - ++_dockingOptionSyncSerial; - widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); super.dispose(); + + widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); } @override Widget build(BuildContext context) { return Obx(() { // Wait for initialization to complete to prevent flickering - if (!widget.state.initialized.value || - !_dockingOptionsInitialized.value) { + if (!widget.state.initialized.value) { return const SizedBox.shrink(); } // If toolbar is hidden, return empty widget if (hide.value) { return const SizedBox.shrink(); } - final edge = _edge.value; - final isHorizontal = _isHorizontalEdge(edge); - - // Measure the live toolbar after every layout so the preview ghost can - // match its actual footprint (collapsed handle vs expanded toolbar). - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_dragging.isTrue) return; - final ro = _toolbarKey.currentContext?.findRenderObject(); - if (ro is RenderBox && ro.hasSize) { - final s = ro.size; - if (_toolbarSize.value != s) _toolbarSize.value = s; - } - }); - - final toolbar = Align( - alignment: _alignmentForEdge(edge, _fraction.value), - child: KeyedSubtree( - key: _toolbarKey, - child: collapse.isFalse - ? _buildToolbar(context, edge, isHorizontal) - : _buildDraggableCollapse(context, edge, isHorizontal), - ), - ); - - // Always return the Stack — even when not dragging — so the toolbar's - // position in the Element tree stays stable. Wrapping/unwrapping it - // mid-drag was killing the Draggable's gesture state. - return Stack( - fit: StackFit.expand, - children: [ - IgnorePointer( - child: Obx(() { - final pe = _previewEdge.value; - final pf = _previewFraction.value; - if (!_dragging.isTrue || pe == null || pf == null) { - return const SizedBox.shrink(); - } - return _buildDragPreview(context, pe, pf, _toolbarSize.value); - }), - ), - toolbar, - ], + return Align( + alignment: Alignment.topCenter, + child: collapse.isFalse + ? _buildToolbar(context) + : _buildDraggableCollapse(context), ); }); } - Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge, - double fraction, Size? measured) { - final color = Theme.of(context).colorScheme.primary; - // Use the measured live toolbar size so collapsed vs expanded looks - // right. The current orientation may differ from the preview orientation - // (e.g. dragging a top-docked toolbar toward the left edge), so swap the - // long/short axes when previewing a different orientation. - final previewSize = _toolbarSizeForEdge(edge, measured); - return Align( - alignment: _alignmentForEdge(edge, fraction), - child: Container( - width: previewSize.width, - height: previewSize.height, - decoration: BoxDecoration( - color: color.withOpacity(0.10), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: color.withOpacity(0.55), width: 1.5), - ), - ), - ); - } - - Widget _buildDraggableCollapse( - BuildContext context, _ToolbarEdge edge, bool isHorizontal) { + Widget _buildDraggableCollapse(BuildContext context) { return Obx(() { if (collapse.isFalse && _dragging.isFalse) { triggerAutoHide(); } - final borderRadius = _collapseHandleBorderRadius(edge); - return Offstage( - offstage: _dragging.isTrue, - child: Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, - borderRadius: borderRadius, - child: _DraggableShowHide( - id: widget.id, - sessionId: widget.ffi.sessionId, - dragging: _dragging, - fraction: _fraction, - edge: _edge, - previewEdge: _previewEdge, - previewFraction: _previewFraction, - toolbarSize: _toolbarSize, - markDragEpoch: _markToolbarDragEpoch, - syncDockingOptionsAfterDragIfNeeded: - _syncDockingOptionsAfterDragIfNeeded, - isHorizontal: isHorizontal, - multiEdgeEnabled: _multiEdgeEnabled.value, - toolbarState: widget.state, - setFullscreen: _setFullscreen, - setMinimize: _minimize, + final borderRadius = BorderRadius.vertical( + bottom: Radius.circular(5), + ); + return Align( + alignment: FractionalOffset(_fractionX.value, 0), + child: Offstage( + offstage: _dragging.isTrue, + child: Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, borderRadius: borderRadius, + child: _DraggableShowHide( + id: widget.id, + sessionId: widget.ffi.sessionId, + dragging: _dragging, + fractionX: _fractionX, + toolbarState: widget.state, + setFullscreen: _setFullscreen, + setMinimize: _minimize, + borderRadius: borderRadius, + ), ), ), ); }); } - Widget _buildToolbar( - BuildContext context, _ToolbarEdge edge, bool isHorizontal) { + Widget _buildToolbar(BuildContext context) { final List toolbarItems = []; toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { @@ -816,7 +382,6 @@ class _RemoteToolbarState extends State { return _MonitorMenu( id: widget.id, ffi: widget.ffi, - edge: edge, setRemoteState: widget.setRemoteState); } else { return Offstage(); @@ -842,53 +407,37 @@ class _RemoteToolbarState extends State { if (!isWeb) toolbarItems.add(_RecordMenu()); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0)); - // innerAxis: how the toolbar icons themselves flow. - // outerAxis: how the toolbar block and the handle stack against each other - // (perpendicular to the dock edge, so the handle hangs off the interior face). - final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical; - final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal; - final spacer = isHorizontal - ? SizedBox(width: _ToolbarTheme.buttonHMargin * 2) - : SizedBox(height: _ToolbarTheme.buttonHMargin * 2); - final toolbarMaterial = Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, - borderRadius: toolbarBorderRadius, - color: Theme.of(context) - .menuBarTheme - .style - ?.backgroundColor - ?.resolve(MaterialState.values.toSet()), - child: SingleChildScrollView( - scrollDirection: innerAxis, - child: Theme( - data: themeData(), - child: _ToolbarTheme.borderWrapper( - context, - Flex( - direction: innerAxis, - mainAxisSize: MainAxisSize.min, - children: [ - spacer, - ...toolbarItems, - spacer, - ], - ), - toolbarBorderRadius), - ), - ), - ); - final handle = _buildDraggableCollapse(context, edge, isHorizontal); - // The handle hangs off the interior face of the toolbar (away from the - // docked edge), centered along that face by the Flex's default cross-axis - // alignment, so the icons themselves sit flush against the docked edge. - final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left) - ? [toolbarMaterial, handle] - : [handle, toolbarMaterial]; - return Flex( - direction: outerAxis, + return Column( mainAxisSize: MainAxisSize.min, - children: children, + children: [ + Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: toolbarBorderRadius, + color: Theme.of(context) + .menuBarTheme + .style + ?.backgroundColor + ?.resolve(MaterialState.values.toSet()), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Theme( + data: themeData(), + child: _ToolbarTheme.borderWrapper( + context, + Row( + children: [ + SizedBox(width: _ToolbarTheme.buttonHMargin * 2), + ...toolbarItems, + SizedBox(width: _ToolbarTheme.buttonHMargin * 2) + ], + ), + toolbarBorderRadius), + ), + ), + ), + _buildDraggableCollapse(context), + ], ); } @@ -967,13 +516,11 @@ class _MobileActionMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget { final String id; final FFI ffi; - final _ToolbarEdge edge; final Function(VoidCallback) setRemoteState; const _MonitorMenu({ Key? key, required this.id, required this.ffi, - required this.edge, required this.setRemoteState, }) : super(key: key); @@ -984,17 +531,9 @@ class _MonitorMenu extends StatelessWidget { !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay; @override - Widget build(BuildContext context) { - final child = showMonitorsToolbar - ? buildMultiMonitorMenu(context) - : Obx(() => buildMonitorMenu(context)); - final quarterTurns = _monitorMenuQuarterTurns(edge); - if (quarterTurns == 0) return child; - return RotatedBox( - quarterTurns: quarterTurns, - child: child, - ); - } + Widget build(BuildContext context) => showMonitorsToolbar + ? buildMultiMonitorMenu(context) + : Obx(() => buildMonitorMenu(context)); Widget buildMonitorMenu(BuildContext context) { final width = SimpleWrapper(0); @@ -1126,8 +665,7 @@ class _MonitorMenu extends StatelessWidget { } final scale = _ToolbarTheme.buttonSize / rect.height * 0.75; - final height = rect.height * scale; - final startY = (_ToolbarTheme.buttonSize - height) * 0.5; + final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5; final startX = startY; final children = []; @@ -1170,7 +708,7 @@ class _MonitorMenu extends StatelessWidget { width.value = rect.width * scale + startX * 2; return SizedBox( width: width.value, - height: height + startY * 2, + height: rect.height * scale + startY * 2, child: Stack( children: children, ), @@ -2981,18 +2519,7 @@ class RdoMenuButton extends StatelessWidget { class _DraggableShowHide extends StatefulWidget { final String id; final SessionID sessionId; - final RxDouble fraction; - final Rx<_ToolbarEdge> edge; - final Rxn<_ToolbarEdge> previewEdge; - final Rxn previewFraction; - final Rxn toolbarSize; - final VoidCallback markDragEpoch; - final VoidCallback syncDockingOptionsAfterDragIfNeeded; - final bool isHorizontal; - // Whether multi-edge docking is enabled for this session (toggled in - // Settings -> Other). When false, the drag handle slides the toolbar - // horizontally on the top edge and never switches edges. - final bool multiEdgeEnabled; + final RxDouble fractionX; final RxBool dragging; final ToolbarState toolbarState; final BorderRadius borderRadius; @@ -3004,15 +2531,7 @@ class _DraggableShowHide extends StatefulWidget { Key? key, required this.id, required this.sessionId, - required this.fraction, - required this.edge, - required this.previewEdge, - required this.previewFraction, - required this.toolbarSize, - required this.markDragEpoch, - required this.syncDockingOptionsAfterDragIfNeeded, - required this.isHorizontal, - required this.multiEdgeEnabled, + required this.fractionX, required this.dragging, required this.toolbarState, required this.setFullscreen, @@ -3025,12 +2544,10 @@ class _DraggableShowHide extends StatefulWidget { } class _DraggableShowHideState extends State<_DraggableShowHide> { + Offset position = Offset.zero; + Size size = Size.zero; double left = 0.0; double right = 1.0; - Offset? _lastPointerDown; - Offset? _dragGrabOffset; - double? _dragLongAxisGrabOffset; - Size? _dragToolbarSize; RxBool get collapse => widget.toolbarState.collapse; @@ -3056,174 +2573,41 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { } } - // Bias applied to the currently-previewed edge so a drag hovering between - // two edges doesn't flicker. Only relevant when multi-edge is enabled. - static const double _switchHysteresisPx = 50.0; - - _ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) { - if (!widget.multiEdgeEnabled) return widget.edge.value; - - double rawDist(_ToolbarEdge e) { - switch (e) { - case _ToolbarEdge.top: - return cursor.dy; - case _ToolbarEdge.bottom: - return mediaSize.height - cursor.dy; - case _ToolbarEdge.left: - return cursor.dx; - case _ToolbarEdge.right: - return mediaSize.width - cursor.dx; - } - } - - final previewed = widget.previewEdge.value; - var winner = widget.edge.value; - var best = double.infinity; - for (final e in _ToolbarEdge.values) { - final biased = - e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e); - if (biased < best) { - best = biased; - winner = e; - } - } - return winner; - } - - void _ensureDragGrabOffset(Offset cursor) { - if (_dragGrabOffset != null) return; - final mediaSize = MediaQueryData.fromView(View.of(context)).size; - final toolbarSize = - _toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value); - _dragToolbarSize = toolbarSize; - final toolbarOffset = _toolbarOffsetForEdge( - edge: widget.edge.value, - fraction: widget.fraction.value, - parentSize: mediaSize, - toolbarSize: toolbarSize, - ); - _dragGrabOffset = cursor - toolbarOffset; - _dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value) - ? _dragGrabOffset?.dx - : _dragGrabOffset?.dy; - } - - double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) { - final offset = _dragLongAxisGrabOffset ?? 0; - final extent = - _isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height; - return _clampToolbarFraction(offset, 0, extent); - } - - void _updatePreview(Offset cursor) { - _ensureDragGrabOffset(cursor); - final mediaSize = MediaQueryData.fromView(View.of(context)).size; - final winner = _nearestToolbarEdge(cursor, mediaSize); - widget.previewEdge.value = winner; - - final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize); - final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize); - final double frac; - if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) { - frac = _fractionForAlignedDrag( - cursor: cursor.dx, - grabOffset: grabOffset, - parentExtent: mediaSize.width, - toolbarExtent: toolbarSize.width, - left: left, - right: right, - ); - } else { - final fractionBounds = _fractionBoundsForEdge(winner, left, right); - frac = _fractionForAlignedDrag( - cursor: cursor.dy, - grabOffset: grabOffset, - parentExtent: mediaSize.height, - toolbarExtent: toolbarSize.height, - left: fractionBounds.left, - right: fractionBounds.right, - ); - } - widget.previewFraction.value = frac; - } - - void _resetDragTracking() { - _lastPointerDown = null; - _dragGrabOffset = null; - _dragLongAxisGrabOffset = null; - _dragToolbarSize = null; - } - - void _commitPreview() { - final newEdge = widget.previewEdge.value; - final frac = widget.previewFraction.value; - widget.previewEdge.value = null; - widget.previewFraction.value = null; - widget.dragging.value = false; - widget.markDragEpoch(); - _resetDragTracking(); - widget.syncDockingOptionsAfterDragIfNeeded(); - if (newEdge == null || frac == null) return; - widget.edge.value = newEdge; - widget.fraction.value = frac; - _cacheToolbarDockingOptions( - sessionId: widget.sessionId, - edge: newEdge, - fraction: frac, - multiEdgeEnabled: widget.multiEdgeEnabled, - ); - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: kOptionRemoteMenubarEdge, - value: _toolbarEdgeToString(newEdge), - ); - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: kOptionRemoteMenubarFraction, - value: frac.toString(), - ); - if (widget.multiEdgeEnabled) { - return; - } - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: _legacyRemoteMenubarDragX, - value: frac.toString(), - ); - } - Widget _buildDraggable(BuildContext context) { - return Listener( - onPointerDown: (event) => _lastPointerDown = event.position, - child: Draggable( - // When multi-edge docking is off the toolbar stays on the top edge, - // so lock the feedback to horizontal motion — otherwise the handle - // floats away from the top while dragging and the toolbar looks - // unmoored. When multi-edge is on we need 2D drag for snap-to-edge. - axis: widget.multiEdgeEnabled ? null : Axis.horizontal, - child: Icon( - widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle, - size: 20, - color: MyTheme.color(context).drag_indicator, - ), - feedback: widget, - onDragStarted: () { - widget.markDragEpoch(); - final pointerDown = _lastPointerDown; - if (pointerDown != null) { - _ensureDragGrabOffset(pointerDown); - } - widget.dragging.value = true; - // Seed the preview at the current docked edge/fraction so something - // shows the instant the drag begins, before the first onDragUpdate. - widget.previewEdge.value = widget.edge.value; - widget.previewFraction.value = widget.fraction.value; - }, - onDragUpdate: (details) { - _updatePreview(details.globalPosition); - }, - onDragEnd: (_) => _commitPreview(), + return Draggable( + axis: Axis.horizontal, + child: Icon( + Icons.drag_indicator, + size: 20, + color: MyTheme.color(context).drag_indicator, ), + feedback: widget, + onDragStarted: (() { + final RenderObject? renderObj = context.findRenderObject(); + if (renderObj != null) { + final RenderBox renderBox = renderObj as RenderBox; + size = renderBox.size; + position = renderBox.localToGlobal(Offset.zero); + } + widget.dragging.value = true; + }), + onDragEnd: (details) { + final mediaSize = MediaQueryData.fromView(View.of(context)).size; + widget.fractionX.value += + (details.offset.dx - position.dx) / (mediaSize.width - size.width); + if (widget.fractionX.value < left) { + widget.fractionX.value = left; + } + if (widget.fractionX.value > right) { + widget.fractionX.value = right; + } + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: 'remote-menubar-drag-x', + value: widget.fractionX.value.toString(), + ); + widget.dragging.value = false; + }, ); } @@ -3253,9 +2637,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } - final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical; - final child = Flex( - direction: axis, + final child = Row( mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), @@ -3296,7 +2678,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { message: translate( collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( - _toolbarCollapseIcon(widget.edge.value, collapse.isTrue), + collapse.isFalse ? Icons.expand_less : Icons.expand_more, size: iconSize, ), ))), @@ -3338,8 +2720,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { borderRadius: widget.borderRadius, ), child: SizedBox( - height: widget.isHorizontal ? 20 : null, - width: widget.isHorizontal ? null : 20, + height: 20, child: child, ), ), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 984d6a25c..6fdffd796 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -346,7 +346,7 @@ class InputModel { /// which runs per-engine, so each isolate registers its own handler tied /// to its own set of InputModels. static void initSideButtonChannel() { - if (!isLinux) return; + if (!Platform.isLinux) return; if (_sideButtonChannelInitialized) return; _sideButtonChannelInitialized = true; diff --git a/libs/hbb_common b/libs/hbb_common index 9043c15ac..5a78ec423 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0 +Subproject commit 5a78ec42303ca046d808742e911a156636a2432b diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 8859d0d3b..aedf786b7 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -276,21 +276,12 @@ impl PipeWireRecorder { // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 src.set_property("always-copy", &true)?; - // COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink. - // xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream - // format set is too narrow (appsink only accepts BGRx/RGBx), producing - // "no more output formats" / not-negotiated (-4). videoconvert accepts any - // system-memory video/x-raw format, widening negotiation so the portal can - // settle on a format it can deliver via its SHM path. - let convert = gst::ElementFactory::make("videoconvert", None)?; - let sink = gst::ElementFactory::make("appsink", None)?; sink.set_property("drop", &true)?; sink.set_property("max-buffers", &1u32)?; - pipeline.add_many(&[&src, &convert, &sink])?; - src.link(&convert)?; - convert.link(&sink)?; + pipeline.add_many(&[&src, &sink])?; + src.link(&sink)?; let appsink = sink .dynamic_cast::() diff --git a/src/cli.rs b/src/cli.rs index 2f3b3550f..351a580f1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use hbb_common::{ config::PeerConfig, config::READ_TIMEOUT, - futures::{SinkExt, StreamExt}, + futures::StreamExt, log, message_proto::*, protobuf::Message as _, @@ -46,6 +46,7 @@ impl Session { false, None, None, + None, ); session } @@ -53,7 +54,7 @@ impl Session { #[async_trait] impl Interface for Session { - fn get_login_config_handler(&self) -> Arc> { + fn get_lch(&self) -> Arc> { return self.lc.clone(); } @@ -61,14 +62,20 @@ impl Interface for Session { match msgtype { "input-password" => { self.sender - .send(Data::Login((self.password.clone(), true))) + .send(Data::Login(( + String::new(), + String::new(), + self.password.clone(), + true, + ))) .ok(); } "re-input-password" => { log::error!("{}: {}", title, text); match rpassword::prompt_password("Enter password: ") { Ok(password) => { - let login_data = Data::Login((password, true)); + let login_data = + Data::Login((String::new(), String::new(), password, true)); self.sender.send(login_data).ok(); } Err(e) => { @@ -93,6 +100,8 @@ impl Interface for Session { self.lc.write().unwrap().handle_peer_info(&pi); } + fn set_multiple_windows_session(&self, _sessions: Vec) {} + async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) { log::info!( "password={}", @@ -137,8 +146,8 @@ pub async fn connect_test(id: &str, key: String, token: String) { Err(err) => { log::error!("Failed to connect {}: {}", &id, err); } - Ok((mut stream, direct)) => { - log::info!("direct: {}", direct); + Ok(((mut stream, _direct, _secure, _kcp, _typ), direct)) => { + log::info!("direct: {:?}", direct); // rpassword::prompt_password("Input anything to exit").ok(); loop { tokio::select! { diff --git a/src/client.rs b/src/client.rs index 321a49ee6..fd4a5e881 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,11 +65,12 @@ use hbb_common::{ self, net::UdpSocket, sync::{ - mpsc::{unbounded_channel, UnboundedReceiver}, + mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver}, oneshot, }, time::{interval, Duration, Instant}, }, + webrtc::WebRTCStream, AddrMangle, ResultType, Stream, }; pub use helper::*; @@ -330,6 +331,19 @@ impl Client { } else { (None, None) }; + let ipv6 = if crate::get_ipv6_punch_enabled() { + crate::get_ipv6_socket().await + } else { + None + }; + let webrtc_offerer = + match WebRTCStream::new("", interface.is_force_relay(), CONNECT_TIMEOUT).await { + Ok(stream) => Some(stream), + Err(err) => { + log::warn!("webrtc offerer setup failed: {}", err); + None + } + }; let fut = Self::_start_inner( peer.to_owned(), key.to_owned(), @@ -338,6 +352,8 @@ impl Client { interface.clone(), udp.clone(), Some(stop_udp_tx), + ipv6, + webrtc_offerer, rendezvous_server.clone(), servers.clone(), contained, @@ -355,6 +371,8 @@ impl Client { interface, (None, None), None, + None, + None, rendezvous_server, servers, contained, @@ -366,6 +384,67 @@ impl Client { } } + fn is_expected_webrtc_ice_candidate(ice: &IceCandidate, session_key: &str) -> bool { + !session_key.is_empty() && ice.session_key == session_key && !ice.candidate.is_empty() + } + + fn spawn_webrtc_ice_bridge( + mut socket: Stream, + mut local_ice_rx: Option>, + webrtc: WebRTCStream, + peer: String, + session_key: String, + ) -> oneshot::Sender<()> { + let (stop_tx, mut stop_rx) = oneshot::channel::<()>(); + tokio::spawn(async move { + loop { + match stop_rx.try_recv() { + Ok(_) | Err(tokio::sync::oneshot::error::TryRecvError::Closed) => break, + Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {} + } + + if let Some(rx) = local_ice_rx.as_mut() { + loop { + match rx.try_recv() { + Ok(candidate) => { + let mut msg = RendezvousMessage::new(); + msg.set_ice_candidate(IceCandidate { + id: peer.clone(), + session_key: session_key.clone(), + candidate, + ..Default::default() + }); + if let Err(err) = socket.send(&msg).await { + log::warn!("failed to send WebRTC ICE candidate: {}", err); + return; + } + } + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => { + local_ice_rx = None; + break; + } + } + } + } + + if let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(100)).await + { + if let Some(rendezvous_message::Union::IceCandidate(ice)) = msg_in.union { + if Self::is_expected_webrtc_ice_candidate(&ice, &session_key) { + if let Err(err) = webrtc.add_remote_ice_candidate(&ice.candidate).await + { + log::warn!("failed to add WebRTC ICE candidate: {}", err); + } + } + } + } + } + }); + stop_tx + } + async fn _start_inner( peer: String, key: String, @@ -374,6 +453,8 @@ impl Client { interface: impl Interface, mut udp: (Option>, Option>>), stop_udp_tx: Option>, + mut ipv6: Option<(Arc, bytes::Bytes)>, + mut webrtc_offerer: Option, mut rendezvous_server: String, servers: Vec, contained: bool, @@ -446,14 +527,20 @@ impl Client { // Stop UDP NAT test task if still running stop_udp_tx.map(|tx| tx.send(())); let mut msg_out = RendezvousMessage::new(); - let mut ipv6 = if crate::get_ipv6_punch_enabled() { - if let Some((socket, addr)) = crate::get_ipv6_socket().await { - (Some(socket), Some(addr)) - } else { - (None, None) + let mut ipv6 = ipv6 + .take() + .map(|(socket, addr)| (Some(socket), Some(addr))) + .unwrap_or((None, None)); + let webrtc_sdp_offer = if let Some(webrtc) = webrtc_offerer.as_ref() { + match webrtc.get_local_endpoint().await { + Ok(endpoint) => endpoint, + Err(err) => { + log::warn!("failed to read local WebRTC offer: {}", err); + String::new() + } } } else { - (None, None) + String::new() }; let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; @@ -467,9 +554,16 @@ impl Client { udp_port: udp_nat_port as _, force_relay: interface.is_force_relay(), socket_addr_v6: ipv6.1.unwrap_or_default(), + webrtc_sdp_offer: webrtc_sdp_offer.clone(), ..Default::default() }); - for i in 1..=3 { + let webrtc_session_key = webrtc_offerer + .as_ref() + .map(|webrtc| webrtc.session_key().to_owned()) + .unwrap_or_default(); + let mut webrtc_sdp_answer = String::new(); + let mut pending_webrtc_ice = Vec::::new(); + 'punch_attempts: for i in 1..=3 { log::info!( "#{} {} punch attempt with {}, id: {}", i, @@ -479,9 +573,20 @@ impl Client { ); socket.send(&msg_out).await?; // below timeout should not bigger than hbbs's connection timeout. - if let Some(msg_in) = - crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await - { + let attempt_deadline = Instant::now() + Duration::from_millis((i * 3000) as u64); + loop { + let remaining = attempt_deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + break; + } + let timeout_ms = remaining + .as_millis() + .clamp(1, u64::MAX as u128) as u64; + let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout_ms)).await + else { + break; + }; match msg_in.union { Some(rendezvous_message::Union::PunchHoleResponse(ph)) => { if ph.socket_addr.is_empty() { @@ -510,6 +615,7 @@ impl Client { relay_server = ph.relay_server; peer_addr = AddrMangle::decode(&ph.socket_addr); feedback = ph.feedback; + webrtc_sdp_answer = ph.webrtc_sdp_answer; let s = udp.0.take(); if ph.is_udp && s.is_some() { if let Some(s) = s { @@ -528,7 +634,7 @@ impl Client { } } log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr); - break; + break 'punch_attempts; } } Some(rendezvous_message::Union::RelayResponse(rr)) => { @@ -549,6 +655,38 @@ impl Client { } } signed_id_pk = rr.pk().into(); + let mut webrtc_bridge_stop = None; + let mut webrtc_for_connect = None; + if !rr.webrtc_sdp_answer.is_empty() { + if let Some(webrtc) = webrtc_offerer.take() { + if let Err(err) = + webrtc.set_remote_endpoint(&rr.webrtc_sdp_answer).await + { + log::warn!("failed to set WebRTC relay answer: {}", err); + } else { + for candidate in pending_webrtc_ice.drain(..) { + if let Err(err) = + webrtc.add_remote_ice_candidate(&candidate).await + { + log::warn!( + "failed to add buffered WebRTC ICE candidate: {}", + err + ); + } + } + let session_key = webrtc.session_key().to_owned(); + let local_ice_rx = webrtc.take_local_ice_rx(); + webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge( + socket, + local_ice_rx, + webrtc.clone(), + peer.clone(), + session_key, + )); + webrtc_for_connect = Some(webrtc); + } + } + } let fut = Self::create_relay( &peer, rr.uuid, @@ -564,30 +702,81 @@ impl Client { } .boxed(), ); + if let Some(mut webrtc) = webrtc_for_connect { + connect_futures.push( + async move { + webrtc.wait_connected(CONNECT_TIMEOUT).await?; + Ok((Stream::WebRTC(webrtc), None, "WebRTC")) + } + .boxed(), + ); + } // Run all connection attempts concurrently, return the first successful one let (conn, kcp, typ) = match select_ok(connect_futures).await { Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), Err(e) => (Err(e), None, ""), }; + if let Some(stop) = webrtc_bridge_stop { + let _ = stop.send(()); + } let mut conn = conn?; feedback = rr.feedback; log::info!("{:?} used to establish {typ} connection", start.elapsed()); let pk = Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; return Ok(( - (conn, typ == "IPv6", pk, kcp, typ), + (conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ), (feedback, rendezvous_server), false, )); } + Some(rendezvous_message::Union::IceCandidate(ice)) => { + if Self::is_expected_webrtc_ice_candidate(&ice, &webrtc_session_key) { + pending_webrtc_ice.push(ice.candidate); + } else { + log::debug!( + "dropping ICE candidate for unexpected WebRTC session key {}", + ice.session_key, + ); + } + } _ => { log::error!("Unexpected protobuf msg received: {:?}", msg_in); } } } } - drop(socket); + let mut webrtc_bridge_stop = None; + let mut webrtc_for_connect = None; + if !webrtc_sdp_answer.is_empty() { + if let Some(webrtc) = webrtc_offerer.take() { + if let Err(err) = webrtc.set_remote_endpoint(&webrtc_sdp_answer).await { + log::warn!("failed to set WebRTC answer: {}", err); + drop(socket); + } else { + for candidate in pending_webrtc_ice.drain(..) { + if let Err(err) = webrtc.add_remote_ice_candidate(&candidate).await { + log::warn!("failed to add buffered WebRTC ICE candidate: {}", err); + } + } + let session_key = webrtc.session_key().to_owned(); + let local_ice_rx = webrtc.take_local_ice_rx(); + webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge( + socket, + local_ice_rx, + webrtc.clone(), + peer.clone(), + session_key, + )); + webrtc_for_connect = Some(webrtc); + } + } else { + drop(socket); + } + } else { + drop(socket); + } if peer_addr.port() == 0 { bail!("Failed to connect via rendezvous server"); } @@ -621,6 +810,8 @@ impl Client { interface, udp.0, ipv6.0, + webrtc_for_connect, + webrtc_bridge_stop, punch_type, ) .await?, @@ -647,6 +838,8 @@ impl Client { interface: impl Interface, udp_socket_nat: Option>, udp_socket_v6: Option>, + webrtc_offerer: Option, + webrtc_bridge_stop: Option>, punch_type: &str, ) -> ResultType<( Stream, @@ -705,11 +898,23 @@ impl Client { if let Some(udp_socket_v6) = udp_socket_v6 { connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed()); } + if let Some(mut webrtc) = webrtc_offerer { + connect_futures.push( + async move { + webrtc.wait_connected(connect_timeout).await?; + Ok((Stream::WebRTC(webrtc), None, "WebRTC")) + } + .boxed(), + ); + } // Run all connection attempts concurrently, return the first successful one let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await { Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), Err(e) => (Err(e), None, ""), }; + if let Some(stop) = webrtc_bridge_stop { + let _ = stop.send(()); + } let mut direct = !conn.is_err(); if interface.is_force_relay() || conn.is_err() { @@ -4120,8 +4325,22 @@ pub mod peer_online { #[cfg(test)] mod tests { + use crate::client::Client; + use hbb_common::rendezvous_proto::IceCandidate; use hbb_common::tokio; + #[test] + fn accepts_webrtc_ice_by_session_key_only() { + let ice = IceCandidate { + session_key: "session-a".to_owned(), + candidate: "candidate-json".to_owned(), + ..Default::default() + }; + + assert!(Client::is_expected_webrtc_ice_candidate(&ice, "session-a")); + assert!(!Client::is_expected_webrtc_ice_candidate(&ice, "session-b")); + } + #[tokio::test] async fn test_query_onlines() { super::query_online_states( diff --git a/src/core_main.rs b/src/core_main.rs index 4515faa6b..a0ca5eb95 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -199,20 +199,6 @@ pub fn core_main() -> Option> { } std::thread::spawn(move || crate::start_server(false, no_server)); } else { - #[cfg(any(target_os = "linux", target_os = "macos"))] - // Root CLI management commands must talk to the user `--server` main IPC. - // Example: `sudo rustdesk --option custom-rendezvous-server` should query the - // user's IPC instead of root's `/tmp/-0/ipc`; `connect()` still limits this - // routing to empty-postfix main IPC only. - let _user_main_ipc_scope = if crate::platform::is_installed() - && is_root() - && is_user_main_ipc_scope_cli_command(&args) - { - Some(crate::ipc::UserMainIpcScope::new()) - } else { - None - }; - #[cfg(windows)] { use crate::platform; @@ -952,57 +938,6 @@ fn is_root() -> bool { crate::platform::is_root() } -#[cfg(any(target_os = "linux", target_os = "macos", test))] -fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool { - matches!( - args.first().map(String::as_str), - Some("--password") - | Some("--set-unlock-pin") - | Some("--get-id") - | Some("--set-id") - | Some("--config") - | Some("--option") - | Some("--assign") - | Some("--deploy") - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn args(values: &[&str]) -> Vec { - values.iter().map(|value| value.to_string()).collect() - } - - #[test] - fn user_main_ipc_scope_cli_command_matches_management_commands_only() { - for command in [ - "--password", - "--set-unlock-pin", - "--get-id", - "--set-id", - "--config", - "--option", - "--assign", - "--deploy", - ] { - assert!(is_user_main_ipc_scope_cli_command(&args(&[command]))); - } - - for command in [ - "--service", - "--server", - "--tray", - "--cm", - "--check-hwcodec-config", - "--connect", - ] { - assert!(!is_user_main_ipc_scope_cli_command(&args(&[command]))); - } - } -} - /// Check if the executable is a Quick Support version. /// Note: This function must be kept in sync with `libs/portable/src/main.rs`. #[cfg(windows)] diff --git a/src/ipc.rs b/src/ipc.rs index ffe1b08a5..0cd30634a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -33,25 +33,25 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use ipc_auth::authorize_service_scoped_ipc_connection; #[cfg(windows)] pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection; #[cfg(windows)] pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt; #[cfg(windows)] pub(crate) use ipc_auth::log_rejected_windows_ipc_connection; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection}; +#[cfg(target_os = "linux")] +pub(crate) use ipc_auth::{ + active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid, + log_rejected_uinput_connection, peer_uid_from_fd, +}; #[cfg(windows)] use ipc_auth::{ authorize_windows_main_ipc_connection, portable_service_listener_security_attributes, should_allow_everyone_create_on_windows, }; #[cfg(target_os = "linux")] -pub(crate) use ipc_auth::{ - ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid, - log_rejected_uinput_connection, peer_uid_from_fd, -}; -#[cfg(target_os = "linux")] use ipc_fs::terminal_count_candidate_uids; #[cfg(any(target_os = "linux", target_os = "macos"))] use ipc_fs::{ @@ -63,8 +63,6 @@ use parity_tokio_ipc::{ }; use serde_derive::{Deserialize, Serialize}; #[cfg(any(target_os = "linux", target_os = "macos"))] -use std::cell::Cell; -#[cfg(any(target_os = "linux", target_os = "macos"))] use std::os::unix::fs::PermissionsExt; use std::{ collections::HashMap, @@ -73,47 +71,12 @@ use std::{ // IPC actions here. pub const IPC_ACTION_CLOSE: &str = "close"; -#[cfg(target_os = "windows")] const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000; -#[cfg(target_os = "windows")] pub(crate) const IPC_TOKEN_LEN: usize = 64; -#[cfg(target_os = "windows")] const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2; -#[cfg(target_os = "windows")] const _: () = assert!(IPC_TOKEN_LEN % 2 == 0); pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); -#[cfg(any(target_os = "linux", target_os = "macos"))] -thread_local! { - static USE_USER_MAIN_IPC: Cell = Cell::new(false); -} - -#[must_use = "bind this guard to a local variable to keep the IPC scope active"] -/// Thread-local guard for routing root main IPC to the active user on Linux/macOS. -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub(crate) struct UserMainIpcScope { - previous: bool, -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl UserMainIpcScope { - pub(crate) fn new() -> Self { - let previous = USE_USER_MAIN_IPC.with(|use_user_main| { - let previous = use_user_main.get(); - use_user_main.set(true); - previous - }); - Self { previous } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl Drop for UserMainIpcScope { - fn drop(&mut self) { - USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous)); - } -} - #[inline] pub async fn connect_service(ms_timeout: u64) -> ResultType> { connect(ms_timeout, crate::POSTFIX_SERVICE).await @@ -1149,7 +1112,11 @@ async fn handle(data: Data, stream: &mut Connection) { }; } -#[cfg(target_os = "windows")] +pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { + let path = Config::ipc_path(postfix); + connect_with_path(ms_timeout, &path).await +} + pub(crate) fn generate_one_time_ipc_token() -> ResultType { use hbb_common::rand::{rngs::OsRng, RngCore as _}; use std::fmt::Write as _; @@ -1170,7 +1137,6 @@ pub(crate) fn generate_one_time_ipc_token() -> ResultType { Ok(token) } -#[cfg(target_os = "windows")] pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool { if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN { return false; @@ -1183,7 +1149,6 @@ pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> boo == 0 } -#[cfg(target_os = "windows")] pub(crate) async fn portable_service_ipc_handshake_as_client( stream: &mut ConnectionTmpl, token: &str, @@ -1208,7 +1173,6 @@ where } } -#[cfg(target_os = "windows")] pub(crate) async fn portable_service_ipc_handshake_as_server( stream: &mut ConnectionTmpl, mut validate_token: F, @@ -1245,103 +1209,6 @@ async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType, - prefer_root: bool, -) -> ResultType { - let mut server_uids = server_uids.to_vec(); - server_uids.sort_unstable(); - server_uids.dedup(); - - match server_uids.as_slice() { - [] => { - if let Some(uid) = active_uid { - // If no `--server` processes are found but the active user is identifiable, - // try the active user anyway because the main process may also listen on "" IPC. - return Ok(uid); - } else { - bail!("No --server process found for user main IPC") - } - } - [uid] => return Ok(*uid), - _ => {} - } - - if prefer_root && server_uids.contains(&0) { - return Ok(0); - } - if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) { - return Ok(active_uid); - } - bail!("Multiple --server processes found for user main IPC"); -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn running_server_uids_for_current_exe() -> ResultType> { - let current_exe = std::env::current_exe()?; - let current_exe_path = std::fs::canonicalize(¤t_exe)?; - let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id()); - let mut sys = hbb_common::sysinfo::System::new(); - sys.refresh_processes(); - let mut server_uids = Vec::new(); - for process in sys.processes().values() { - if process.pid() == current_pid { - continue; - } - if process.cmd().get(1).map_or(true, |arg| arg != "--server") { - continue; - } - let Ok(process_path) = std::fs::canonicalize(process.exe()) else { - continue; - }; - if process_path != current_exe_path { - continue; - } - let Some(uid) = process.user_id().map(|uid| **uid as u32) else { - // Root CLI management commands need a stable matching `--server` target. - // If this key process races during enumeration, failing the command is clearer - // than silently skipping it; `--server` is not expected to exit frequently. - bail!("Failed to read --server process uid"); - }; - server_uids.push(uid); - } - Ok(server_uids) -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn user_main_ipc_server_uid() -> ResultType { - let server_uids = running_server_uids_for_current_exe()?; - #[cfg(target_os = "linux")] - let prefer_root = crate::platform::linux::is_login_screen_wayland(); - #[cfg(target_os = "macos")] - let prefer_root = false; - select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root) -} - -pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get()); - let is_root_main_ipc = - unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc; - if is_root_main_ipc { - let uid = user_main_ipc_server_uid()?; - let path = Config::ipc_path_for_uid(uid, postfix); - return connect_with_path(ms_timeout, &path).await; - } - let path = Config::ipc_path(postfix); - return connect_with_path(ms_timeout, &path).await; - } - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - let path = Config::ipc_path(postfix); - connect_with_path(ms_timeout, &path).await - } -} - #[cfg(target_os = "linux")] pub async fn connect_for_uid( ms_timeout: u64, @@ -2135,16 +2002,7 @@ mod test { assert!(std::mem::size_of::() <= 120); } - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_service_ipc_path_is_shared_across_uids() { - assert_eq!( - Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE), - Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE) - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "linux")] #[test] fn test_ipc_path_differs_by_uid_for_cm() { let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; @@ -2163,46 +2021,4 @@ mod test { Config::ipc_path_for_uid(other_uid, postfix) ); } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_uses_active_uid_when_no_server_found() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(), - 501 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_uses_single_server_uid() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(), - 501 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_prefers_active_uid_with_multiple_servers() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(), - 501 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_prefers_root_on_wayland_login_screen() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(), - 0 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() { - assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err()); - } } diff --git a/src/ipc/auth.rs b/src/ipc/auth.rs index 77fd148c6..746a32eed 100644 --- a/src/ipc/auth.rs +++ b/src/ipc/auth.rs @@ -607,30 +607,27 @@ pub(crate) fn log_rejected_windows_ipc_connection( peer_session_id: Option, expected_session_id: Option, peer_is_system: Option, - peer_is_elevated: Option, ) { static LOG_THROTTLE: OnceLock> = OnceLock::new(); throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { if suppressed > 0 { log::warn!( - "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)", + "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?} (suppressed {} similar events)", postfix, peer_pid, peer_session_id, expected_session_id, peer_is_system, - peer_is_elevated, suppressed ); } else { log::warn!( - "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}", + "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}", postfix, peer_pid, peer_session_id, expected_session_id, - peer_is_system, - peer_is_elevated + peer_is_system ); } }); @@ -658,14 +655,8 @@ pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postf #[cfg(windows)] pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool { - let ( - authorized, - peer_pid, - peer_session_id, - server_session_id, - peer_is_system, - peer_is_elevated, - ) = stream.server_authorization_status(); + let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) = + stream.server_authorization_status(); if !authorized { log_rejected_windows_ipc_connection( postfix, @@ -673,7 +664,6 @@ pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix peer_session_id, server_session_id, peer_is_system, - peer_is_elevated, ); return false; } @@ -786,14 +776,7 @@ impl ConnectionTmpl { fn server_authorization_status( &self, - ) -> ( - bool, - Option, - Option, - Option, - Option, - Option, - ) { + ) -> (bool, Option, Option, Option, Option) { let peer_pid = self.peer_pid(); let server_session_id = crate::platform::windows::get_current_process_session_id(); let peer_session_id = @@ -803,34 +786,20 @@ impl ConnectionTmpl { let peer_is_system = peer_is_system_result .as_ref() .and_then(|r| r.as_ref().ok().copied()); - let session_authorized = is_allowed_windows_session_scoped_peer( - peer_is_system.unwrap_or(false), - peer_session_id, - server_session_id, - ); - let peer_is_elevated_result = if session_authorized { - None - } else { - peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid))) - }; - let peer_is_elevated = peer_is_elevated_result - .as_ref() - .and_then(|r| r.as_ref().ok().copied()); - if server_session_id.is_none() - && !peer_is_system.unwrap_or(false) - && !peer_is_elevated.unwrap_or(false) - { + if server_session_id.is_none() && !peer_is_system.unwrap_or(false) { // When the server session id cannot be determined, the session-id allow-path is - // disabled and only privileged peers can be authorized. + // disabled and only SYSTEM peers can be authorized. log::debug!( - "IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}", + "IPC authorization: server session id unavailable; rejecting non-SYSTEM peer, peer_pid={:?}, peer_session_id={:?}", peer_pid, peer_session_id ); } - // Main IPC trusts same-session peers, LocalSystem, and elevated administrators. - // Service-scoped IPC channels keep their own stricter authorization paths. - let authorized = session_authorized || peer_is_elevated.unwrap_or(false); + let authorized = is_allowed_windows_session_scoped_peer( + peer_is_system.unwrap_or(false), + peer_session_id, + server_session_id, + ); if !authorized { if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { log::debug!( @@ -839,13 +808,6 @@ impl ConnectionTmpl { err ); } - if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) { - log::debug!( - "Failed to determine whether peer process is elevated, pid={}, err={}", - pid, - err - ); - } } ( authorized, @@ -853,7 +815,6 @@ impl ConnectionTmpl { peer_session_id, server_session_id, peer_is_system, - peer_is_elevated, ) } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index e13404802..4113c1391 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "كلمة المرور مخفية"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 9f6b69c8b..1a3260c5a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 0aa61b1eb..17a89ce07 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 2f706cc89..799ca951f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a90e5e194..1ff10c49d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "永久密码已设置(已隐藏)"), ("preset-password-in-use-tip", "当前使用预设密码"), ("Enable privacy mode", "允许隐私模式"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7f50d826f..2b9c6219e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c9d3b4eb0..7410124df 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e6233e91e..030bc626d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), ("Enable privacy mode", "Datenschutzmodus aktivieren"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index d03bb069c..0633889a7 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 595169b8a..73974a2e5 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -274,6 +274,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ("password-hidden-tip", "Permanent password is set (hidden)."), ("preset-password-in-use-tip", "Preset password is currently in use."), - ("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 131a85fbf..16d43c9b4 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5e73b58a8..b822432a0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."), ("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."), ("Enable privacy mode", "Habilitar modo privado"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 76abc8563..a00c312b8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 9e19d1fea..aaf8a8be8 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9e01b7eb0..d34e4239e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index f8283685b..1bddd39d1 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f21d9b0df..6f7bb2880 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), ("Enable privacy mode", "Activer le mode de confidentialité"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 2fc8f282d..fba2fd83d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index ac0a588a8..8b8568c85 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "એક્સેસિબલ ઉપકરણો"), ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), - ("Use D3D rendering", ""), ("Printer", "પ્રિન્ટર"), ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), @@ -744,6 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 44b940784..682ee0c46 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index 904d43118..d35095fd1 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "सुलभ डिवाइस"), ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), - ("Use D3D rendering", ""), ("Printer", "प्रिंटर"), ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), @@ -743,7 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "प्रदर्शित नाम"), ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 0593ff6b7..505b01df9 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3eb16890f..b4cbc1f23 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), ("Enable privacy mode", "Adatvédelmi mód aktiválása"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index bcda0a3a8..bbd95e79a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a5132e027..479551fcc 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), ("Enable privacy mode", "Abilita modalità privacy"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2879e86bf..b55a6664f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 350d570b0..de68574e1 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), ("Enable privacy mode", "개인정보 보호 모드 사용함"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 4476fadc7..a2a1624f7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 47ace51ae..82422c30a 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 4f8e1f59f..906d056bd 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index 4dcfe9e74..099f1d385 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), - ("Use D3D rendering", ""), ("Printer", "പ്രിന്റർ"), ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), @@ -743,7 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "ഡിസ്‌പ്ലേ പേര്"), ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 9325dfa1f..5795b9eeb 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 55d272666..0f91d6a61 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), ("Enable privacy mode", "Privacymodus inschakelen"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fdf4ae8c5..972afc170 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4138b46e4..899c8da71 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 1428a71d0..4eb2c1544 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -740,10 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("Continue with {}", "Continuar com {}"), - ("Display Name", "Nome de Exibição"), - ("password-hidden-tip", "A senha permanente está definida como (oculta)."), - ("preset-password-in-use-tip", "A senha predefinida está sendo usada."), - ("Enable privacy mode", "Habilitar modo de privacidade"), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index bde4a4201..45b22684e 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -540,7 +540,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), ("Check for software update on startup", "Verifică actualizări la pornire"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), + ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), ("Filter by intersection", "Filtrează prin intersecție"), ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 2605582f4..3917c6fa2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), ("Enable privacy mode", "Использовать режим конфиденциальности"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 06919b752..68ce541f2 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 963f48728..6b4e16688 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0f85af0c3..3f35dea88 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 7c965cd45..f7f6c16d4 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index fc33e4671..bedbe4856 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 664dc4745..eda7851c1 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 93aeb6462..6e5652560 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 33b359c5e..5e25801d2 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a24c60bf6..c2d058c98 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index c28086cc9..d93ad4f68 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Parola gizli"), ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), ("Enable privacy mode", "Gizlilik modunu etkinleştir"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6df025303..b23b84949 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("preset-password-in-use-tip", "目前正在使用預設密碼"), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 7107bc261..3e1c4f25e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 0910025ed..3fadb0efc 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/main.rs b/src/main.rs index 9bc90a8fa..3d5237ab5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,49 +38,68 @@ fn main() { if !common::global_init() { return; } - use clap::App; + use clap::{Arg, ArgAction, Command}; use hbb_common::log; - let args = format!( - "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' - -c, --connect=[REMOTE_ID] 'test only' - -k, --key=[KEY] '' - -s, --server=[] 'Start server'", - ); - let matches = App::new("rustdesk") + let matches = Command::new("rustdesk") .version(crate::VERSION) .author("Purslane Ltd") .about("RustDesk command line tool") - .args_from_usage(&args) + .arg( + Arg::new("port-forward") + .short('p') + .long("port-forward") + .value_name("PORT-FORWARD-OPTIONS") + .help("Format: remote-id:local-port:remote-port[:remote-host]"), + ) + .arg( + Arg::new("connect") + .short('c') + .long("connect") + .value_name("REMOTE_ID") + .help("test only"), + ) + .arg(Arg::new("key").short('k').long("key").value_name("KEY")) + .arg( + Arg::new("server") + .short('s') + .long("server") + .action(ArgAction::SetTrue) + .help("Start server"), + ) .get_matches(); use hbb_common::{config::LocalConfig, env_logger::*}; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); - if let Some(p) = matches.value_of("port-forward") { - let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); + if let Some(p) = matches.get_one::("port-forward") { + let options: Vec = p.split(':').map(|x| x.to_owned()).collect(); if options.len() < 3 { log::error!("Wrong port-forward options"); return; } - let mut port = 0; - if let Ok(v) = options[1].parse::() { - port = v; - } else { - log::error!("Wrong local-port"); - return; - } - let mut remote_port = 0; - if let Ok(v) = options[2].parse::() { - remote_port = v; - } else { - log::error!("Wrong remote-port"); - return; - } + let port = match options[1].parse::() { + Ok(v) => v, + Err(_) => { + log::error!("Wrong local-port"); + return; + } + }; + let remote_port = match options[2].parse::() { + Ok(v) => v, + Err(_) => { + log::error!("Wrong remote-port"); + return; + } + }; let mut remote_host = "localhost".to_owned(); if options.len() > 3 { remote_host = options[3].clone(); } common::test_rendezvous_server(); common::test_nat_type(); - let key = matches.value_of("key").unwrap_or("").to_owned(); + let key = matches + .get_one::("key") + .map(String::as_str) + .unwrap_or("") + .to_owned(); let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward( options[0].clone(), @@ -90,13 +109,17 @@ fn main() { key, token, ); - } else if let Some(p) = matches.value_of("connect") { + } else if let Some(p) = matches.get_one::("connect") { common::test_rendezvous_server(); common::test_nat_type(); - let key = matches.value_of("key").unwrap_or("").to_owned(); + let key = matches + .get_one::("key") + .map(String::as_str) + .unwrap_or("") + .to_owned(); let token = LocalConfig::get_option("access_token"); cli::connect_test(p, key, token); - } else if let Some(p) = matches.value_of("server") { + } else if matches.get_flag("server") { log::info!("id={}", hbb_common::config::Config::get_id()); crate::start_server(true, false); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 1dc4a788a..a755714f9 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -614,7 +614,6 @@ fn authorize_service_scoped_ipc_connection( peer_session_id, expected_active_session_id, peer_is_system, - None, ); return false; } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 89d7fa01e..41fc75b18 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, net::SocketAddr, sync::{ atomic::{AtomicBool, Ordering}, @@ -21,8 +22,13 @@ use hbb_common::{ rendezvous_proto::*, sleep, socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for}, - tokio::{self, select, sync::Mutex, time::interval}, + tokio::{ + self, select, + sync::{mpsc, Mutex}, + time::interval, + }, udp::FramedSocket, + webrtc::WebRTCStream, AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr, }; @@ -32,11 +38,13 @@ use crate::{ }; type Message = RendezvousMessage; +type RendezvousSender = mpsc::UnboundedSender; lazy_static::lazy_static! { static ref SOLVING_PK_MISMATCH: Mutex = Default::default(); static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); + static ref WEBRTC_ICE_TXS: Mutex>> = Default::default(); } static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); @@ -72,6 +80,7 @@ pub struct RendezvousMediator { host: String, host_prefix: String, keep_alive: i32, + rz_sender: RendezvousSender, } impl RendezvousMediator { @@ -182,11 +191,13 @@ impl RendezvousMediator { let host = check_port(&host, RENDEZVOUS_PORT); log::info!("start udp: {host}"); let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?; + let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::(); let mut rz = Self { addr: addr.clone(), host: host.clone(), host_prefix: Self::get_host_prefix(&host), keep_alive: crate::DEFAULT_KEEP_ALIVE, + rz_sender, }; let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); @@ -246,6 +257,9 @@ impl RendezvousMediator { }, } }, + Some(msg_out) = rz_out_rx.recv() => { + Sink::Framed(&mut socket, &addr).send(&msg_out).await?; + }, _ = timer.tick() => { if SHOULD_EXIT.load(Ordering::SeqCst) { break; @@ -367,6 +381,17 @@ impl RendezvousMediator { allow_err!(rz.handle_intranet(fla, server).await); }); } + Some(rendezvous_message::Union::IceCandidate(ice)) => { + let tx = WEBRTC_ICE_TXS.lock().await.get(&ice.session_key).cloned(); + if let Some(tx) = tx { + let _ = tx.send(ice.candidate); + } else { + log::debug!( + "dropping ICE candidate for unknown WebRTC session key {}", + ice.session_key + ); + } + } Some(rendezvous_message::Union::ConfigureUpdate(cu)) => { let v0 = Config::get_rendezvous_servers(); Config::set_option( @@ -389,11 +414,13 @@ impl RendezvousMediator { let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?; let key = crate::get_key(true).await; crate::secure_tcp(&mut conn, &key).await?; + let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::(); let mut rz = Self { addr: conn.local_addr().into_target_addr()?, host: host.clone(), host_prefix: Self::get_host_prefix(&host), keep_alive: crate::DEFAULT_KEEP_ALIVE, + rz_sender, }; let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); let mut last_register_sent: Option = None; @@ -421,6 +448,9 @@ impl RendezvousMediator { let msg = Message::parse_from_bytes(&bytes)?; rz.handle_resp(msg.union, Sink::Stream(&mut conn), &server, &mut update_latency).await? } + Some(msg_out) = rz_out_rx.recv() => { + Sink::Stream(&mut conn).send(&msg_out).await?; + } _ = timer.tick() => { if SHOULD_EXIT.load(Ordering::SeqCst) { break; @@ -472,6 +502,7 @@ impl RendezvousMediator { rr.secure, false, Default::default(), + String::new(), rr.control_permissions.clone().into_option(), ) .await @@ -486,6 +517,7 @@ impl RendezvousMediator { secure: bool, initiate: bool, socket_addr_v6: bytes::Bytes, + webrtc_sdp_answer: String, control_permissions: Option, ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); @@ -504,6 +536,7 @@ impl RendezvousMediator { socket_addr: socket_addr.into(), version: crate::VERSION.to_owned(), socket_addr_v6, + webrtc_sdp_answer, ..Default::default() }; if initiate { @@ -571,6 +604,7 @@ impl RendezvousMediator { true, true, socket_addr_v6, + String::new(), fla.control_permissions.into_option(), ) .await @@ -613,6 +647,81 @@ impl RendezvousMediator { Ok(()) } + async fn spawn_webrtc_answerer( + &self, + ph: &PunchHole, + force_relay: bool, + server: ServerPtr, + peer_addr: SocketAddr, + control_permissions: Option, + ) -> ResultType { + let mut stream = + WebRTCStream::new(&ph.webrtc_sdp_offer, force_relay, CONNECT_TIMEOUT).await?; + let answer = stream.get_local_endpoint().await?; + let session_key = stream.session_key().to_owned(); + let return_route = ph.socket_addr.clone(); + + let (remote_ice_tx, mut remote_ice_rx) = mpsc::unbounded_channel::(); + WEBRTC_ICE_TXS + .lock() + .await + .insert(session_key.clone(), remote_ice_tx); + + let stream_for_remote_ice = stream.clone(); + tokio::spawn(async move { + while let Some(candidate) = remote_ice_rx.recv().await { + if let Err(err) = stream_for_remote_ice.add_remote_ice_candidate(&candidate).await + { + log::warn!("failed to add remote WebRTC ICE candidate: {}", err); + } + } + }); + + if let Some(mut local_ice_rx) = stream.take_local_ice_rx() { + let sender = self.rz_sender.clone(); + let socket_addr = return_route.clone(); + let session_key_for_ice = session_key.clone(); + tokio::spawn(async move { + while let Some(candidate) = local_ice_rx.recv().await { + let mut msg = Message::new(); + msg.set_ice_candidate(IceCandidate { + socket_addr: socket_addr.clone(), + session_key: session_key_for_ice.clone(), + candidate, + ..Default::default() + }); + let _ = sender.send(msg); + } + }); + } + + let session_key_for_cleanup = session_key.clone(); + tokio::spawn(async move { + let result = stream.wait_connected(CONNECT_TIMEOUT).await; + WEBRTC_ICE_TXS + .lock() + .await + .remove(&session_key_for_cleanup); + if let Err(err) = result { + log::warn!("webrtc wait_connected failed: {}", err); + return; + } + if let Err(err) = crate::server::create_tcp_connection( + server, + Stream::WebRTC(stream), + peer_addr, + true, + control_permissions, + ) + .await + { + log::warn!("failed to create WebRTC server connection: {}", err); + } + }); + + Ok(answer) + } + async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> { let mut peer_addr = AddrMangle::decode(&ph.socket_addr); let last = *LAST_MSG.lock().await; @@ -624,7 +733,23 @@ impl RendezvousMediator { let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); let relay = use_ws() || Config::is_proxy() || ph.force_relay; let mut socket_addr_v6 = Default::default(); - let control_permissions = ph.control_permissions.into_option(); + let control_permissions = ph.control_permissions.clone().into_option(); + let webrtc_sdp_answer = if !ph.webrtc_sdp_offer.is_empty() { + self.spawn_webrtc_answerer( + &ph, + relay, + server.clone(), + peer_addr, + control_permissions.clone(), + ) + .await + .unwrap_or_else(|err| { + log::warn!("failed to create WebRTC answer: {}", err); + String::new() + }) + } else { + String::new() + }; if peer_addr_v6.port() > 0 && !relay { socket_addr_v6 = start_ipv6( peer_addr_v6, @@ -651,6 +776,7 @@ impl RendezvousMediator { true, true, socket_addr_v6.clone(), + webrtc_sdp_answer.clone(), control_permissions, ) .await; @@ -664,6 +790,7 @@ impl RendezvousMediator { nat_type: nat_type.into(), version: crate::VERSION.to_owned(), socket_addr_v6, + webrtc_sdp_answer, ..Default::default() }; if ph.udp_port > 0 {