diff --git a/Cargo.lock b/Cargo.lock index fe1f67cc0..febfd6b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5996,8 +5996,8 @@ dependencies = [ [[package]] name = "parity-tokio-ipc" -version = "0.7.3-6" -source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01" +version = "0.7.3-5" +source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291" dependencies = [ "futures", "libc", diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 537014246..2e7247d95 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -16,12 +16,6 @@ import 'package:get/get.dart'; bool isEditOsPassword = false; -// macOS privacy mode blacks out all online displays, so switching the remote -// display does not weaken the local privacy protection. -bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) { - return pi.platform == kPeerPlatformMacOS; -} - class TTextMenu { final Widget child; final VoidCallback? onPressed; @@ -690,9 +684,8 @@ Future> toolbarDisplayToggle( child: Text(translate('Lock after session end')))); } - final privacyModeState = PrivacyModeState.find(id); if (pi.isSupportMultiDisplay && - (privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) && + PrivacyModeState.find(id).isEmpty && pi.displaysCount.value > 1 && bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { final value = @@ -783,8 +776,7 @@ List toolbarPrivacyMode( onChanged: enabled ? (value) { if (value == null) return; - if (!allowDisplaySwitchInPrivacyMode(pi) && - ffiModel.pi.currentDisplay != 0 && + if (ffiModel.pi.currentDisplay != 0 && ffiModel.pi.currentDisplay != kAllDisplayValue) { msgBox( sessionId, 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..5da253e80 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) { @@ -810,13 +376,11 @@ class _RemoteToolbarState extends State { } toolbarItems.add(Obx(() { - if ((PrivacyModeState.find(widget.id).isEmpty || - allowDisplaySwitchInPrivacyMode(pi)) && + if (PrivacyModeState.find(widget.id).isEmpty && pi.displaysCount.value > 1) { return _MonitorMenu( id: widget.id, ffi: widget.ffi, - edge: edge, setRemoteState: widget.setRemoteState); } else { return Offstage(); @@ -842,53 +406,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 +515,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 +530,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 +664,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 +707,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 +2518,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 +2530,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 +2543,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 +2572,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 +2636,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 +2677,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 +2719,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/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9ef7d38d9..ef195b493 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -593,13 +593,13 @@ class _DesktopTabState extends State } Widget _buildBar() { - final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage(); return Row( children: [ Expanded( child: GestureDetector( // custom double tap handler - onTap: !isIncomingHomePage && showMaximize + onTap: !(bind.isIncomingOnly() && isInHomePage()) && + showMaximize ? () { final current = DateTime.now().millisecondsSinceEpoch; final elapsed = current - _lastClickTime; @@ -610,7 +610,7 @@ class _DesktopTabState extends State .then((value) => stateGlobal.setMaximized(value)); } } - : (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch. + : null, onPanStart: (_) => startDragging(isMainWindow), onPanCancel: () { // We want to disable dragging of the tab area in the tab bar. diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 7d91b03b3..35001cbf2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -391,30 +391,14 @@ class FileController { await Future.delayed(Duration(milliseconds: 100)); - final savedDir = (await bind.sessionGetPeerOption( + final dir = (await bind.sessionGetPeerOption( sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir")); - Future tryOpenReadyDirs() async { - final dirs = { - if (directory.value.path.isNotEmpty) directory.value.path, - if (savedDir.isNotEmpty) savedDir, - options.value.home, - }; - for (final dir in dirs) { - if (await _openDirectoryPath(dir, isBack: true)) { - return true; - } - } - return false; - } - - var opened = await tryOpenReadyDirs(); + openDirectory(dir.isEmpty ? options.value.home : dir); await Future.delayed(Duration(seconds: 1)); - if (!opened) { - // The peer may become ready during the reconnect delay, so retry the - // same candidates instead of only retrying the default home directory. - await tryOpenReadyDirs(); + if (directory.value.path.isEmpty) { + openDirectory(options.value.home); } } @@ -445,23 +429,19 @@ class FileController { }); } - Future refresh() async { - // "." can be both a refresh command and a real remote directory path. - // Refresh must bypass openDirectory's command dispatch to avoid recursion. - return await _openDirectoryPath(directory.value.path, isBack: true); + Future refresh() async { + await openDirectory(directory.value.path); } - Future openDirectory(String path, {bool isBack = false}) async { - if (!isBack && path == ".") { - return await refresh(); + Future openDirectory(String path, {bool isBack = false}) async { + if (path == ".") { + refresh(); + return; } - if (!isBack && path == "..") { - return await _goToParentDirectory(isBack: isBack); + if (path == "..") { + goToParentDirectory(); + return; } - return await _openDirectoryPath(path, isBack: isBack); - } - - Future _openDirectoryPath(String path, {bool isBack = false}) async { if (!isBack) { pushHistory(); } @@ -478,10 +458,8 @@ class FileController { final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden); fd.format(isWindows, sort: sortBy.value); directory.value = fd; - return true; } catch (e) { debugPrint("Failed to openDirectory $path: $e"); - return false; } } @@ -509,22 +487,19 @@ class FileController { goBack(); return; } - unawaited(_openDirectoryPath(path, isBack: true).then((_) {})); + openDirectory(path, isBack: true); } void goToParentDirectory() { - unawaited(_goToParentDirectory().then((_) {})); - } - - Future _goToParentDirectory({bool isBack = false}) async { final isWindows = options.value.isWindows; final dirPath = directory.value.path; var parent = PathUtil.dirname(dirPath, isWindows); // specially for C:\, D:\, goto '/' if (parent == dirPath && isWindows) { - return await _openDirectoryPath('/', isBack: isBack); + openDirectory('/'); + return; } - return await _openDirectoryPath(parent, isBack: isBack); + openDirectory(parent); } // TODO deprecated this 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..42af0f0ae 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0 +Subproject commit 42af0f0aed0bb5fd5df4ff95fd4cc9816fcf5769 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/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index f4780dd87..0107929f3 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -31,17 +31,17 @@ LExit: return WcaFinalize(er); } -// Helper function to safely delete a file using handle-based deletion. -// Directories are refused after opening the handle. +// Helper function to safely delete a file or directory using handle-based deletion. +// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions. BOOL SafeDeleteItem(LPCWSTR fullPath) { - // Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT + // Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT // to prevent following symlinks. // Use shared access to allow deletion even when other processes have the file open. DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT; HANDLE hFile = CreateFileW( fullPath, - DELETE | FILE_READ_ATTRIBUTES, + DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access NULL, OPEN_EXISTING, @@ -55,21 +55,6 @@ BOOL SafeDeleteItem(LPCWSTR fullPath) return FALSE; } - BY_HANDLE_FILE_INFORMATION fileInfo; - if (FALSE == GetFileInformationByHandle(hFile, &fileInfo)) - { - WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError()); - CloseHandle(hFile); - return FALSE; - } - - if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath); - CloseHandle(hFile); - return FALSE; - } - // Use SetFileInformationByHandle to mark for deletion. // The file will be deleted when the handle is closed. FILE_DISPOSITION_INFO dispInfo; @@ -92,74 +77,98 @@ BOOL SafeDeleteItem(LPCWSTR fullPath) return result; } -BOOL PathEndsWithSlash(LPCWSTR path) +// Helper function to recursively delete a directory's contents with detailed logging. +void RecursiveDelete(LPCWSTR path) { - size_t length = 0; - HRESULT hr = StringCchLengthW(path, MAX_PATH, &length); - if (FAILED(hr) || length == 0) - { - return FALSE; - } - - WCHAR last = path[length - 1]; - return last == L'\\' || last == L'/'; -} - -void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes) -{ - if (!(attributes & FILE_ATTRIBUTE_READONLY)) + // Ensure the path is not empty or null. + if (path == NULL || path[0] == L'\0') { return; } - DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY; - if (writableAttributes == 0) + // Extra safety: never operate directly on a root path. + if (PathIsRootW(path)) { - writableAttributes = FILE_ATTRIBUTE_NORMAL; - } - - if (SetFileAttributesW(fullPath, writableAttributes)) - { - WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath); + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path); return; } - WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError()); -} - -BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName) -{ - WCHAR fullPath[MAX_PATH]; - LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\"; - HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName); - if (FAILED(hr)) - { - WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName); - return FALSE; + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR searchPath[MAX_PATH]; + HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path); + return; } - DWORD attributes = GetFileAttributesW(fullPath); - if (attributes == INVALID_FILE_ATTRIBUTES) + WIN32_FIND_DATAW findData; + HANDLE hFind = FindFirstFileW(searchPath, &findData); + + if (hFind == INVALID_HANDLE_VALUE) { - DWORD error = GetLastError(); - if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) + // This can happen if the directory is empty or doesn't exist, which is not an error in our case. + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError()); + return; + } + + do + { + // Skip '.' and '..' directories. + if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0) { - return TRUE; + continue; } - WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error); - return FALSE; - } + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR fullPath[MAX_PATH]; + hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path); + continue; + } - if (attributes & FILE_ATTRIBUTE_DIRECTORY) + // Before acting, ensure the read-only attribute is not set. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) + { + if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError()); + } + } + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + // Check for reparse points (symlinks/junctions) to prevent directory traversal attacks. + // Do not follow reparse points, only remove the link itself. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath); + SafeDeleteItem(fullPath); + } + else + { + // Recursively delete directory contents first + RecursiveDelete(fullPath); + // Then delete the directory itself + SafeDeleteItem(fullPath); + } + } + else + { + // Delete file using safe handle-based deletion + SafeDeleteItem(fullPath); + } + } while (FindNextFileW(hFind, &findData) != 0); + + DWORD lastError = GetLastError(); + if (lastError != ERROR_NO_MORE_FILES) { - WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath); - return FALSE; + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError); } - ClearReadOnlyAttribute(fullPath, attributes); - WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath); - return SafeDeleteItem(fullPath); + FindClose(hFind); } // See `Package.wxs` for the sequence of this custom action. @@ -169,13 +178,13 @@ BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName) // 2. RemoveExistingProducts // ├─ TerminateProcesses // ├─ TryStopDeleteService -// ├─ RemoveRuntimeGeneratedFiles - <-- Here +// ├─ RemoveInstallFolder - <-- Here // └─ RemoveFiles // 3. InstallValidate // 4. InstallFiles // 5. InstallExecute // 6. InstallFinalize -UINT __stdcall RemoveRuntimeGeneratedFiles( +UINT __stdcall RemoveInstallFolder( __in MSIHANDLE hInstall) { HRESULT hr = S_OK; @@ -185,7 +194,7 @@ UINT __stdcall RemoveRuntimeGeneratedFiles( LPWSTR pwz = NULL; LPWSTR pwzData = NULL; - hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles"); + hr = WcaInitialize(hInstall, "RemoveInstallFolder"); ExitOnFailure(hr, "Failed to initialize"); hr = WcaGetProperty(L"CustomActionData", &pwzData); @@ -193,20 +202,24 @@ UINT __stdcall RemoveRuntimeGeneratedFiles( pwz = pwzData; hr = WcaReadStringFromCaData(&pwz, &installFolder); - ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); if (installFolder == NULL || installFolder[0] == L'\0') { - WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup."); + WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete."); goto LExit; } if (PathIsRootW(installFolder)) { - WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder); + WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder); goto LExit; } - WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder); - DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe"); + WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder); + + RecursiveDelete(installFolder); + + // The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories. + // We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer. LExit: ReleaseStr(pwzData); diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def index d50fbf59b..01b03490c 100644 --- a/res/msi/CustomActions/CustomActions.def +++ b/res/msi/CustomActions/CustomActions.def @@ -2,7 +2,7 @@ LIBRARY "CustomActions" EXPORTS CustomActionHello - RemoveRuntimeGeneratedFiles + RemoveInstallFolder TerminateProcesses AddFirewallRules SetPropertyIsServiceRunning diff --git a/res/msi/Package/Components/Folders.wxs b/res/msi/Package/Components/Folders.wxs index 6911600e9..de9edb7f3 100644 --- a/res/msi/Package/Components/Folders.wxs +++ b/res/msi/Package/Components/Folders.wxs @@ -16,15 +16,8 @@ - - - - - - - - - + + diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index 952172bdc..337e84ec3 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -12,7 +12,7 @@ - + @@ -77,21 +77,21 @@ - - - + + + - + - + - + - + diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs index 3a9811eb8..3727c0dd3 100644 --- a/res/msi/Package/Fragments/CustomActions.wxs +++ b/res/msi/Package/Fragments/CustomActions.wxs @@ -5,7 +5,7 @@ - + diff --git a/res/msi/Package/UI/MyInstallDlg.wxs b/res/msi/Package/UI/MyInstallDlg.wxs index 06c37097c..bf59d569c 100644 --- a/res/msi/Package/UI/MyInstallDlg.wxs +++ b/res/msi/Package/UI/MyInstallDlg.wxs @@ -23,13 +23,12 @@ Patch dialog sequence: --> - - + @@ -65,16 +64,9 @@ Patch dialog sequence: - - - - - - - - - - + + + diff --git a/src/core_main.rs b/src/core_main.rs index 4515faa6b..e27091927 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -146,13 +146,7 @@ pub fn core_main() -> Option> { crate::portable_service::client::set_quick_support(_is_quick_support); } let mut log_name = "".to_owned(); - // Keep portable-service logs under a stable directory name. - let has_portable_service_shmem_arg = args - .iter() - .any(|arg| arg.starts_with("--portable-service-shmem-name=")); - if has_portable_service_shmem_arg { - log_name = "portable-service".to_owned(); - } else if args.len() > 0 && args[0].starts_with("--") { + if args.len() > 0 && args[0].starts_with("--") { let name = args[0].replace("--", ""); if !name.is_empty() { log_name = name; @@ -199,20 +193,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; @@ -641,98 +621,6 @@ pub fn core_main() -> Option> { println!("Installation and administrative privileges required!"); } return None; - } else if args[0] == "--deploy" { - if config::Config::no_register_device() { - println!("Cannot deploy an unregistrable device!"); - } else if crate::platform::is_installed() && is_root() { - let max = args.len() - 1; - let pos = args.iter().position(|x| x == "--token").unwrap_or(max); - if pos >= max { - println!("--token is required!"); - return None; - } - let token = args[pos + 1].to_owned(); - let get_value = |c: &str| { - let pos = args.iter().position(|x| x == c).unwrap_or(max); - if pos < max { - Some(args[pos + 1].to_owned()) - } else { - None - } - }; - let new_id = get_value("--id"); - let local_id = crate::ipc::get_id(); - let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone()); - let uuid = crate::encode64(hbb_common::get_uuid()); - let pk = crate::encode64( - hbb_common::config::Config::get_key_pair().1, - ); - let body = serde_json::json!({ - "id": id_to_deploy, - "uuid": uuid, - "pk": pk, - }); - let header = "Authorization: Bearer ".to_owned() + &token; - let url = crate::ui_interface::get_api_server() + "/api/devices/deploy"; - match crate::post_request_sync(url, body.to_string(), &header) { - Err(err) => { - println!("Request failed: {}", err); - std::process::exit(1); - } - Ok(text) => { - let parsed: serde_json::Value = - serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); - let result = parsed["result"].as_str().unwrap_or(""); - match result { - "OK" => { - if let Some(ref new_id) = new_id { - if *new_id != local_id { - if let Err(err) = - crate::ipc::set_config("id", new_id.clone()) - { - println!( - "Failed to persist deployed id locally: {}", - err - ); - std::process::exit(1); - } - } - } - if let Err(err) = crate::ipc::notify_deployed() { - log::warn!("Failed to notify deployed state: {}", err); - } - println!("Device deployed."); - } - "NOT_ENABLED" => { - println!("Server does not require deployment."); - std::process::exit(3); - } - "INVALID_INPUT" => { - println!("Invalid input."); - std::process::exit(5); - } - "ID_TAKEN" => { - println!( - "Id `{}` is already used by another machine on the server.", - id_to_deploy - ); - std::process::exit(6); - } - _ => { - if text.is_empty() { - println!("Unknown response."); - } else { - println!("{}", text); - } - std::process::exit(1); - } - } - } - } - } else { - println!("Installation and administrative privileges required!"); - } - return None; } else if args[0] == "--check-hwcodec-config" { #[cfg(feature = "hwcodec")] crate::ipc::hwcodec_process(); @@ -952,57 +840,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..82b52a60c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,28 +1,33 @@ -#[path = "ipc/auth.rs"] -mod ipc_auth; -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[path = "ipc/fs.rs"] -mod ipc_fs; +use crate::{ + common::CheckTestNatType, + privacy_mode::PrivacyModeState, + ui_interface::{get_local_option, set_local_option}, +}; +use bytes::Bytes; +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::plugin::ipc::Plugin; -use crate::{ - common::{is_server, CheckTestNatType}, - privacy_mode, - privacy_mode::PrivacyModeState, - rendezvous_mediator::RendezvousMediator, - ui_interface::{get_local_option, set_local_option}, -}; -use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipboardFile; -#[cfg(target_os = "linux")] -use hbb_common::anyhow; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, - config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, + config::{ + self, + keys::{self, OPTION_ALLOW_WEBSOCKET}, + Config, Config2, + }, futures::StreamExt as _, futures_util::sink::SinkExt, log, password_security as password, timeout, @@ -33,92 +38,13 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; -#[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(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::{ - check_pid, ensure_secure_ipc_parent_dir, scrub_secure_ipc_parent_dir, - should_scrub_parent_entries_after_check_pid, write_pid, -}; -use parity_tokio_ipc::{ - Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, -}; -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, - sync::atomic::{AtomicBool, Ordering}, -}; + +use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator}; // 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 -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { @@ -281,8 +207,6 @@ pub enum DataControl { pub enum DataPortableService { Ping, Pong, - AuthToken(String), - AuthResult(bool), ConnCount(Option), Mouse((Vec, i32, String, u32, bool, bool)), Pointer((Vec, i32)), @@ -349,7 +273,6 @@ pub enum Data { ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, - Deployed, #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -488,22 +411,6 @@ pub async fn start(postfix: &str) -> ResultType<()> { Ok(stream) => { let mut stream = Connection::new(stream); let postfix = postfix.to_owned(); - #[cfg(any(target_os = "linux", target_os = "macos"))] - if config::is_service_ipc_postfix(&postfix) { - if !authorize_service_scoped_ipc_connection(&stream, &postfix) { - continue; - } - } - #[cfg(windows)] - if postfix.is_empty() { - // Windows main IPC (`postfix == ""`) is authorized here. - // Other security-sensitive channels use dedicated authorization paths: - // - `_portable_service`: portable-service listener + handshake policy - // - service-scoped postfixes: service-specific listener/authorization - if !authorize_windows_main_ipc_connection(&stream, &postfix) { - continue; - } - } tokio::spawn(async move { loop { match stream.next().await { @@ -512,48 +419,9 @@ pub async fn start(postfix: &str) -> ResultType<()> { break; } Ok(Some(data)) => { - // On Linux/macOS, the protected `_service` channel is used only for - // syncing config between root service and the active user process. - // - // NOTE: `is_service_ipc_postfix()` also includes `_uinput_*`, but those - // channels are handled by the dedicated uinput listener/protocol in - // `src/server/uinput.rs` and therefore do not share this Data enum - // allowlist. The SyncConfig allowlist here is intentionally scoped to the - // `_service` channel only. - // - // Keep this explicit branch to avoid policy drift between `_service` and - // uinput IPC paths while still minimizing exposed message surface here. - #[cfg(any(target_os = "linux", target_os = "macos"))] - if postfix == crate::POSTFIX_SERVICE { - if matches!(&data, Data::SyncConfig(_)) { - handle(data, &mut stream).await; - } else { - log::warn!( - "Rejected non-sync data on protected _service IPC channel: postfix={}, data_kind={:?}, peer_uid={:?}", - postfix, - std::mem::discriminant(&data), - stream.peer_uid() - ); - // Close the connection to avoid keeping a protected channel - // alive while repeatedly receiving invalid traffic. - break; - } - continue; - } handle(data, &mut stream).await; } - Ok(None) => { - // `Ok(None)` means a complete frame arrived but did not - // deserialize into `Data`. Peer close/reset is returned as - // `Err` by `ConnectionTmpl::next()`. Keep the historical - // ignore behavior except on the protected `_service` channel. - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - if postfix == crate::POSTFIX_SERVICE { - break; - } - } - } + _ => {} } } }); @@ -568,77 +436,20 @@ pub async fn start(postfix: &str) -> ResultType<()> { pub async fn new_listener(postfix: &str) -> ResultType { let path = Config::ipc_path(postfix); - #[cfg(any(target_os = "linux", target_os = "macos"))] - let should_scrub_parent_entries = ensure_secure_ipc_parent_dir(&path, postfix)?; - #[cfg(any(target_os = "linux", target_os = "macos"))] - let existing_listener_alive = check_pid(postfix).await; - #[cfg(any(target_os = "linux", target_os = "macos"))] - if should_scrub_parent_entries_after_check_pid( - should_scrub_parent_entries, - existing_listener_alive, - ) { - scrub_secure_ipc_parent_dir(&path, postfix)?; - } + #[cfg(not(any(windows, target_os = "android", target_os = "ios")))] + check_pid(postfix).await; let mut endpoint = Endpoint::new(path.clone()); - let security_attrs = { - #[cfg(windows)] - { - if postfix == "_portable_service" { - portable_service_listener_security_attributes() - } else if should_allow_everyone_create_on_windows(postfix) { - SecurityAttributes::allow_everyone_create() - } else { - Ok(SecurityAttributes::empty()) - } - } - #[cfg(not(windows))] - { - SecurityAttributes::allow_everyone_create() - } - }; - match security_attrs { + match SecurityAttributes::allow_everyone_create() { Ok(attr) => endpoint.set_security_attributes(attr), - Err(err) => { - log::error!("Failed to set ipc{} security: {}", postfix, err); - #[cfg(windows)] - if postfix == "_portable_service" { - // Fail closed for `_portable_service` when SDDL construction fails. - // This endpoint is security-critical and must not start with default ACLs. - return Err(err.into()); - } - } + Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err), }; match endpoint.incoming() { Ok(incoming) => { - if postfix == crate::POSTFIX_SERVICE { - log::info!("Started protected ipc service server: postfix={}", postfix); - } else { - log::info!("Started ipc{} server at path: {}", postfix, &path); - } - #[cfg(any(target_os = "linux", target_os = "macos"))] + log::info!("Started ipc{} server at path: {}", postfix, &path); + #[cfg(not(windows))] { - // NOTE: On Linux/macOS, some IPC sockets are intentionally world-connectable - // (0666) so the active (non-root) user process can connect. Authorization is - // enforced at accept-time for these channels, and the protected `_service` - // channel is further restricted by an explicit message allowlist (SyncConfig - // only). - let socket_mode = if config::is_service_ipc_postfix(postfix) { - 0o0666 - } else { - 0o0600 - }; - if let Err(err) = - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(socket_mode)) - { - log::error!( - "Failed to set permissions on ipc{} socket at path {}: {}", - postfix, - &path, - err - ); - std::fs::remove_file(&path).ok(); - return Err(err.into()); - } + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); write_pid(postfix); } Ok(incoming) @@ -967,10 +778,6 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } - Data::Deployed => { - crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst); - crate::rendezvous_mediator::RendezvousMediator::restart(); - } #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::SwitchSidesRequest(id) => { @@ -1146,210 +953,13 @@ async fn handle(data: Data, stream: &mut Connection) { ); } _ => {} - }; -} - -#[cfg(target_os = "windows")] -pub(crate) fn generate_one_time_ipc_token() -> ResultType { - use hbb_common::rand::{rngs::OsRng, RngCore as _}; - use std::fmt::Write as _; - - let mut random_bytes = [0u8; IPC_TOKEN_RANDOM_BYTES]; - let mut rng = OsRng; - rng.try_fill_bytes(&mut random_bytes).map_err(|err| { - hbb_common::anyhow::anyhow!( - "failed to generate portable service ipc token from OsRng: {}", - err - ) - })?; - - let mut token = String::with_capacity(IPC_TOKEN_LEN); - for byte in random_bytes { - let _ = write!(token, "{:02x}", byte); } - 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; - } - expected - .as_bytes() - .iter() - .zip(candidate.as_bytes().iter()) - .fold(0u8, |diff, (left, right)| diff | (*left ^ *right)) - == 0 -} - -#[cfg(target_os = "windows")] -pub(crate) async fn portable_service_ipc_handshake_as_client( - stream: &mut ConnectionTmpl, - token: &str, -) -> ResultType<()> -where - T: AsyncRead + AsyncWrite + std::marker::Unpin, -{ - stream - .send(&Data::DataPortableService(DataPortableService::AuthToken( - token.to_owned(), - ))) - .await?; - match stream - .next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS) - .await? - { - Some(Data::DataPortableService(DataPortableService::AuthResult(true))) => Ok(()), - Some(Data::DataPortableService(DataPortableService::AuthResult(false))) => { - bail!("portable service ipc handshake was rejected by server") - } - Some(_) | None => bail!("portable service ipc handshake returned an unexpected response"), - } -} - -#[cfg(target_os = "windows")] -pub(crate) async fn portable_service_ipc_handshake_as_server( - stream: &mut ConnectionTmpl, - mut validate_token: F, -) -> ResultType<()> -where - T: AsyncRead + AsyncWrite + std::marker::Unpin, - // Token validators must use `constant_time_ipc_token_eq` or an equivalent - // fixed-length comparison; this handshake is part of the privilege boundary. - F: FnMut(&str) -> bool, -{ - let authorized = match stream - .next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS) - .await? - { - Some(Data::DataPortableService(DataPortableService::AuthToken(token))) => { - validate_token(&token) - } - Some(_) | None => false, - }; - stream - .send(&Data::DataPortableService(DataPortableService::AuthResult( - authorized, - ))) - .await?; - if !authorized { - bail!("portable service ipc handshake failed") - } - Ok(()) -} - -#[inline] -async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType> { - let client = timeout(ms_timeout, Endpoint::connect(path)).await??; - Ok(ConnectionTmpl::new(client)) -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -fn select_server_uid_for_user_main_ipc( - server_uids: &[u32], - active_uid: Option, - 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, - uid: u32, - postfix: &str, -) -> ResultType> { - let path = Config::ipc_path_for_uid(uid, postfix); - connect_with_path(ms_timeout, &path).await + let path = Config::ipc_path(postfix); + let client = timeout(ms_timeout, Endpoint::connect(&path)).await??; + Ok(ConnectionTmpl::new(client)) } #[cfg(target_os = "linux")] @@ -1429,6 +1039,54 @@ pub async fn start_pa() { } } +#[inline] +#[cfg(not(windows))] +fn get_pid_file(postfix: &str) -> String { + let path = Config::ipc_path(postfix); + format!("{}.pid", path) +} + +#[cfg(not(any(windows, target_os = "android", target_os = "ios")))] +async fn check_pid(postfix: &str) { + let pid_file = get_pid_file(postfix); + if let Ok(mut file) = File::open(&pid_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let pid = content.parse::().unwrap_or(0); + if pid > 0 { + use hbb_common::sysinfo::System; + let mut sys = System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(pid.into()) { + if let Some(current) = sys.process((std::process::id() as usize).into()) { + if current.name() == p.name() { + // double check with connect + if connect(1000, postfix).await.is_ok() { + return; + } + } + } + } + } + } + // if not remove old ipc file, the new ipc creation will fail + // if we remove a ipc file, but the old ipc process is still running, + // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive + std::fs::remove_file(&Config::ipc_path(postfix)).ok(); +} + +#[inline] +#[cfg(not(windows))] +fn write_pid(postfix: &str) { + let path = get_pid_file(postfix); + if let Ok(mut file) = File::create(&path) { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + file.write_all(&std::process::id().to_string().into_bytes()) + .ok(); + } +} + pub struct ConnectionTmpl { inner: Framed, } @@ -1875,13 +1533,6 @@ pub async fn test_rendezvous_server() -> ResultType<()> { Ok(()) } -#[tokio::main(flavor = "current_thread")] -pub async fn notify_deployed() -> ResultType<()> { - let mut c = connect(1000, "").await?; - c.send(&Data::Deployed).await?; - Ok(()) -} - #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { connect(1_000, "_url") @@ -1899,10 +1550,9 @@ pub fn close_all_instances() -> ResultType { } } -#[cfg(windows)] #[tokio::main(flavor = "current_thread")] pub async fn connect_to_user_session(usid: Option) -> ResultType<()> { - let mut stream = crate::ipc::connect_service(1000).await?; + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??; Ok(()) } @@ -2028,76 +1678,13 @@ pub async fn update_controlling_session_count(count: usize) -> ResultType<()> { #[cfg(target_os = "linux")] #[tokio::main(flavor = "current_thread")] pub async fn get_terminal_session_count() -> ResultType { - let timeout_ms = 1_000; - let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - let candidate_uids = terminal_count_candidate_uids(effective_uid); - let mut last_err: Option = None; - for candidate_uid in candidate_uids { - let socket_path = Config::ipc_path_for_uid(candidate_uid, ""); - let connect_result = timeout(timeout_ms, Endpoint::connect(&socket_path)) - .await - .map_err(|err| { - anyhow::anyhow!( - "Timeout connecting to terminal ipc at {}: {}", - socket_path, - err - ) - }); - let connection = match connect_result { - Ok(Ok(connection)) => connection, - Ok(Err(err)) => { - last_err = Some(anyhow::anyhow!( - "Failed to connect to terminal ipc at {}: {}", - socket_path, - err - )); - continue; - } - Err(err) => { - last_err = Some(err); - continue; - } - }; - let mut ipc_conn = ConnectionTmpl::new(connection); - if let Err(err) = ipc_conn.send(&Data::TerminalSessionCount(0)).await { - last_err = Some(anyhow::anyhow!( - "Failed to request terminal session count via ipc at {}: {}", - socket_path, - err - )); - continue; - } - match ipc_conn.next_timeout(timeout_ms).await { - Ok(Some(Data::TerminalSessionCount(session_count))) => { - return Ok(session_count); - } - Ok(None) => { - last_err = Some(anyhow::anyhow!( - "Invalid response when requesting terminal session count via ipc at {}", - socket_path - )); - } - Ok(other) => { - last_err = Some(anyhow::anyhow!( - "Unexpected response when requesting terminal session count via ipc at {}: {:?}", - socket_path, - other.map(|v| std::mem::discriminant(&v)) - )); - } - Err(err) => { - last_err = Some(anyhow::anyhow!( - "Failed to read terminal session count via ipc at {}: {}", - socket_path, - err - )); - } - } - } - if let Some(err) = last_err { - Err(err.into()) - } else { - Ok(0) + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::TerminalSessionCount(0)).await?; + if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? { + return Ok(c); } + Ok(0) } async fn handle_wayland_screencast_restore_token( @@ -2128,81 +1715,9 @@ pub async fn set_install_option(k: String, v: String) -> ResultType<()> { #[cfg(test)] mod test { use super::*; - #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); 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"))] - #[test] - fn test_ipc_path_differs_by_uid_for_cm() { - let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - let other_uid = effective_uid.saturating_add(1); - let postfix = "_cm"; - - // Default connect path targets the current effective uid. - assert_eq!( - Config::ipc_path(postfix), - Config::ipc_path_for_uid(effective_uid, postfix) - ); - // A different uid yields a different socket path - this is the root cause of the - // cross-user regression when root spawns a user process but still connects as uid 0. - assert_ne!( - Config::ipc_path(postfix), - 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 deleted file mode 100644 index 77fd148c6..000000000 --- a/src/ipc/auth.rs +++ /dev/null @@ -1,1075 +0,0 @@ -use crate::ipc::{Connection, ConnectionTmpl}; -#[cfg(all(windows, not(feature = "flutter")))] -use hbb_common::sha2::{Digest, Sha256}; -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -use hbb_common::{anyhow, bail, log, ResultType}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use hbb_common::{ - libc, - tokio::io::{AsyncRead, AsyncWrite}, -}; -#[cfg(windows)] -use parity_tokio_ipc::SecurityAttributes; -#[cfg(windows)] -use std::io; -#[cfg(all(windows, not(feature = "flutter")))] -use std::io::Read; -#[cfg(target_os = "macos")] -use std::os::unix::fs::MetadataExt; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::os::unix::io::RawFd; -#[cfg(windows)] -use std::os::windows::io::AsRawHandle; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -use std::{ - fs, - path::{Path, PathBuf}, - sync::{Mutex, OnceLock}, -}; -#[cfg(windows)] -use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; - -#[cfg(windows)] -#[inline] -pub(crate) fn should_allow_everyone_create_on_windows(postfix: &str) -> bool { - postfix.is_empty() || hbb_common::config::is_service_ipc_postfix(postfix) -} - -#[cfg(windows)] -#[inline] -pub(crate) fn portable_service_listener_security_attributes() -> io::Result { - let user_sid = crate::platform::windows::current_process_user_sid_string().map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("failed to resolve current process SID: {}", err), - ) - })?; - debug_assert!( - user_sid.starts_with("S-1-") - && user_sid - .bytes() - .all(|byte| byte.is_ascii_digit() || byte == b'-'), - "current_process_user_sid_string returned a non-SDDL SID: {}", - user_sid - ); - // SDDL: - // - `D:P` => protected DACL (no inherited ACEs) - // - `(A;;GA;;;SY)` => allow GENERIC_ALL to LocalSystem - // - `(A;;GA;;;{user_sid})` => allow GENERIC_ALL to current process user SID - // References: - // - Security Descriptor String Format: https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format - // - ACE strings in SDDL: https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings - let sddl = format!("D:P(A;;GA;;;SY)(A;;GA;;;{user_sid})"); - SecurityAttributes::from_sddl(&sddl).map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!( - "failed to build portable service listener security attributes from SDDL '{}': {}", - sddl, err - ), - ) - }) -} - -#[cfg(target_os = "macos")] -#[inline] -fn macos_service_ipc_allows_gui_and_service_binaries( - peer_exe: &Path, - current_exe: &Path, - postfix: &str, -) -> bool { - if postfix != crate::POSTFIX_SERVICE { - return false; - } - let Some(peer_dir) = peer_exe.parent() else { - return false; - }; - let Some(current_dir) = current_exe.parent() else { - return false; - }; - if !executable_paths_match(peer_dir, current_dir) { - return false; - } - - // On installed macOS builds, `_service` is listened by the `service` binary while the GUI - // process connects from the app executable within the same app bundle. - let gui_exe_name = std::ffi::OsString::from(crate::get_app_name()); - let gui_exe = gui_exe_name.as_os_str(); - let service_exe = std::ffi::OsStr::new("service"); - let allowed_exe = [Some(gui_exe), Some(service_exe)]; - let peer_name = peer_exe.file_name(); - let current_name = current_exe.file_name(); - allowed_exe - .iter() - .any(|name| os_str_eq_ignore_ascii_case(peer_name, *name)) - && allowed_exe - .iter() - .any(|name| os_str_eq_ignore_ascii_case(current_name, *name)) -} - -#[cfg(target_os = "windows")] -#[inline] -fn windows_portable_service_ipc_allows_logon_helper_executable( - _peer_exe: &Path, - postfix: &str, -) -> bool { - if postfix != "_portable_service" { - return false; - } - #[cfg(feature = "flutter")] - { - false - } - #[cfg(not(feature = "flutter"))] - { - let Some((_, expected)) = crate::platform::windows::portable_service_logon_helper_paths() - else { - return false; - }; - let Ok(expected) = fs::canonicalize(expected) else { - return false; - }; - let Ok(current_exe) = current_exe_canonical_path() else { - return false; - }; - portable_service_helper_is_trusted(_peer_exe, &expected, ¤t_exe) - } -} - -#[cfg(windows)] -#[inline] -pub(crate) fn is_allowed_windows_session_scoped_peer( - client_is_system: bool, - client_session_id: Option, - expected_session_id: Option, -) -> bool { - client_is_system - || matches!( - (client_session_id, expected_session_id), - (Some(client), Some(expected)) if client == expected - ) -} - -#[cfg(windows)] -#[inline] -fn is_allowed_windows_portable_service_peer( - client_is_system: Option, - _client_session_id: Option, - _expected_session_id: Option, -) -> bool { - // Portable-service listener DACL includes SYSTEM and current-process SID. - // In the portable-service path, current process is expected to run as SYSTEM, - // and the higher-layer peer policy stays SYSTEM-only. - matches!(client_is_system, Some(true)) -} - -#[cfg(any(target_os = "macos", target_os = "linux"))] -#[inline] -pub(crate) fn is_allowed_service_peer_uid(peer_uid: u32, active_uid: Option) -> bool { - // Root is allowed at the UID gate because the service side may run as root. - // Callers still enforce executable matching before accepting service-scoped peers. - peer_uid == 0 || active_uid.is_some_and(|uid| uid == peer_uid) -} - -#[cfg(target_os = "macos")] -#[inline] -fn console_owner_uid() -> Option { - fs::metadata("/dev/console") - .ok() - .map(|metadata| metadata.uid()) -} - -#[cfg(target_os = "macos")] -#[inline] -fn active_uid_strict() -> Option { - // Prefer the filesystem metadata over parsing external command output. - console_owner_uid() -} - -#[cfg(target_os = "linux")] -#[inline] -fn active_uid_strict() -> Option { - let reported_uid_raw = crate::platform::linux::get_active_userid(); - let trimmed = reported_uid_raw.trim(); - if let Ok(uid) = trimmed.parse::() { - return Some(uid); - } - if trimmed.is_empty() { - log::debug!("Failed to resolve active user uid on linux: active uid is empty"); - } else { - log::warn!("Failed to parse active user uid on linux: '{}'", trimmed); - } - None -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -pub(crate) fn active_uid() -> Option { - active_uid_strict() -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -pub(crate) fn peer_uid_from_fd(fd: RawFd) -> Option { - #[cfg(target_os = "linux")] - { - return peer_cred_from_fd(fd).map(|cred| cred.uid as u32); - } - #[cfg(target_os = "macos")] - { - let mut uid = 0; - let mut gid = 0; - if unsafe { libc::getpeereid(fd, &mut uid, &mut gid) } == 0 { - Some(uid as u32) - } else { - None - } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -fn peer_pid_from_fd(fd: RawFd) -> Option { - #[cfg(target_os = "linux")] - { - return peer_cred_from_fd(fd).and_then(|cred| (cred.pid > 0).then_some(cred.pid as u32)); - } - #[cfg(target_os = "macos")] - { - let mut pid = 0; - let mut len = std::mem::size_of::() as _; - let rc = unsafe { - libc::getsockopt( - fd, - libc::SOL_LOCAL, - libc::LOCAL_PEERPID, - &mut pid as *mut _ as *mut libc::c_void, - &mut len, - ) - }; - if rc == 0 && pid > 0 { - Some(pid as _) - } else { - None - } - } -} - -#[cfg(target_os = "linux")] -#[inline] -fn peer_cred_from_fd(fd: RawFd) -> Option { - let mut cred: libc::ucred = unsafe { std::mem::zeroed() }; - let mut len = std::mem::size_of::() as _; - let rc = unsafe { - libc::getsockopt( - fd, - libc::SOL_SOCKET, - libc::SO_PEERCRED, - &mut cred as *mut _ as *mut libc::c_void, - &mut len, - ) - }; - if rc == 0 { - Some(cred) - } else { - None - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -fn current_exe_canonical_path() -> ResultType { - let current = std::env::current_exe() - .map_err(|err| anyhow::anyhow!("Failed to resolve current executable path: {}", err))?; - fs::canonicalize(¤t).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize current executable path '{}': {}", - current.display(), - err - ) - .into() - }) -} - -#[cfg(target_os = "linux")] -#[inline] -fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { - let proc_exe = PathBuf::from(format!("/proc/{peer_pid}/exe")); - let peer_exe = fs::read_link(&proc_exe).map_err(|err| { - anyhow::anyhow!( - "Failed to read peer executable link '{}': {}", - proc_exe.display(), - err - ) - })?; - fs::canonicalize(&peer_exe).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize peer executable path '{}': {}", - peer_exe.display(), - err - ) - .into() - }) -} - -#[cfg(target_os = "macos")] -#[inline] -fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { - const PROC_PIDPATH_BUF_SIZE: usize = libc::PROC_PIDPATHINFO_MAXSIZE as _; - let mut buffer = vec![0u8; PROC_PIDPATH_BUF_SIZE]; - let length = unsafe { - libc::proc_pidpath( - peer_pid as _, - buffer.as_mut_ptr() as _, - PROC_PIDPATH_BUF_SIZE as _, - ) - }; - if length <= 0 { - bail!("Failed to query peer process path from pid {}", peer_pid); - } - buffer.truncate(length as _); - let path = PathBuf::from(String::from_utf8_lossy(&buffer).to_string()); - fs::canonicalize(&path).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize peer executable path '{}': {}", - path.display(), - err - ) - .into() - }) -} - -#[cfg(target_os = "windows")] -#[inline] -fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { - let path = crate::platform::windows::get_process_executable_path(peer_pid)?; - fs::canonicalize(&path).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize peer executable path '{}': {}", - path.display(), - err - ) - .into() - }) -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -pub(crate) fn executable_paths_match(left: &Path, right: &Path) -> bool { - #[cfg(target_os = "windows")] - { - // Callers pass paths resolved through fs::canonicalize() first, so NT - // namespace paths and 8.3 short names are expected to be resolved before - // this check. Keep this normalization limited to remaining Win32 spelling - // differences. - fn normalize(path: &Path) -> String { - let mut normalized = path.to_string_lossy().replace('/', "\\"); - if let Some(stripped) = normalized.strip_prefix(r"\\?\") { - normalized = stripped.to_owned(); - } - normalized.to_ascii_lowercase() - } - return normalize(left) == normalize(right); - } - #[cfg(target_os = "macos")] - { - return paths_refer_to_same_file(left, right); - } - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - { - left == right - } -} - -#[cfg(target_os = "macos")] -#[inline] -fn paths_refer_to_same_file(left: &Path, right: &Path) -> bool { - if left == right { - return true; - } - let (Ok(left), Ok(right)) = (fs::metadata(left), fs::metadata(right)) else { - return false; - }; - left.dev() == right.dev() && left.ino() == right.ino() -} - -#[cfg(target_os = "macos")] -#[inline] -fn os_str_eq_ignore_ascii_case( - left: Option<&std::ffi::OsStr>, - right: Option<&std::ffi::OsStr>, -) -> bool { - let (Some(left), Some(right)) = (left, right) else { - return false; - }; - left.to_string_lossy() - .eq_ignore_ascii_case(&right.to_string_lossy()) -} - -#[cfg(all(windows, not(feature = "flutter")))] -#[inline] -fn file_sha256(path: &Path) -> ResultType<[u8; 32]> { - let mut file = fs::File::open(path)?; - let mut hasher = Sha256::new(); - let mut buffer = [0u8; 8 * 1024]; - loop { - let read_bytes = file.read(&mut buffer)?; - if read_bytes == 0 { - break; - } - hasher.update(&buffer[..read_bytes]); - } - Ok(hasher.finalize().into()) -} - -#[cfg(all(windows, not(feature = "flutter")))] -#[inline] -fn portable_service_helper_is_trusted( - peer_exe: &Path, - expected_exe: &Path, - current_exe: &Path, -) -> bool { - if !executable_paths_match(peer_exe, expected_exe) { - return false; - } - let peer_hash = match file_sha256(peer_exe) { - Ok(hash) => hash, - Err(err) => { - log::warn!( - "Failed to hash peer portable helper executable '{}': {}", - peer_exe.display(), - err - ); - return false; - } - }; - let current_hash = match file_sha256(current_exe) { - Ok(hash) => hash, - Err(err) => { - log::warn!( - "Failed to hash current executable '{}' for portable helper trust check: {}", - current_exe.display(), - err - ); - return false; - } - }; - peer_hash == current_hash -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -fn ensure_peer_executable_matches_current_by_pid(peer_pid: u32, postfix: &str) -> ResultType<()> { - let peer_exe = peer_exe_canonical_path_by_pid(peer_pid)?; - let current_exe = current_exe_canonical_path()?; - if executable_paths_match(&peer_exe, ¤t_exe) { - return Ok(()); - } - #[cfg(target_os = "macos")] - if macos_service_ipc_allows_gui_and_service_binaries(&peer_exe, ¤t_exe, postfix) { - return Ok(()); - } - #[cfg(target_os = "windows")] - if windows_portable_service_ipc_allows_logon_helper_executable(&peer_exe, postfix) { - return Ok(()); - } - bail!( - "Peer executable path mismatch on ipc channel '{}': peer_pid={}, peer_exe='{}', current_exe='{}'", - postfix, - peer_pid, - peer_exe.display(), - current_exe.display() - ); -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -pub(crate) fn ensure_peer_executable_matches_current_by_pid_opt( - peer_pid: Option, - postfix: &str, -) -> ResultType<()> { - let peer_pid = peer_pid.ok_or_else(|| { - anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix) - })?; - ensure_peer_executable_matches_current_by_pid(peer_pid, postfix) -} - -#[cfg(target_os = "linux")] -#[inline] -pub(crate) fn ensure_peer_executable_matches_current_by_fd( - fd: RawFd, - postfix: &str, -) -> ResultType<()> { - let peer_pid = peer_pid_from_fd(fd).ok_or_else(|| { - anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix) - })?; - ensure_peer_executable_matches_current_by_pid(peer_pid, postfix) -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -const UNAUTHORIZED_IPC_LOG_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[derive(Default)] -struct UnauthorizedIpcLogThrottle { - last_log_at: Option, - suppressed: u64, -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -impl UnauthorizedIpcLogThrottle { - #[inline] - fn on_reject(&mut self, now: std::time::Instant) -> Option { - if let Some(last) = self.last_log_at { - if now.saturating_duration_since(last) < UNAUTHORIZED_IPC_LOG_INTERVAL { - self.suppressed += 1; - return None; - } - } - self.last_log_at = Some(now); - Some(std::mem::take(&mut self.suppressed)) - } -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[inline] -fn throttled_unauthorized_ipc_log( - throttle_cell: &OnceLock>, - emit: impl FnOnce(u64), -) { - let throttle = throttle_cell.get_or_init(|| Mutex::new(UnauthorizedIpcLogThrottle::default())); - let should_log = match throttle.lock() { - Ok(mut throttle) => throttle.on_reject(std::time::Instant::now()), - Err(_) => Some(0), - }; - if let Some(suppressed) = should_log { - emit(suppressed); - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -fn log_rejected_service_connection(postfix: &str, peer_uid: Option, active_uid: Option) { - static LOG_THROTTLE: OnceLock> = OnceLock::new(); - throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { - if suppressed > 0 { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)", - postfix, - peer_uid, - active_uid, - suppressed - ); - } else { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?}", - postfix, - peer_uid, - active_uid - ); - } - }); -} - -#[cfg(target_os = "linux")] -#[inline] -pub(crate) fn log_rejected_uinput_connection( - postfix: &str, - peer_uid: Option, - active_uid: Option, -) { - static LOG_THROTTLE: OnceLock> = OnceLock::new(); - throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { - if suppressed > 0 { - log::warn!( - "Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)", - postfix, - peer_uid, - active_uid, - suppressed - ); - } else { - log::warn!( - "Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?}", - postfix, - peer_uid, - active_uid - ); - } - }); -} - -#[cfg(windows)] -#[inline] -pub(crate) fn log_rejected_windows_ipc_connection( - postfix: &str, - peer_pid: Option, - 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)", - 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={:?}", - postfix, - peer_pid, - peer_session_id, - expected_session_id, - peer_is_system, - peer_is_elevated - ); - } - }); -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postfix: &str) -> bool { - let peer_pid = stream.peer_pid(); - let (authorized, peer_uid, active_uid) = stream.service_authorization_status(); - if !authorized { - log_rejected_service_connection(postfix, peer_uid, active_uid); - return false; - } - if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", - postfix, - peer_pid, - err - ); - return false; - } - true -} - -#[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(); - if !authorized { - log_rejected_windows_ipc_connection( - postfix, - peer_pid, - peer_session_id, - server_session_id, - peer_is_system, - peer_is_elevated, - ); - return false; - } - if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { - log::warn!( - "Rejected unauthorized connection on ipc channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", - postfix, - peer_pid, - err - ); - return false; - } - true -} - -#[cfg(windows)] -pub(crate) fn authorize_windows_portable_service_ipc_connection( - stream: &Connection, - postfix: &str, -) -> bool { - // Portable service IPC policy: - // - only SYSTEM peers are authorized by is_allowed_windows_portable_service_peer() - // - expected_session_id is still collected for diagnostics and identity checks - // - final privilege boundary is enforced by named-pipe ACL + one-time token handshake - // - when peer identity is unavailable on some hosts, executable verification remains - // best-effort telemetry (not fail-closed) to avoid breaking valid SYSTEM bootstrap - // flows that cannot be fully introspected - let expected_session_id = crate::platform::windows::get_current_process_session_id(); - let (authorized, peer_pid, peer_session_id, peer_is_system) = - stream.portable_service_authorization_status_for_session(expected_session_id); - if !authorized { - // Session lookup may succeed while SYSTEM identity lookup fails, so only the - // SYSTEM identity result determines whether peer identity is unavailable here. - // Don't use `peer_pid.is_some() && peer_session_id.is_none() && peer_is_system.is_none();` here. - let identity_unavailable = peer_pid.is_some() && peer_is_system.is_none(); - if identity_unavailable { - // In portable-service startup, resolving SYSTEM peer identity may fail on some hosts. - // `ProcessIdToSessionId` can still succeed while `OpenProcessToken(TOKEN_QUERY)` is - // denied by the peer token DACL or missing privileges. Treat that partial identity - // failure as unavailable and defer final authorization to pipe ACL + token handshake. - if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { - log::warn!( - "Portable service ipc peer identity unavailable and executable verification failed; continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, err={}", - postfix, - peer_pid, - err - ); - } else { - log::warn!( - "Portable service ipc peer identity unavailable; executable verification matched, continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, expected_session_id={:?}", - postfix, - peer_pid, - expected_session_id - ); - } - return true; - } - log::warn!( - "Rejected unauthorized connection on portable service 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 - ); - return false; - } - true -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl ConnectionTmpl -where - T: AsyncRead + AsyncWrite + std::marker::Unpin + std::os::unix::io::AsRawFd, -{ - pub(super) fn peer_uid(&self) -> Option { - peer_uid_from_fd(self.inner.get_ref().as_raw_fd()) - } - - fn service_authorization_status(&self) -> (bool, Option, Option) { - let peer_uid = self.peer_uid(); - // On Linux, `_service` can use the cached active UID from the service loop for - // stable config sync. Uinput does a fresh active-UID lookup in its own authorizer. - let active_uid = active_uid(); - let authorized = peer_uid.is_some_and(|uid| is_allowed_service_peer_uid(uid, active_uid)); - (authorized, peer_uid, active_uid) - } - - pub(super) fn peer_pid(&self) -> Option { - peer_pid_from_fd(self.inner.get_ref().as_raw_fd()) - } -} - -#[cfg(windows)] -impl ConnectionTmpl { - fn peer_pid(&self) -> Option { - let pipe_handle = self.inner.get_ref().as_raw_handle(); - if pipe_handle.is_null() { - return None; - } - let mut pid = 0u32; - let ok = unsafe { GetNamedPipeClientProcessId(HANDLE(pipe_handle), &mut pid as *mut u32) } - .is_ok(); - if ok && pid != 0 { - Some(pid) - } else { - None - } - } - - fn server_authorization_status( - &self, - ) -> ( - bool, - Option, - 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 = - peer_pid.and_then(crate::platform::windows::get_session_id_of_process); - let peer_is_system_result = - peer_pid.map(crate::platform::windows::is_process_running_as_system); - 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) - { - // When the server session id cannot be determined, the session-id allow-path is - // disabled and only privileged peers can be authorized. - log::debug!( - "IPC authorization: server session id unavailable; rejecting non-privileged 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); - if !authorized { - if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { - log::debug!( - "Failed to determine whether peer process is SYSTEM, pid={}, err={}", - pid, - 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, - peer_pid, - peer_session_id, - server_session_id, - peer_is_system, - peer_is_elevated, - ) - } - - pub(crate) fn service_authorization_status_for_session( - &self, - expected_active_session_id: Option, - ) -> (bool, Option, Option, Option) { - let peer_pid = self.peer_pid(); - let peer_session_id = - peer_pid.and_then(crate::platform::windows::get_session_id_of_process); - let peer_is_system_result = - peer_pid.map(crate::platform::windows::is_process_running_as_system); - let peer_is_system = peer_is_system_result - .as_ref() - .and_then(|r| r.as_ref().ok().copied()); - let authorized = is_allowed_windows_session_scoped_peer( - peer_is_system.unwrap_or(false), - peer_session_id, - expected_active_session_id, - ); - if !authorized { - if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { - log::debug!( - "Failed to determine whether peer process is SYSTEM, pid={}, err={}", - pid, - err - ); - } - } - (authorized, peer_pid, peer_session_id, peer_is_system) - } - - pub(crate) fn portable_service_authorization_status_for_session( - &self, - expected_active_session_id: Option, - ) -> (bool, Option, Option, Option) { - // Portable-service policy: - // only SYSTEM peers are allowed. - let (_service_authorized, peer_pid, peer_session_id, peer_is_system) = - self.service_authorization_status_for_session(expected_active_session_id); - ( - is_allowed_windows_portable_service_peer( - peer_is_system, - peer_session_id, - expected_active_session_id, - ), - peer_pid, - peer_session_id, - peer_is_system, - ) - } -} - -#[cfg(test)] -mod tests { - #[test] - #[cfg(any(target_os = "macos", target_os = "linux"))] - fn test_service_peer_uid_policy() { - assert!(super::is_allowed_service_peer_uid(0, None)); - assert!(super::is_allowed_service_peer_uid(501, Some(501))); - assert!(!super::is_allowed_service_peer_uid(502, Some(501))); - assert!(!super::is_allowed_service_peer_uid(501, None)); - } - - #[test] - #[cfg(windows)] - fn test_windows_server_peer_policy() { - assert!(super::is_allowed_windows_session_scoped_peer( - true, None, None - )); - assert!(super::is_allowed_windows_session_scoped_peer( - false, - Some(1), - Some(1) - )); - assert!(!super::is_allowed_windows_session_scoped_peer( - false, - Some(1), - Some(2) - )); - assert!(!super::is_allowed_windows_session_scoped_peer( - false, - None, - Some(1) - )); - } - - #[test] - #[cfg(windows)] - fn test_windows_portable_service_peer_policy() { - assert!(super::is_allowed_windows_portable_service_peer( - Some(true), - None, - None - )); - assert!(!super::is_allowed_windows_portable_service_peer( - Some(false), - Some(1), - Some(1) - )); - assert!(!super::is_allowed_windows_portable_service_peer( - Some(false), - Some(1), - Some(2) - )); - assert!(!super::is_allowed_windows_portable_service_peer( - None, - Some(1), - Some(1) - )); - } - - #[test] - #[cfg(windows)] - fn test_should_allow_everyone_create_on_windows_policy() { - assert!(super::should_allow_everyone_create_on_windows("")); - assert!(super::should_allow_everyone_create_on_windows("_service")); - assert!(!super::should_allow_everyone_create_on_windows( - "_portable_service" - )); - } - - #[test] - #[cfg(windows)] - fn test_executable_paths_match_windows_normalization() { - let left = std::path::PathBuf::from(r"\\?\C:\Program Files\RustDesk\RustDesk.exe"); - let right = std::path::PathBuf::from(r"c:\program files\rustdesk\rustdesk.exe"); - assert!(super::executable_paths_match(&left, &right)); - } - - #[test] - #[cfg(target_os = "macos")] - fn test_os_str_eq_ignore_ascii_case_for_process_names() { - assert!(super::os_str_eq_ignore_ascii_case( - Some(std::ffi::OsStr::new("RustDesk")), - Some(std::ffi::OsStr::new("rustdesk")) - )); - assert!(!super::os_str_eq_ignore_ascii_case( - Some(std::ffi::OsStr::new("RustDesk")), - Some(std::ffi::OsStr::new("service")) - )); - } - - #[cfg(all(windows, not(feature = "flutter")))] - struct TempDirGuard(std::path::PathBuf); - - #[cfg(all(windows, not(feature = "flutter")))] - impl Drop for TempDirGuard { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.0); - } - } - - #[test] - #[cfg(all(windows, not(feature = "flutter")))] - fn test_portable_service_helper_trust_requires_content_match() { - let unique = format!( - "rustdesk-portable-helper-trust-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - let _cleanup = TempDirGuard(base.clone()); - - let current_exe = base.join("current.exe"); - let helper_exe = base.join("helper.exe"); - std::fs::write(¤t_exe, b"trusted-binary").unwrap(); - std::fs::write(&helper_exe, b"tampered-binary").unwrap(); - - assert!( - !super::portable_service_helper_is_trusted(&helper_exe, &helper_exe, ¤t_exe), - "helper trust check must reject path-match-only binaries with mismatched content" - ); - } - - #[test] - #[cfg(all(windows, not(feature = "flutter")))] - fn test_portable_service_helper_trust_accepts_matching_content() { - let unique = format!( - "rustdesk-portable-helper-trust-match-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - let _cleanup = TempDirGuard(base.clone()); - - let current_exe = base.join("current.exe"); - let helper_exe = base.join("helper.exe"); - std::fs::write(¤t_exe, b"trusted-binary").unwrap(); - std::fs::write(&helper_exe, b"trusted-binary").unwrap(); - - assert!(super::portable_service_helper_is_trusted( - &helper_exe, - &helper_exe, - ¤t_exe - )); - } - - #[cfg(target_os = "macos")] - #[test] - fn test_console_owner_uid_matches_get_active_userid() { - let console_uid = - super::console_owner_uid().expect("/dev/console must have a resolvable uid"); - let raw_uid = crate::platform::macos::get_active_userid(); - let parsed_uid: u32 = raw_uid - .trim() - .parse() - .unwrap_or_else(|_| panic!("failed to parse get_active_userid() output: '{raw_uid}'")); - assert_eq!(parsed_uid, console_uid); - } -} diff --git a/src/ipc/fs.rs b/src/ipc/fs.rs deleted file mode 100644 index e0157f3a9..000000000 --- a/src/ipc/fs.rs +++ /dev/null @@ -1,951 +0,0 @@ -#[cfg(target_os = "linux")] -use super::ipc_auth::active_uid; -use crate::ipc::{connect, Data}; -use hbb_common::{config, log, ResultType}; -use std::{ - ffi::CString, - io::{Error, ErrorKind}, - os::unix::ffi::OsStrExt, - path::Path, -}; - -struct FdGuard(i32); -impl Drop for FdGuard { - fn drop(&mut self) { - unsafe { - hbb_common::libc::close(self.0); - } - } -} - -#[cfg(target_os = "linux")] -#[inline] -pub(crate) fn terminal_count_candidate_uids(effective_uid: u32) -> Vec { - if effective_uid != 0 { - return vec![effective_uid]; - } - let mut candidates = Vec::with_capacity(2); - if let Some(uid) = active_uid().filter(|uid| *uid != 0) { - candidates.push(uid); - } - candidates.push(0); - candidates -} - -#[inline] -fn expected_ipc_parent_mode(postfix: &str) -> u32 { - if config::is_service_ipc_postfix(postfix) { - 0o0711 - } else { - 0o0700 - } -} - -fn open_ipc_parent_dir_fd(parent_c: &CString) -> std::io::Result { - let fd = unsafe { - hbb_common::libc::open( - parent_c.as_ptr(), - hbb_common::libc::O_RDONLY - | hbb_common::libc::O_DIRECTORY - | hbb_common::libc::O_CLOEXEC - | hbb_common::libc::O_NOFOLLOW, - ) - }; - if fd < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(fd) - } -} - -// Remove one preexisting IPC artifact via an already-opened parent directory FD. -// -// Security intent: -// - Bind cleanup to the exact parent inode that passed O_NOFOLLOW + fstat checks. -// - Avoid path-based TOCTOU during scrub (e.g., parent path rename/swap race). -// -// Flow: -// 1) fstatat(..., AT_SYMLINK_NOFOLLOW) to inspect the target entry under parent_fd. -// 2) Decide file vs directory from st_mode. -// 3) unlinkat relative to parent_fd (AT_REMOVEDIR for directories). -// -// Error policy: -// - NotFound is treated as benign (already removed / raced away). -// - Other errors are surfaced explicitly. -fn remove_parent_entry_via_fd( - parent_fd: i32, - parent_dir: &Path, - entry_name: &str, -) -> ResultType<()> { - if entry_name.contains('/') { - return Err(Error::new( - ErrorKind::InvalidInput, - format!( - "invalid ipc parent entry name (contains '/'): parent={}, entry={}", - parent_dir.display(), - entry_name - ), - ) - .into()); - } - let entry_c = CString::new(entry_name.as_bytes().to_vec()).map_err(|err| { - Error::new( - ErrorKind::InvalidInput, - format!( - "invalid ipc parent entry name: parent={}, entry={}, err={}", - parent_dir.display(), - entry_name, - err - ), - ) - })?; - let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - let stat_rc = unsafe { - hbb_common::libc::fstatat( - parent_fd, - entry_c.as_ptr(), - &mut stat, - hbb_common::libc::AT_SYMLINK_NOFOLLOW, - ) - }; - if stat_rc != 0 { - let err = std::io::Error::last_os_error(); - if err.kind() == ErrorKind::NotFound { - return Ok(()); - } - return Err(Error::new( - err.kind(), - format!( - "failed to stat preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}", - parent_dir.display(), - entry_name, - err - ), - ) - .into()); - } - - let is_dir = (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) - == hbb_common::libc::S_IFDIR; - let unlink_flags = if is_dir { - hbb_common::libc::AT_REMOVEDIR - } else { - 0 - }; - let unlink_rc = - unsafe { hbb_common::libc::unlinkat(parent_fd, entry_c.as_ptr(), unlink_flags) }; - if unlink_rc != 0 { - let err = std::io::Error::last_os_error(); - if err.kind() == ErrorKind::NotFound { - return Ok(()); - } - return Err(Error::new( - err.kind(), - format!( - "failed to remove preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}", - parent_dir.display(), - entry_name, - err - ), - ) - .into()); - } - Ok(()) -} - -fn scrub_preexisting_ipc_parent_entries( - parent_fd: i32, - parent_dir: &Path, - postfix: &str, -) -> ResultType<()> { - let ipc_basename = format!("ipc{}", postfix); - remove_parent_entry_via_fd(parent_fd, parent_dir, &ipc_basename)?; - remove_parent_entry_via_fd(parent_fd, parent_dir, &format!("{}.pid", ipc_basename))?; - Ok(()) -} - -fn remove_ipc_socket_via_secure_parent_fd(postfix: &str) -> ResultType<()> { - let path = config::Config::ipc_path(postfix); - let parent_dir = Path::new(&path) - .parent() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; - let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; - let fd = match open_ipc_parent_dir_fd(&parent_c) { - Ok(fd) => fd, - Err(open_err) => { - if open_err.kind() == ErrorKind::NotFound { - return Ok(()); - } - return Err(Error::new( - open_err.kind(), - format!( - "failed to open ipc parent dir for stale socket cleanup (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - open_err - ), - ) - .into()); - } - }; - let _fd_guard = FdGuard(fd); - remove_parent_entry_via_fd(fd, parent_dir, &format!("ipc{}", postfix)) -} - -// Purpose: -// - Harden the IPC parent directory before creating/listening socket files. -// - Prevent symlink/path-race abuse and reject unsafe owner/mode. -// -// Approach: -// - Open parent dir with O_NOFOLLOW/O_DIRECTORY and operate on that fd. -// - Validate inode type/owner/mode via fstat. -// - For protected service postfix, optionally adopt owner (root only), then scrub stale -// rustdesk IPC artifacts when directory trust boundary changed. -// -// Main steps: -// 1) Resolve parent path and open/create directory securely. -// 2) Verify directory inode type and owner uid. -// 3) Enforce expected mode via fchmod on opened fd. -// 4) Scrub stale IPC artifacts when owner/mode was unsafe before hardening. -// -// References: -// - open(2): O_NOFOLLOW/O_DIRECTORY/O_CLOEXEC -// https://man7.org/linux/man-pages/man2/open.2.html -// - fstat(2): verify file type/metadata on opened fd -// https://man7.org/linux/man-pages/man2/fstat.2.html -// - fchown(2): adopt ownership when running as root -// https://man7.org/linux/man-pages/man2/chown.2.html -// - fchmod(2): enforce exact mode on opened fd -// https://man7.org/linux/man-pages/man2/fchmod.2.html -pub(crate) fn ensure_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType { - let parent_dir = Path::new(path) - .parent() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; - // Harden against common TOCTOU by opening the parent directory with O_NOFOLLOW (so the parent - // itself cannot be a symlink) and then operating on its FD (fstat/fchown/fchmod). This ensures - // we mutate the inode we opened, though it does not protect against symlinks in ancestor path - // components. - let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; - let fd = match open_ipc_parent_dir_fd(&parent_c) { - Ok(fd) => fd, - Err(open_err) => { - // If the directory doesn't exist yet, create it with the expected mode. The parent - // dir is intended to be a single-level /tmp path, so mkdir is sufficient here. - if open_err.raw_os_error() == Some(hbb_common::libc::ENOENT) { - let expected_mode = expected_ipc_parent_mode(postfix); - let rc = unsafe { - hbb_common::libc::mkdir( - parent_c.as_ptr(), - expected_mode as hbb_common::libc::mode_t, - ) - }; - if rc != 0 { - let mkdir_err = std::io::Error::last_os_error(); - // Handle a race where another process created the directory first. - if mkdir_err.raw_os_error() != Some(hbb_common::libc::EEXIST) { - return Err(Error::new( - mkdir_err.kind(), - format!( - "failed to mkdir ipc parent dir: postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - mkdir_err - ), - ) - .into()); - } - } - match open_ipc_parent_dir_fd(&parent_c) { - Ok(fd) => fd, - Err(err) => { - return Err(Error::new( - err.kind(), - format!( - "failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - err - ), - ) - .into()); - } - } - } else { - return Err(Error::new( - open_err.kind(), - format!( - "failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - open_err - ), - ) - .into()); - } - } - }; - let _fd_guard = FdGuard(fd); - - let mut st: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut st as *mut _) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to stat ipc parent dir: postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - os_err - ), - ) - .into()); - } - let mode = st.st_mode as u32; - let is_dir = (mode & (hbb_common::libc::S_IFMT as u32)) == (hbb_common::libc::S_IFDIR as u32); - if !is_dir { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "ipc parent is not directory: postfix={}, parent={}", - postfix, - parent_dir.display() - ), - ) - .into()); - } - - let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - let mut owner_uid = st.st_uid as u32; - let mut adopted_foreign_service_parent = false; - // Service-scoped IPC may be created by different privilege contexts historically. - // If running as root on protected service postfix, try adopting ownership first. - if owner_uid != expected_uid && expected_uid == 0 && config::is_service_ipc_postfix(postfix) { - let rc = unsafe { - hbb_common::libc::fchown( - fd, - expected_uid as hbb_common::libc::uid_t, - hbb_common::libc::gid_t::MAX, - ) - }; - if rc == 0 { - let mut st2: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut st2 as *mut _) } == 0 { - owner_uid = st2.st_uid as u32; - st = st2; - adopted_foreign_service_parent = true; - } - } else { - // Keep behavior unchanged; capture errno to ease diagnosing why chown failed. - let err = std::io::Error::last_os_error(); - log::warn!( - "Failed to chown ipc parent dir, parent={}, postfix={}, expected_uid={}, rc={}, err={:?}", - parent_dir.display(), - postfix, - expected_uid, - rc, - err - ); - } - } - if owner_uid != expected_uid { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "unsafe ipc parent owner, postfix={}, expected uid {expected_uid}, got {owner_uid}: {}", - postfix, - parent_dir.display() - ), - ) - .into()); - } - - let expected_mode = expected_ipc_parent_mode(postfix); - // Include special bits (setuid/setgid/sticky) to ensure the directory is hardened to the exact - // expected mode. - let current_mode = (st.st_mode as u32) & 0o7777; - let repaired_parent_mode = current_mode != expected_mode; - let had_untrusted_parent_mode = (current_mode & 0o022) != 0; - if repaired_parent_mode { - // Use fchmod on the opened fd to avoid path-race between check and chmod. - if unsafe { hbb_common::libc::fchmod(fd, expected_mode as hbb_common::libc::mode_t) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to chmod ipc parent dir: postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - os_err - ), - ) - .into()); - } - } - let should_scrub = - repaired_parent_mode || adopted_foreign_service_parent || had_untrusted_parent_mode; - Ok(should_scrub) -} - -pub(crate) fn scrub_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<()> { - let parent_dir = Path::new(path) - .parent() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; - let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; - let fd = open_ipc_parent_dir_fd(&parent_c).map_err(|err| { - Error::new( - err.kind(), - format!( - "failed to open ipc parent dir for scrub (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - err - ), - ) - })?; - let _fd_guard = FdGuard(fd); - scrub_preexisting_ipc_parent_entries(fd, parent_dir, postfix) -} - -#[inline] -pub(crate) fn get_pid_file(postfix: &str) -> String { - let path = config::Config::ipc_path(postfix); - format!("{}.pid", path) -} - -// Purpose: -// - Write current process pid to pid file without following attacker-controlled symlinks. -// - Ensure the pid file is a regular file owned by the opened inode path. -// -// Approach: -// - Use libc open/fstat/write syscalls (FFI) so flags and inode validation are explicit. -// - Open file with O_NOFOLLOW/O_CLOEXEC and verify S_IFREG with fstat before write. -// - Keep unsafe scopes minimal and check syscall return values immediately. -// -// Main steps: -// 1) Secure-open pid file (without truncation). -// 2) Validate opened inode is a regular file owned by current euid. -// 3) Enforce pid file mode to 0600 and truncate via ftruncate after validation. -// 4) Write process id bytes through fd. -// -// Why not plain std::fs::write? -// - std::fs helpers cannot enforce this exact open-time hardening sequence -// (especially "open with O_NOFOLLOW, then fstat the same opened inode"). -// -// References: -// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK -// https://man7.org/linux/man-pages/man2/open.2.html -// - fstat(2): verify file type on opened fd -// https://man7.org/linux/man-pages/man2/fstat.2.html -// - fchmod(2): enforce secure mode on reused pid file -// https://man7.org/linux/man-pages/man2/fchmod.2.html -// - ftruncate(2): truncate after validation -// https://man7.org/linux/man-pages/man2/ftruncate.2.html -// - write(2): write bytes via fd -// https://man7.org/linux/man-pages/man2/write.2.html -fn write_pid_file(path: &Path) -> ResultType<()> { - let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).map_err(|err| { - Error::new( - ErrorKind::InvalidInput, - format!("invalid pid file path '{}': {}", path.display(), err), - ) - })?; - let flags = hbb_common::libc::O_WRONLY - | hbb_common::libc::O_CREAT - | hbb_common::libc::O_CLOEXEC - | hbb_common::libc::O_NOFOLLOW - | hbb_common::libc::O_NONBLOCK; - let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags, 0o0600) }; - if fd < 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to open pid file with no-follow '{}': {}", - path.display(), - os_err - ), - ) - .into()); - } - let _fd_guard = FdGuard(fd); - let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!("failed to stat pid file '{}': {}", path.display(), os_err), - ) - .into()); - } - if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) - != (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t) - { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!("pid file path is not a regular file: '{}'", path.display()), - ) - .into()); - } - let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - if stat.st_uid as u32 != expected_uid { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "pid file owner mismatch: expected uid {}, got {} for '{}'", - expected_uid, - stat.st_uid, - path.display() - ), - ) - .into()); - } - if unsafe { hbb_common::libc::fchmod(fd, 0o600) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!("failed to chmod pid file '{}': {}", path.display(), os_err), - ) - .into()); - } - if unsafe { hbb_common::libc::ftruncate(fd, 0) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to truncate pid file '{}': {}", - path.display(), - os_err - ), - ) - .into()); - } - - let bytes = std::process::id().to_string(); - let buf = bytes.as_bytes(); - // `write(2)` is allowed to return a short write even for regular files. - // PID content is tiny and usually written in one shot, but we still loop - // until all bytes are persisted so this path is semantically correct. - let mut written = 0usize; - while written < buf.len() { - let rc = unsafe { - hbb_common::libc::write( - fd, - buf[written..].as_ptr() as *const hbb_common::libc::c_void, - buf.len() - written, - ) - }; - if rc < 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!("failed to write pid file '{}': {}", path.display(), os_err), - ) - .into()); - } - if rc == 0 { - return Err(Error::new( - ErrorKind::WriteZero, - format!( - "failed to write pid file '{}': write returned 0 bytes", - path.display() - ), - ) - .into()); - } - written += rc as usize; - } - Ok(()) -} - -#[inline] -pub(crate) fn write_pid(postfix: &str) { - let path = std::path::PathBuf::from(get_pid_file(postfix)); - if let Err(err) = write_pid_file(&path) { - log::warn!( - "Failed to write pid file for postfix '{}', path='{}', err={}", - postfix, - path.display(), - err - ); - } -} - -// Purpose: -// - Read pid file safely and avoid trusting symlink/non-regular files. -// -// Approach: -// - Use libc open/fstat/read syscalls (FFI) to control flags and inode checks. -// - Open path with O_NOFOLLOW, validate opened fd via fstat, then read and parse. -// - Keep unsafe scopes minimal and check syscall return values immediately. -// -// Main steps: -// 1) Secure-open pid file read-only. -// 2) Ensure fd points to regular file. -// 3) Read bytes and parse usize pid. -// -// References: -// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK -// https://man7.org/linux/man-pages/man2/open.2.html -// - fstat(2): validate S_IFREG on opened fd -// https://man7.org/linux/man-pages/man2/fstat.2.html -// - read(2): read bytes via fd -// https://man7.org/linux/man-pages/man2/read.2.html -#[inline] -fn read_pid_file_secure(path: &Path) -> Option { - let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).ok()?; - let flags = hbb_common::libc::O_RDONLY - | hbb_common::libc::O_CLOEXEC - | hbb_common::libc::O_NOFOLLOW - | hbb_common::libc::O_NONBLOCK; - let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags) }; - if fd < 0 { - return None; - } - let _fd_guard = FdGuard(fd); - - let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 { - return None; - } - if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) - != (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t) - { - return None; - } - - let mut buffer = [0u8; 64]; - let read_len = unsafe { - hbb_common::libc::read( - fd, - buffer.as_mut_ptr() as *mut hbb_common::libc::c_void, - buffer.len(), - ) - }; - if read_len <= 0 { - return None; - } - let content = String::from_utf8_lossy(&buffer[..read_len as usize]).to_string(); - content.trim().parse::().ok() -} - -#[inline] -async fn probe_existing_listener(postfix: &str) -> bool { - let Ok(mut stream) = connect(1000, postfix).await else { - return false; - }; - if postfix != crate::POSTFIX_SERVICE { - return true; - } - if stream.send(&Data::SyncConfig(None)).await.is_err() { - return false; - } - matches!( - stream.next_timeout(1000).await, - Ok(Some(Data::SyncConfig(Some(_)))) - ) -} - -pub(crate) async fn check_pid(postfix: &str) -> bool { - let pid_file = std::path::PathBuf::from(get_pid_file(postfix)); - if let Some(pid) = read_pid_file_secure(&pid_file) { - if pid > 0 { - let mut sys = hbb_common::sysinfo::System::new(); - sys.refresh_processes(); - if let Some(p) = sys.process(pid.into()) { - if let Some(current) = sys.process((std::process::id() as usize).into()) { - if current.name() == p.name() && probe_existing_listener(postfix).await { - return true; - } - } - } - } - } - if probe_existing_listener(postfix).await { - return true; - } - // if not remove old ipc file, the new ipc creation will fail - // if we remove a ipc file, but the old ipc process is still running, - // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive - if let Err(err) = remove_ipc_socket_via_secure_parent_fd(postfix) { - log::debug!( - "Failed to remove stale ipc socket via secure parent fd: postfix={}, err={}", - postfix, - err - ); - } - false -} - -#[inline] -pub(crate) fn should_scrub_parent_entries_after_check_pid( - should_scrub_parent_entries: bool, - existing_listener_alive: bool, -) -> bool { - should_scrub_parent_entries && !existing_listener_alive -} - -#[cfg(test)] -mod tests { - #[test] - fn test_write_pid_file_rejects_symlink() { - use std::os::unix::fs::symlink; - - let unique = format!( - "rustdesk-ipc-pid-file-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let target = base.join("target_pid"); - std::fs::write(&target, b"origin").unwrap(); - let link = base.join("pid_link"); - symlink(&target, &link).unwrap(); - - let res = super::write_pid_file(&link); - assert!(res.is_err()); - assert_eq!(std::fs::read_to_string(&target).unwrap(), "origin"); - - std::fs::remove_file(&link).ok(); - std::fs::remove_file(&target).ok(); - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_rejects_symlink_parent() { - use std::os::unix::fs::symlink; - - let unique = format!( - "rustdesk-ipc-secure-dir-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - let real_dir = base.join("real"); - let link_dir = base.join("link"); - std::fs::create_dir_all(&real_dir).unwrap(); - symlink(&real_dir, &link_dir).unwrap(); - let ipc_path = link_dir.join("ipc_service"); - let res = - super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "_service"); - assert!(res.is_err()); - std::fs::remove_file(&link_dir).ok(); - std::fs::remove_dir_all(&real_dir).ok(); - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_creates_parent_with_expected_mode() { - use std::os::unix::fs::PermissionsExt; - - let unique = format!( - "rustdesk-ipc-secure-dir-create-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - // Intentionally choose a parent that does not exist to exercise the ENOENT -> mkdir branch. - let parent_dir = base.join("parent"); - assert!(!parent_dir.exists()); - let ipc_path = parent_dir.join("ipc"); - - let res = super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), ""); - // Restrictive umask can make mkdir create a stricter initial mode. In that case - // ensure_secure_ipc_parent_dir repairs it with fchmod and may request a scrub. - res.unwrap(); - - let md = std::fs::metadata(&parent_dir).unwrap(); - assert!(md.is_dir()); - let mode = md.permissions().mode() & 0o777; - assert_eq!(mode, 0o0700); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_scrub_preexisting_ipc_parent_entries_only_removes_target_postfix_artifacts() { - use std::os::unix::ffi::OsStrExt; - - let unique = format!( - "rustdesk-ipc-scrub-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let ipc_file = base.join("ipc_service"); - let ipc_pid_file = base.join("ipc_service.pid"); - let ipc_other_postfix_file = base.join("ipc_uinput_1"); - let keep_file = base.join("keep.txt"); - let keep_dir = base.join("keep_dir"); - - std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); - std::fs::write(&ipc_pid_file, b"1234").unwrap(); - std::fs::write(&ipc_other_postfix_file, b"other-postfix").unwrap(); - std::fs::write(&keep_file, b"keep").unwrap(); - std::fs::create_dir_all(&keep_dir).unwrap(); - - let base_c = std::ffi::CString::new(base.as_os_str().as_bytes().to_vec()).unwrap(); - let base_fd = super::open_ipc_parent_dir_fd(&base_c).unwrap(); - let _base_guard = super::FdGuard(base_fd); - super::scrub_preexisting_ipc_parent_entries(base_fd, &base, "_service").unwrap(); - - assert!(!ipc_file.exists()); - assert!(!ipc_pid_file.exists()); - assert!(ipc_other_postfix_file.exists()); - assert!(keep_file.exists()); - assert!(keep_dir.exists()); - - std::fs::remove_file(&ipc_other_postfix_file).ok(); - std::fs::remove_file(&keep_file).ok(); - std::fs::remove_dir_all(&keep_dir).ok(); - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_scrub_preexisting_ipc_parent_entries_should_bind_to_opened_inode_not_path() { - use std::os::unix::ffi::OsStrExt; - - let unique = format!( - "rustdesk-ipc-scrub-fd-bind-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let trusted_parent = base.join("trusted_parent"); - let trusted_parent_moved = base.join("trusted_parent_moved"); - let attacker_parent = base.join("attacker_parent"); - std::fs::create_dir_all(&trusted_parent).unwrap(); - std::fs::create_dir_all(&attacker_parent).unwrap(); - - let trusted_ipc_file = trusted_parent.join("ipc_service"); - let attacker_ipc_file = attacker_parent.join("ipc_service"); - std::fs::write(&trusted_ipc_file, b"trusted").unwrap(); - std::fs::write(&attacker_ipc_file, b"attacker").unwrap(); - - let trusted_parent_c = - std::ffi::CString::new(trusted_parent.as_os_str().as_bytes().to_vec()).unwrap(); - let trusted_parent_fd = super::open_ipc_parent_dir_fd(&trusted_parent_c).unwrap(); - let _trusted_parent_guard = super::FdGuard(trusted_parent_fd); - - // Swap the path after the trusted inode has been opened. - std::fs::rename(&trusted_parent, &trusted_parent_moved).unwrap(); - std::fs::rename(&attacker_parent, &trusted_parent).unwrap(); - - super::scrub_preexisting_ipc_parent_entries(trusted_parent_fd, &trusted_parent, "_service") - .unwrap(); - - // Expected secure behavior: scrub should target the inode that was opened before path swap. - assert!( - !trusted_parent_moved.join("ipc_service").exists(), - "trusted inode artifact should be removed even after path swap" - ); - assert!( - trusted_parent.join("ipc_service").exists(), - "path-swapped attacker directory should not be scrubbed" - ); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_keeps_service_artifacts_before_liveness_probe() { - use std::os::unix::fs::PermissionsExt; - - let unique = format!( - "rustdesk-ipc-secure-dir-order-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let parent_dir = base.join("service_parent"); - std::fs::create_dir_all(&parent_dir).unwrap(); - // Trigger "had_untrusted_service_parent_mode". - std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o777)).unwrap(); - - let ipc_file = parent_dir.join("ipc_service"); - let ipc_pid_file = parent_dir.join("ipc_service.pid"); - std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); - std::fs::write(&ipc_pid_file, b"1234").unwrap(); - - let res = - super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "_service"); - assert_eq!(res.unwrap(), true); - - // Parent hardening should run first; artifacts should stay until liveness probe completes. - assert!(ipc_file.exists(), "ipc socket marker should be preserved"); - assert!(ipc_pid_file.exists(), "pid marker should be preserved"); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_marks_non_service_mode_repair_for_scrub() { - use std::os::unix::fs::PermissionsExt; - - let unique = format!( - "rustdesk-ipc-nonservice-mode-repair-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let parent_dir = base.join("non_service_parent"); - std::fs::create_dir_all(&parent_dir).unwrap(); - std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o755)).unwrap(); - - let ipc_file = parent_dir.join("ipc"); - std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); - - let res = super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), ""); - assert_eq!(res.unwrap(), true); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_should_scrub_parent_entries_after_check_pid_only_when_requested_and_not_alive() { - assert!(!super::should_scrub_parent_entries_after_check_pid( - false, false - )); - assert!(!super::should_scrub_parent_entries_after_check_pid( - false, true - )); - assert!(super::should_scrub_parent_entries_after_check_pid( - true, false - )); - assert!(!super::should_scrub_parent_entries_after_check_pid( - true, true - )); - } -} 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..2e543c25e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Connect via relay", "Conectar a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), @@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Olvidó su nombre de usuario"), ("Password missed", "Olvidó su contraseña"), ("Wrong credentials", "Credenciales incorrectas"), - ("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"), + ("The verification code is incorrect or has expired", ""), ("Edit Tag", "Editar tag"), ("Forget Password", "Olvidar contraseña"), ("Favorites", "Favoritos"), @@ -302,8 +302,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), - ("Start on boot", "Iniciar al arrancar"), - ("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -326,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Relación"), ("Image Quality", "Calidad de imagen"), ("Scroll Style", "Estilo de desplazamiento"), - ("Show Toolbar", "Mostrar herramientas"), - ("Hide Toolbar", "Ocultar herramientas"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), ("Direct Connection", "Conexión directa"), ("Relay Connection", "Conexión Relay"), ("Secure Connection", "Conexión segura"), @@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Seguridad"), ("Theme", "Tema"), ("Dark Theme", "Tema Oscuro"), - ("Light Theme", "Tema claro"), + ("Light Theme", ""), ("Dark", "Oscuro"), ("Light", "Claro"), ("Follow System", "Tema del sistema"), @@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Dispositivo de entrada de audio"), ("Use IP Whitelisting", "Usar lista de IPs admitidas"), ("Network", "Red"), - ("Pin Toolbar", "Anclar herramientas"), - ("Unpin Toolbar", "Desanclar herramientas"), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), ("Recording", "Grabando"), ("Directory", "Directorio"), ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), - ("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"), + ("Automatically record outgoing sessions", ""), ("Change", "Cambiar"), ("Start session recording", "Comenzar grabación de sesión"), ("Stop session recording", "Detener grabación de sesión"), @@ -368,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN discovery", "Habilitar descubrimiento de LAN"), ("Deny LAN discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), - ("Prompt", "Solicitud"), + ("Prompt", ""), ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), ("Disconnected", "Desconectado"), @@ -616,9 +616,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During service is on", "Mientras el servicio está activo"), ("Capture screen using DirectX", "Capturar pantalla con DirectX"), ("Back", "Atrás"), - ("Apps", "Aplicaciones"), - ("Volume up", "Subir volumen"), - ("Volume down", "Bajar volumen"), + ("Apps", ""), + ("Volume up", "Bajar volumen"), + ("Volume down", "Subir volumen"), ("Power", "Encendido"), ("Telegram bot", "Bot de Telegram"), ("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."), @@ -651,7 +651,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Actualizar portapapeles del cliente"), ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), - ("Accessible devices", "Dispositivos accesibles"), + ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), ("Use D3D rendering", "Usar renderizado D3D"), @@ -689,9 +689,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use WebSocket", "Usar WebSocket"), ("Trackpad speed", "Velocidad de trackpad"), ("Default trackpad speed", "Velocidad predeterminada de trackpad"), - ("Numeric one-time password", "Contraseña numérica de un solo uso"), - ("Enable IPv6 P2P connection", "Habilitar conexión IPv6 P2P"), - ("Enable UDP hole punching", "Habilitar perforación de agujero UDP"), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), ("View camera", "Ver cámara"), ("Enable camera", "Habilitar cámara"), ("No cameras", "No hay cámaras"), @@ -708,8 +708,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), ("Supported only in the installed version.", "Soportado solo en la versión instalada."), ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), - ("Preparing for installation ...", "Preparando instlación..."), - ("Show my cursor", "Mostrar mi cursor"), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), ("Scale custom", "Escala personalizada"), ("Custom scale slider", "Control deslizante de escala personalizada"), ("Decrease", "Disminuir"), @@ -721,29 +721,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Mostrar joystick virtual"), ("Edit note", "Editar nota"), ("Alias", ""), - ("ScrollEdge", "Desplazamiento de pantalla"), - ("Allow insecure TLS fallback", "Permitir conexión TLS insegura de respaldo"), - ("allow-insecure-tls-fallback-tip", "De forma predeterminada, RustDesk verifica el certificado de servidor para protocolos que usen TLS.\nCon esta opción habilitada, Rustdesk volverá al paso de omisión de verificación y procederá en caso de fallo de verificación."), - ("Disable UDP", "Inhabilitar UDP"), - ("disable-udp-tip", "Controla si se usa TCP solamente.\nCuando esta opción está activa, RustDesk no usará más el puerto UDP 21116, en su lugar se usará el TCP 21116."), - ("server-oss-not-support-tip", "NOTA: El servidor RustDesk OSS no incluye esta característica."), - ("input note here", "Introducir nota aquí"), - ("note-at-conn-end-tip", "Pedir nota al finalizar la conexión"), - ("Show terminal extra keys", "Mostrar teclas extra del terminal"), - ("Relative mouse mode", "Modo de ratón relativo"), - ("rel-mouse-not-supported-peer-tip", "El modo relativo de ratón no está soportado por el par."), - ("rel-mouse-not-ready-tip", "El modo relativo de ratón aún no está preparado. Por favor, inténtalo de nuevo."), - ("rel-mouse-lock-failed-tip", "Ha fallado el bloqueo del cursor. El modo relativo del ratón ha sido inhabilitado."), - ("rel-mouse-exit-{}-tip", "Pulsa {} para salir."), - ("rel-mouse-permission-lost-tip", "Permiso de teclado revocado. El modo relativo del ratón ha sido inhabilitado."), - ("Changelog", "Registro de cambios"), - ("keep-awake-during-outgoing-sessions-label", "Mantener la pantalla activa durante sesiones salientes"), - ("keep-awake-during-incoming-sessions-label", "Mantener la pantalla activa durante sesiones entrantes"), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuar con {}"), - ("Display Name", "Nombre de pantalla"), - ("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", ""), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].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..7f9b3299e 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -743,7 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Kijelző név"), ("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", ""), + ("Enable privacy mode", ""), ].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..20caca0a7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -739,11 +739,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "更新履歴"), ("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"), ("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"), - ("Continue with {}", "{} で続行する"), + ("Continue with {}", "{}で続行する"), ("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/ko.rs b/src/lang/ko.rs index 350d570b0..7b3ffd98e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -743,7 +743,6 @@ 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", ""), + ("Enable privacy mode", ""), ].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..833c947cf 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -743,7 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Naam Weergeven"), ("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", ""), + ("Enable privacy mode", ""), ].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/platform/linux.rs b/src/platform/linux.rs index 9a4bb37ec..7157da760 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -29,12 +29,6 @@ use wallpaper; pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; -#[derive(Clone, Debug)] -struct ActiveUserLookupCache { - uid: String, - username: String, -} - const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; @@ -56,8 +50,6 @@ lazy_static::lazy_static! { } } }; - static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex> = - std::sync::Mutex::new(None); // https://github.com/rustdesk/rustdesk/issues/13705 // Check if `sudo -E` actually preserves environment. // @@ -90,27 +82,6 @@ lazy_static::lazy_static! { }; } -#[inline] -fn update_active_user_lookup_cache(desktop: &Desktop) { - if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() { - if desktop.uid.is_empty() || desktop.username.is_empty() { - *cache = None; - } else { - *cache = Some(ActiveUserLookupCache { - uid: desktop.uid.clone(), - username: desktop.username.clone(), - }); - } - } -} - -#[inline] -fn get_active_user_id_name_from_cache() -> Option<(String, String)> { - let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?; - let entry = cache.as_ref()?; - Some((entry.uid.clone(), entry.username.clone())) -} - thread_local! { // XDO context - created via libxdo-sys (which uses dynamic loading stub). // If libxdo is not available, xdo will be null and xdo-based functions become no-ops. @@ -818,7 +789,6 @@ pub fn start_os_service() { let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { desktop.refresh(); - update_active_user_lookup_cache(&desktop); // Duplicate logic here with should_start_server // Login wayland will try to start a headless --server. @@ -891,29 +861,13 @@ pub fn start_os_service() { } #[inline] -/// Returns the cached active `(uid, username)` snapshot when available. -/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_user_id_name() -> (String, String) { - if let Some(id_name) = get_active_user_id_name_from_cache() { - return id_name; - } let vec_id_name = get_values_of_seat0(&[1, 2]); (vec_id_name[0].clone(), vec_id_name[1].clone()) } #[inline] -/// Returns the cached active uid when available. -/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_userid() -> String { - if let Some((uid, _)) = get_active_user_id_name_from_cache() { - return uid; - } - get_values_of_seat0(&[1])[0].clone() -} - -#[inline] -/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache. -pub fn get_active_userid_fresh() -> String { get_values_of_seat0(&[1])[0].clone() } @@ -968,12 +922,7 @@ fn _get_display_manager() -> String { } #[inline] -/// Returns the cached active username when available. -/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_username() -> String { - if let Some((_, username)) = get_active_user_id_name_from_cache() { - return username; - } get_values_of_seat0(&[2])[0].clone() } diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index 0a512939b..03f1f6250 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -2,7 +2,7 @@ use super::{linux::*, ResultType}; use crate::client::{ LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, - LOGIN_MSG_DESKTOP_XSESSION_FAILED, LOGIN_MSG_PASSWORD_WRONG, + LOGIN_MSG_DESKTOP_XSESSION_FAILED, }; use hbb_common::{ allow_err, bail, log, @@ -94,49 +94,6 @@ fn detect_headless() -> Option<&'static str> { None } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum XSessionStartErrorKind { - Auth, - Env, -} - -const XSESSION_AUTH_FAILURE_DETAIL: &str = "authentication failed"; - -#[derive(Debug)] -struct XSessionStartError { - kind: XSessionStartErrorKind, - detail: String, -} - -impl XSessionStartError { - fn auth(detail: String) -> Self { - Self { - kind: XSessionStartErrorKind::Auth, - detail, - } - } - - fn env(detail: String) -> Self { - Self { - kind: XSessionStartErrorKind::Env, - detail, - } - } -} - -impl std::fmt::Display for XSessionStartError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.detail) - } -} - -fn map_xsession_start_error_to_login_msg(kind: XSessionStartErrorKind) -> &'static str { - match kind { - XSessionStartErrorKind::Auth => LOGIN_MSG_PASSWORD_WRONG, - XSessionStartErrorKind::Env => LOGIN_MSG_DESKTOP_XSESSION_FAILED, - } -} - pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { debug_assert!(crate::is_server()); if _username.is_empty() { @@ -179,21 +136,14 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { } } Err(e) => { - match e.kind { - XSessionStartErrorKind::Auth => { - log::warn!("Failed to authenticate xsession user {}", e); - } - XSessionStartErrorKind::Env => { - log::error!("Failed to start xsession {}", e); - } - } - map_xsession_start_error_to_login_msg(e.kind).to_owned() + log::error!("Failed to start xsession {}", e); + LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned() } } } } -fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), XSessionStartError> { +fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> { let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); if let Some(desktop_manager) = &mut (*desktop_manager) { if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() { @@ -211,9 +161,7 @@ fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), desktop_manager.is_running(), )) } else { - Err(XSessionStartError::env( - crate::client::LOGIN_MSG_DESKTOP_NOT_INITED.to_owned(), - )) + bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED); } } @@ -299,15 +247,10 @@ impl DesktopManager { self.is_child_running.load(Ordering::SeqCst) } - fn try_start_x_session( - &mut self, - username: &str, - password: &str, - ) -> Result<(), XSessionStartError> { + fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> { match get_user_by_name(username) { Some(userinfo) => { - let mut client = pam::Client::with_password(&pam_get_service_name()) - .map_err(|e| XSessionStartError::env(format!("failed to init pam client, {}", e)))?; + let mut client = pam::Client::with_password(&pam_get_service_name())?; client .conversation_mut() .set_credentials(username, password); @@ -324,24 +267,17 @@ impl DesktopManager { Ok(()) } Err(e) => { - Err(XSessionStartError::env(format!( - "failed to start x session, {}", - e - ))) + bail!("failed to start x session, {}", e); } } } - Err(_e) => { - Err(XSessionStartError::auth( - XSESSION_AUTH_FAILURE_DETAIL.to_owned(), - )) + Err(e) => { + bail!("failed to check user pass for {}, {}", username, e); } } } None => { - Err(XSessionStartError::auth( - XSESSION_AUTH_FAILURE_DETAIL.to_owned(), - )) + bail!("failed to get userinfo of {}", username); } } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 1dc4a788a..4c09bbe9f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -73,19 +73,10 @@ use winapi::{ }; use windows::Win32::{ Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE}, - Security::{ - GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser, - WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER, - }, System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, }, - System::Threading::{ - OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken, - QueryFullProcessImageNameW as WinQueryFullProcessImageNameW, - PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION, - }, }; use windows_service::{ define_windows_service, @@ -97,14 +88,6 @@ use windows_service::{ }; use winreg::{enums::*, RegKey}; -mod acl; -pub(crate) use acl::current_process_user_sid_string; -pub use acl::{ - set_path_permission, set_path_permission_for_portable_service_shmem_dir, - set_path_permission_for_portable_service_shmem_file, - validate_path_for_portable_service_shmem_dir, -}; - pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window pub const EXPLORER_EXE: &'static str = "explorer.exe"; pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; @@ -582,56 +565,6 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD { unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } } -#[inline] -fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option { - let share_rdp_enabled = is_share_rdp(); - if get_available_sessions(false) - .iter() - .any(|e| e.sid == session_id) - { - return Some(session_id); - } - let current_active_session = - unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) }; - if current_active_session == u32::MAX { - None - } else { - Some(current_active_session) - } -} - -#[inline] -fn authorize_service_scoped_ipc_connection( - stream: &ipc::Connection, - expected_active_session_id: Option, -) -> bool { - let (authorized, peer_pid, peer_session_id, peer_is_system) = - stream.service_authorization_status_for_session(expected_active_session_id); - if !authorized { - ipc::log_rejected_windows_ipc_connection( - crate::POSTFIX_SERVICE, - peer_pid, - peer_session_id, - expected_active_session_id, - peer_is_system, - None, - ); - return false; - } - if let Err(err) = - ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE) - { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", - crate::POSTFIX_SERVICE, - peer_pid, - err - ); - return false; - } - true -} - extern "system" { fn BlockInput(v: BOOL) -> BOOL; } @@ -698,15 +631,6 @@ async fn run_service(_arguments: Vec) -> ResultType<()> { Ok(res) => match res { Some(Ok(stream)) => { let mut stream = ipc::Connection::new(stream); - // Keep IPC authorization consistent with the session we are currently serving. - // Recompute expected session right before authorization to avoid using a stale - // session_id after awaiting incoming.next(). - let expected_active_session_id = - resolve_expected_active_session_id_for_service(session_id); - if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id) - { - continue; - } if let Ok(Some(data)) = stream.next_timeout(1000).await { match data { ipc::Data::Close => { @@ -1217,22 +1141,6 @@ pub fn get_active_user_home() -> Option { None } -#[cfg(not(feature = "flutter"))] -#[inline] -pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> { - // Keep parity with history for now: derive LocalAppData from user profile path. - // If users report redirected/non-standard LocalAppData issues, switch to: - // `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution. - let user_dir = hbb_common::directories_next::UserDirs::new()?; - let dir = user_dir - .home_dir() - .join("AppData") - .join("Local") - .join("rustdesk-sciter"); - let dst = dir.join("rustdesk.exe"); - Some((dir, dst)) -} - pub fn is_prelogin() -> bool { let Some(username) = get_current_session_username() else { return false; @@ -2419,33 +2327,16 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst is_run_as_system, crate::username(), ); - let mut arg_elevate = if is_setup { + let arg_elevate = if is_setup { "--noinstall --elevate" } else { "--elevate" - } - .to_owned(); - let mut arg_run_as_system = if is_setup { + }; + let arg_run_as_system = if is_setup { "--noinstall --run-as-system" } else { "--run-as-system" - } - .to_owned(); - let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args(); - if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() { - log::error!("Invalid portable service shared memory argument, aborting elevation flow"); - // This is a malformed bootstrap argument in a privilege-sensitive path. - // Keep fail-closed process termination here to avoid continuing elevation - // with inconsistent shared-memory contract. - std::process::exit(1); - } - if let Some(shmem_name) = shmem_name_from_args { - let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name); - arg_elevate.push(' '); - arg_elevate.push_str(&shmem_arg); - arg_run_as_system.push(' '); - arg_run_as_system.push_str(&shmem_arg); - } + }; if is_root() { if is_run_as_system { log::info!("run portable service"); @@ -2456,7 +2347,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst Ok(elevated) => { if elevated { if !is_run_as_system { - if run_as_system(arg_run_as_system.as_str()).is_ok() { + if run_as_system(arg_run_as_system).is_ok() { std::process::exit(0); } else { log::error!( @@ -2467,7 +2358,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst } } else { if !is_elevate { - if let Ok(true) = elevate(arg_elevate.as_str()) { + if let Ok(true) = elevate(arg_elevate) { std::process::exit(0); } else { log::error!("Failed to elevate, error {}", io::Error::last_os_error()); @@ -2525,115 +2416,6 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } -#[inline] -unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType> { - let mut token_user_size = 0u32; - let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size); - match get_info_result { - Ok(()) => { - if token_user_size == 0 { - bail!( - "Failed to get {} token user size: unexpected zero buffer size", - subject - ); - } - } - Err(e) => { - // Allow expected size-probe failures if Windows still returns required size. - let is_insufficient_buffer = - e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32); - let is_bad_length = - e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32); - if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 { - bail!("Failed to get {} token user size: {}", subject, e); - } - } - } - - let mut buffer = vec![0u8; token_user_size as usize]; - WinGetTokenInformation( - token, - TokenUser, - Some(buffer.as_mut_ptr() as *mut core::ffi::c_void), - token_user_size, - &mut token_user_size, - ) - .map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?; - - let min_size = std::mem::size_of::(); - if buffer.len() < min_size { - bail!( - "Failed to parse {} token user: buffer too small (got {}, need >= {})", - subject, - buffer.len(), - min_size - ); - } - Ok(buffer) -} - -/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process. -/// -/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18). -/// -/// TODO: After a few releases of real-world validation, consider replacing -/// the legacy `is_local_system()` with this implementation. -pub fn is_process_running_as_system(process_id: DWORD) -> ResultType { - unsafe { - let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) - .map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?; - - let mut token = WinHANDLE::default(); - let result = (|| -> ResultType { - WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token) - .map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?; - - let token_subject = format!("process {}", process_id); - let buffer = read_token_user_buffer(token, token_subject.as_str())?; - let token_user: TOKEN_USER = - std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER); - Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool()) - })(); - - if !token.is_invalid() { - let _ = WinCloseHandle(token); - } - let _ = WinCloseHandle(process); - result - } -} - -pub fn get_process_executable_path(process_id: DWORD) -> ResultType { - const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024; - unsafe { - let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) - .map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?; - - let result = (|| -> ResultType { - let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN]; - let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32; - WinQueryFullProcessImageNameW( - process, - windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0), - windows::core::PWSTR(buffer.as_mut_ptr()), - &mut length, - ) - .map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?; - if length == 0 { - bail!( - "Failed to query process {} image path: empty result", - process_id - ); - } - buffer.truncate(length as usize); - Ok(PathBuf::from(OsString::from_wide(&buffer))) - })(); - - let _ = WinCloseHandle(process); - result - } -} - pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -2926,6 +2708,16 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> return Ok(()); } +pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> { + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg(format!("*S-1-1-0:(OI)(CI){}", permission)) + .arg("/T") + .spawn()?; + Ok(()) +} + #[inline] fn str_to_device_name(name: &str) -> [u16; 32] { let mut device_name: Vec = wide_string(name); @@ -4489,87 +4281,6 @@ pub(super) fn get_pids_with_first_arg_by_wmic, S2: AsRef>( #[cfg(test)] mod tests { use super::*; - - // Test-only reusable Win32 HANDLE RAII helper. - // If a future non-test path needs the same pattern, move it out of this test module. - // - // This struct is similar to `hbb_common::platform::windows::RAIIHandle`, - // but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate. - struct HandleGuard(WinHANDLE); - - impl HandleGuard { - #[inline] - fn new(handle: WinHANDLE) -> Self { - Self(handle) - } - - #[inline] - fn get(&self) -> WinHANDLE { - self.0 - } - } - - impl Drop for HandleGuard { - fn drop(&mut self) { - unsafe { - if !self.0.is_invalid() { - let _ = WinCloseHandle(self.0); - } - } - } - } - - #[test] - fn test_is_process_running_as_system_invalid_pid_errors() { - assert!(is_process_running_as_system(u32::MAX).is_err()); - } - - #[test] - fn test_is_process_running_as_system_matches_current_process_token_user() { - let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() }; - let actual = is_process_running_as_system(pid).unwrap(); - - let expected = unsafe { - // Keep this test consistent: use only the `windows` crate APIs/types. - let process = HandleGuard::new( - WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid) - .expect("WinOpenProcess should succeed for current process"), - ); - let mut token = WinHANDLE::default(); - WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token) - .expect("WinOpenProcessToken should succeed for current process"); - let token = HandleGuard::new(token); - - let mut token_user_size = 0u32; - let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size); - assert_ne!(token_user_size, 0, "TokenUser size should be non-zero"); - - let mut buffer = vec![0u8; token_user_size as usize]; - WinGetTokenInformation( - token.get(), - TokenUser, - Some(buffer.as_mut_ptr() as *mut core::ffi::c_void), - token_user_size, - &mut token_user_size, - ) - .expect("WinGetTokenInformation(TokenUser) should succeed for current process"); - - let min_size = std::mem::size_of::(); - assert!( - buffer.len() >= min_size, - "TokenUser buffer too small (got {}, need >= {})", - buffer.len(), - min_size - ); - let token_user: TOKEN_USER = - std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER); - let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool(); - expected - }; - - assert_eq!(actual, expected); - } - #[test] fn test_uninstall_cert() { println!("uninstall driver certs: {:?}", cert::uninstall_cert()); diff --git a/src/platform/windows/acl.rs b/src/platform/windows/acl.rs deleted file mode 100644 index 682e66fed..000000000 --- a/src/platform/windows/acl.rs +++ /dev/null @@ -1,903 +0,0 @@ -// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary - -use super::{read_token_user_buffer, wide_string, ResultType}; -use hbb_common::{anyhow::anyhow, bail}; -use std::{ - fs, io, - os::windows::{ffi::OsStrExt, fs::MetadataExt}, - path::Path, -}; -use windows::{ - core::{PCWSTR, PWSTR}, - Win32::{ - Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL}, - Security::{ - Authorization::{ - ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW, - SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS, - SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W, - }, - ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE, - OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, - TOKEN_QUERY, TOKEN_USER, - }, - Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE}, - System::Threading::{GetCurrentProcess, OpenProcessToken}, - }, -}; - -const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400; - -#[inline] -fn is_reparse_point(metadata: &fs::Metadata) -> bool { - (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0 -} - -fn apply_grant_sid_allow_ace_to_path( - path: &Path, - sid_ptr: *mut std::ffi::c_void, - access_mask: u32, - is_group: bool, - is_dir: bool, -) -> ResultType<()> { - // Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW. - // https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c-- - let mut old_dacl: *mut ACL = std::ptr::null_mut(); - let mut security_descriptor = PSECURITY_DESCRIPTOR::default(); - let path_utf16: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let get_named_result = unsafe { - GetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(&mut old_dacl), - None, - &mut security_descriptor, - ) - }; - if get_named_result.0 != 0 { - bail!( - "GetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - get_named_result.0 - ); - } - let _sd_guard = LocalAllocGuard(security_descriptor.0); - - let inherit_flags = if is_dir { - ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0) - } else { - NO_INHERITANCE - }; - let explicit_access = [make_sid_trustee_entry( - sid_ptr, - access_mask, - inherit_flags, - is_group, - )]; - let old_acl_option = if old_dacl.is_null() { - None - } else { - Some(old_dacl as *const ACL) - }; - let mut new_acl: *mut ACL = std::ptr::null_mut(); - let set_entries_result = unsafe { - SetEntriesInAclW( - Some(explicit_access.as_slice()), - old_acl_option, - &mut new_acl, - ) - }; - if set_entries_result.0 != 0 { - bail!( - "SetEntriesInAclW failed for '{}': win32_error={}", - path.display(), - set_entries_result.0 - ); - } - if new_acl.is_null() { - bail!( - "SetEntriesInAclW returned null ACL for '{}'", - path.display() - ); - } - let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void); - - let set_named_result = unsafe { - SetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(new_acl), - None, - ) - }; - if set_named_result.0 != 0 { - bail!( - "SetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - set_named_result.0 - ); - } - Ok(()) -} - -/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be -/// readable/executable across user contexts. -/// -/// `access_mask` is the Win32 file access mask to grant recursively. -pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> { - let metadata = fs::symlink_metadata(dir).map_err(|e| { - anyhow!( - "Failed to inspect ACL target directory '{}': {}", - dir.display(), - e - ) - })?; - if is_reparse_point(&metadata) { - bail!( - "ACL target directory is a reparse point and is rejected: '{}'", - dir.display() - ); - } - if !metadata.file_type().is_dir() { - bail!("ACL target is not a directory: '{}'", dir.display()); - } - - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?; - let mut stack = vec![dir.to_path_buf()]; - while let Some(path) = stack.pop() { - let metadata = fs::symlink_metadata(&path) - .map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?; - if is_reparse_point(&metadata) { - continue; - } - let is_dir = metadata.file_type().is_dir(); - apply_grant_sid_allow_ace_to_path( - &path, - everyone_sid.as_sid_ptr(), - access_mask, - true, - is_dir, - )?; - if !is_dir { - continue; - } - for entry in fs::read_dir(&path) - .map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))? - { - let entry = entry.map_err(|e| { - anyhow!( - "Failed to read ACL target dir entry under '{}': {}", - path.display(), - e - ) - })?; - stack.push(entry.path()); - } - } - Ok(()) -} - -/// Returns the current process user SID as a standard SID string -/// (for example: `S-1-5-18`). -/// -/// Source: -/// - Official SID-to-string API (`ConvertSidToStringSidW`): -/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw -pub(crate) fn current_process_user_sid_string() -> ResultType { - let mut token = HANDLE::default(); - let result = (|| -> ResultType { - unsafe { - OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) - .map_err(|e| anyhow!("Failed to open current process token: {}", e))?; - } - - let buffer = unsafe { read_token_user_buffer(token, "current process")? }; - let token_user: TOKEN_USER = - unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) }; - if token_user.User.Sid.0.is_null() { - bail!("Token SID is null"); - } - - let mut sid_string_ptr = PWSTR::null(); - unsafe { - ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| { - anyhow!( - "ConvertSidToStringSidW failed for current process token SID: {}", - e - ) - })?; - } - if sid_string_ptr.is_null() { - bail!("ConvertSidToStringSidW returned null SID string pointer"); - } - let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void); - unsafe { - sid_string_ptr - .to_string() - .map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e)) - } - })(); - - if !token.is_invalid() { - unsafe { - let _ = CloseHandle(token); - } - } - result -} - -/// Hardens ACLs for portable-service shared-memory path (directory or file). -/// -/// Why: -/// - Shared memory used by portable service carries runtime control/data and must not inherit -/// broad/default ACLs. -/// - We explicitly grant only trusted principals and remove broad groups to reduce local -/// privilege-boundary bypass risk. -/// -/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`): -/// - common (directory + file): -/// - `S-1-5-18` (LocalSystem): full control -/// - `S-1-5-32-544` (Built-in Administrators): full control -/// - `current_process_user_sid_string()` result: full control -/// - directory (`portable_service_shmem` parent): -/// - keep `Authenticated Users` directory-level write so other local accounts can -/// create their own runtime shmem files after account switching -/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself; -/// it is intentionally not inherited by children. -/// Reference: -/// - File access rights: -/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants -/// - ACE inheritance rules: -/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules -/// - remove `Everyone` and `Users` grants -/// - file (`shared_memory*` flink): -/// - remove broad grants: -/// - `S-1-1-0` (Everyone) -/// - `S-1-5-11` (Authenticated Users) -/// - `S-1-5-32-545` (Users) -/// -/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids -pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> { - set_path_permission_for_portable_service_shmem_impl(path, true) -} - -#[inline] -pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> { - validate_portable_service_shmem_dir_target(path) -} - -#[inline] -pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> { - set_path_permission_for_portable_service_shmem_impl(path, false) -} - -#[derive(Debug)] -pub(super) struct LocalAllocGuard(*mut std::ffi::c_void); - -impl LocalAllocGuard { - #[inline] - pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void { - self.0 - } -} - -impl Drop for LocalAllocGuard { - fn drop(&mut self) { - if self.0.is_null() { - return; - } - // Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW / - // ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed. - unsafe { - let _ = LocalFree(Some(HLOCAL(self.0))); - } - } -} - -#[inline] -pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType { - let sid_utf16 = wide_string(sid); - let mut sid_ptr = PSID::default(); - unsafe { - ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr) - .map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?; - } - if sid_ptr.0.is_null() { - bail!("ConvertStringSidToSidW returned null SID for '{}'", sid); - } - Ok(LocalAllocGuard(sid_ptr.0)) -} - -#[inline] -fn make_sid_trustee_entry( - sid_ptr: *mut std::ffi::c_void, - access_permissions: u32, - inheritance: ACE_FLAGS, - is_group: bool, -) -> EXPLICIT_ACCESS_W { - // `is_group` is explicitly provided by the caller from the concrete SID semantic - // (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user). - EXPLICIT_ACCESS_W { - grfAccessPermissions: access_permissions, - grfAccessMode: SET_ACCESS, - grfInheritance: inheritance, - Trustee: TRUSTEE_W { - pMultipleTrustee: std::ptr::null_mut(), - MultipleTrusteeOperation: Default::default(), - TrusteeForm: TRUSTEE_IS_SID, - TrusteeType: if is_group { - TRUSTEE_IS_GROUP - } else { - TRUSTEE_IS_USER - }, - // SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID. - ptstrName: PWSTR::from_raw(sid_ptr as *mut u16), - }, - } -} - -fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> { - let metadata = fs::symlink_metadata(path).map_err(|e| { - anyhow!( - "Failed to inspect portable service shared-memory ACL directory '{}': {}", - path.display(), - e - ) - })?; - if is_reparse_point(&metadata) { - bail!( - "Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'", - path.display() - ); - } - if !metadata.file_type().is_dir() { - bail!( - "Portable service shared-memory ACL target is not a directory: '{}'", - path.display() - ); - } - Ok(()) -} - -fn set_path_permission_for_portable_service_shmem_impl( - path: &Path, - expect_dir: bool, -) -> ResultType<()> { - if expect_dir { - validate_portable_service_shmem_dir_target(path)?; - } else { - let metadata_result = fs::symlink_metadata(path); - match metadata_result { - Ok(metadata) => { - if metadata.file_type().is_dir() { - bail!( - "Portable service shared-memory ACL target is a directory, expected file-like path: '{}'", - path.display() - ); - } - if is_reparse_point(&metadata) { - bail!( - "Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'", - path.display() - ); - } - } - Err(e) - if e.kind() == io::ErrorKind::NotFound - || e.kind() == io::ErrorKind::PermissionDenied => - { - // Keep going and let Win32 ACL APIs return the final OS error. - // `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into - // a false "not found" signal under restricted directory ACLs. - } - Err(e) => { - bail!( - "Failed to inspect portable service shared-memory ACL target '{}': {}", - path.display(), - e - ); - } - } - } - - let user_sid = current_process_user_sid_string()?; - let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?; - let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?; - let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?; - let authenticated_users_sid = if expect_dir { - Some(sid_string_to_local_alloc_guard("S-1-5-11")?) - } else { - None - }; - - let inherit_flags = if expect_dir { - ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0) - } else { - NO_INHERITANCE - }; - let mut entries = vec![ - make_sid_trustee_entry( - local_system_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0, - inherit_flags, - false, - ), - make_sid_trustee_entry( - administrators_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0, - inherit_flags, - true, - ), - make_sid_trustee_entry( - current_user_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0, - inherit_flags, - false, - ), - ]; - if let Some(auth_sid) = authenticated_users_sid.as_ref() { - // Keep the shared parent directory multi-user writable at directory level. - entries.push(make_sid_trustee_entry( - auth_sid.as_sid_ptr(), - FILE_GENERIC_WRITE.0, - NO_INHERITANCE, - true, - )); - } - - // Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected. - // This avoids carrying over broad legacy ACEs from inherited/default ACLs. - // Reference: - // - SetEntriesInAclW: - // https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw - // - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION): - // https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow - let mut new_acl: *mut ACL = std::ptr::null_mut(); - let set_entries_result = - unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) }; - if set_entries_result.0 != 0 { - bail!( - "SetEntriesInAclW failed for '{}': win32_error={}", - path.display(), - set_entries_result.0 - ); - } - if new_acl.is_null() { - bail!( - "SetEntriesInAclW returned null ACL for '{}'", - path.display() - ); - } - let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void); - - let path_utf16: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; - let set_named_result = unsafe { - SetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - security_info, - None, - None, - Some(new_acl), - None, - ) - }; - if set_named_result.0 != 0 { - bail!( - "SetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - set_named_result.0 - ); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::{ - current_process_user_sid_string, set_path_permission, - set_path_permission_for_portable_service_shmem_dir, - set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard, - LocalAllocGuard, ResultType, - }; - use hbb_common::bail; - use std::{ - fs, - os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file}, - path::{Path, PathBuf}, - }; - use windows::{ - core::PCWSTR, - Win32::{ - Security::{ - AclSizeInformation, - Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT}, - EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl, - ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION, - DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED, - }, - Storage::FileSystem::{ - FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, - }, - }, - }; - - const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0; - - fn unique_acl_test_path(prefix: &str) -> PathBuf { - std::env::temp_dir().join(format!( - "rustdesk_acl_{}_{}_{}", - prefix, - std::process::id(), - hbb_common::rand::random::() - )) - } - - fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool { - match symlink_dir(target, link) { - Ok(()) => true, - Err(err) => { - eprintln!( - "skip {}: failed to create directory reparse point (symlink): {}", - test_name, err - ); - false - } - } - } - - fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool { - match symlink_file(target, link) { - Ok(()) => true, - Err(err) => { - eprintln!( - "skip {}: failed to create file reparse point (symlink): {}", - test_name, err - ); - false - } - } - } - - fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> { - let mut dacl: *mut ACL = std::ptr::null_mut(); - let mut sd = PSECURITY_DESCRIPTOR::default(); - let path_utf16: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let result = unsafe { - GetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(&mut dacl), - None, - &mut sd, - ) - }; - if result.0 != 0 { - bail!( - "GetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - result.0 - ); - } - if dacl.is_null() || sd.0.is_null() { - bail!("DACL/security descriptor missing for '{}'", path.display()); - } - Ok((dacl, LocalAllocGuard(sd.0))) - } - - fn has_allow_ace_with_mask( - dacl: *const ACL, - sid_ptr: *mut std::ffi::c_void, - mask: u32, - ) -> bool { - let mut info = ACL_SIZE_INFORMATION::default(); - if unsafe { - GetAclInformation( - dacl, - &mut info as *mut _ as *mut std::ffi::c_void, - std::mem::size_of::() as u32, - AclSizeInformation, - ) - } - .is_err() - { - return false; - } - for index in 0..info.AceCount { - let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); - if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() { - continue; - } - let header = unsafe { &*(ace_ptr as *const ACE_HEADER) }; - if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 { - continue; - } - let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) }; - let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void); - if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok() - && (allowed.Mask & mask) == mask - { - return true; - } - } - false - } - - fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool { - has_allow_ace_with_mask(dacl, sid_ptr, 0) - } - - fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool { - let mut control: u16 = 0; - let mut revision: u32 = 0; - if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() { - return false; - } - (control & SE_DACL_PROTECTED.0) != 0 - } - - #[test] - fn test_portable_service_shmem_dir_acl_policy() { - let dir = unique_acl_test_path("dir"); - fs::create_dir_all(&dir).unwrap(); - set_path_permission_for_portable_service_shmem_dir(&dir).unwrap(); - - let (dacl, sd_guard) = get_file_dacl(&dir).unwrap(); - let current_user_sid = - sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap(); - let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap(); - let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap(); - let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap(); - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); - let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap(); - - assert!(has_allow_ace_with_mask( - dacl, - system_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - admin_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - current_user_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - auth_users_sid.as_sid_ptr(), - FILE_GENERIC_WRITE.0 - )); - assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr())); - assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr())); - assert!(is_dacl_protected(PSECURITY_DESCRIPTOR( - sd_guard.as_sid_ptr() - ))); - - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_portable_service_shmem_file_acl_policy() { - let dir = unique_acl_test_path("file"); - fs::create_dir_all(&dir).unwrap(); - let file = dir.join("shared_memory_portable_service_test"); - fs::write(&file, b"x").unwrap(); - set_path_permission_for_portable_service_shmem_file(&file).unwrap(); - - let (dacl, sd_guard) = get_file_dacl(&file).unwrap(); - let current_user_sid = - sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap(); - let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap(); - let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap(); - let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap(); - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); - let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap(); - - assert!(has_allow_ace_with_mask( - dacl, - system_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - admin_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - current_user_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(!has_any_allow_ace_for_sid( - dacl, - auth_users_sid.as_sid_ptr() - )); - assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr())); - assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr())); - assert!(is_dacl_protected(PSECURITY_DESCRIPTOR( - sd_guard.as_sid_ptr() - ))); - - let _ = fs::remove_file(&file); - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_set_path_permission_rx_applies_recursively() { - let root = unique_acl_test_path("set_path_permission"); - let child_dir = root.join("child"); - let child_file = child_dir.join("helper.exe"); - fs::create_dir_all(&child_dir).unwrap(); - fs::write(&child_file, b"x").unwrap(); - - if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) { - let text = err.to_string(); - let _ = fs::remove_file(&child_file); - let _ = fs::remove_dir_all(&root); - if text.contains("win32_error=5") || text.contains("Access is denied") { - eprintln!( - "skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}", - text - ); - return; - } - panic!("set_path_permission failed unexpectedly: {}", text); - } - - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); - let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0; - for target in [&root, &child_dir, &child_file] { - let (dacl, _sd_guard) = get_file_dacl(target).unwrap(); - assert!( - has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask), - "Everyone RX grant missing on '{}'", - target.display() - ); - } - - let _ = fs::remove_file(&child_file); - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn test_portable_service_shmem_dir_acl_rejects_file_target() { - let dir = unique_acl_test_path("dir_target_file"); - fs::create_dir_all(&dir).unwrap(); - let file = dir.join("target.txt"); - fs::write(&file, b"x").unwrap(); - let result = set_path_permission_for_portable_service_shmem_dir(&file); - assert!(result.is_err()); - let _ = fs::remove_file(&file); - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_portable_service_shmem_file_acl_rejects_dir_target() { - let dir = unique_acl_test_path("file_target_dir"); - fs::create_dir_all(&dir).unwrap(); - let result = set_path_permission_for_portable_service_shmem_file(&dir); - assert!(result.is_err()); - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_portable_service_shmem_file_acl_rejects_missing_target() { - let path = unique_acl_test_path("missing").join("shared_memory_missing"); - let result = set_path_permission_for_portable_service_shmem_file(&path); - assert!(result.is_err()); - } - - #[test] - fn test_set_path_permission_rejects_reparse_entrypoint() { - let root = unique_acl_test_path("reparse_entry"); - let real_dir = root.join("real"); - let link_dir = root.join("link"); - fs::create_dir_all(&real_dir).unwrap(); - if !try_create_dir_reparse_point( - &real_dir, - &link_dir, - "test_set_path_permission_rejects_reparse_entrypoint", - ) { - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - return; - } - - let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0); - let text = result.err().map(|e| e.to_string()).unwrap_or_default(); - assert!( - text.contains("reparse point"), - "expected reparse-point rejection, got '{}'", - text - ); - - let _ = fs::remove_dir(&link_dir); - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn test_portable_service_shmem_dir_acl_rejects_reparse_target() { - let root = unique_acl_test_path("reparse_shmem_dir"); - let real_dir = root.join("real"); - let link_dir = root.join("link"); - fs::create_dir_all(&real_dir).unwrap(); - if !try_create_dir_reparse_point( - &real_dir, - &link_dir, - "test_portable_service_shmem_dir_acl_rejects_reparse_target", - ) { - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - return; - } - - let result = set_path_permission_for_portable_service_shmem_dir(&link_dir); - let text = result.err().map(|e| e.to_string()).unwrap_or_default(); - assert!( - text.contains("reparse point"), - "expected reparse-point rejection, got '{}'", - text - ); - - let _ = fs::remove_dir(&link_dir); - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn test_portable_service_shmem_file_acl_rejects_reparse_target() { - let root = unique_acl_test_path("reparse_shmem_file"); - let real_file = root.join("real.txt"); - let link_file = root.join("link.txt"); - fs::create_dir_all(&root).unwrap(); - fs::write(&real_file, b"x").unwrap(); - if !try_create_file_reparse_point( - &real_file, - &link_file, - "test_portable_service_shmem_file_acl_rejects_reparse_target", - ) { - let _ = fs::remove_file(&real_file); - let _ = fs::remove_dir_all(&root); - return; - } - - let result = set_path_permission_for_portable_service_shmem_file(&link_file); - let text = result.err().map(|e| e.to_string()).unwrap_or_default(); - assert!( - text.contains("reparse point"), - "expected reparse-point rejection, got '{}'", - text - ); - - let _ = fs::remove_file(&link_file); - let _ = fs::remove_file(&real_file); - let _ = fs::remove_dir_all(&root); - } -} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 89d7fa01e..3ef280a2a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -41,30 +41,6 @@ lazy_static::lazy_static! { static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); -pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false); -// register_pk retry interval (ms) when device is awaiting deployment -const DEPLOY_RETRY_INTERVAL: i64 = 30_000; -lazy_static::lazy_static! { - static ref LAST_NOT_DEPLOYED_REGISTER: Mutex> = Mutex::new(None); -} - -// Single source of truth for the "awaiting deployment" backoff. The server has -// already told us this device is not in its db; until the operator runs -// `rustdesk --deploy --token ` there is no point re-running the -// register path more often than DEPLOY_RETRY_INTERVAL. Gating in the timer -// loops (rather than only inside register_pk) also avoids the -// last_register_sent / fails / latency / UDP-rebind churn the loop would -// otherwise spin on while no response ever comes back. -async fn deploy_register_throttled() -> bool { - if !NEEDS_DEPLOY.load(Ordering::SeqCst) { - return false; - } - LAST_NOT_DEPLOYED_REGISTER - .lock() - .await - .map(|t| (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL) - .unwrap_or(false) -} #[derive(Clone)] pub struct RendezvousMediator { @@ -250,14 +226,6 @@ impl RendezvousMediator { if SHOULD_EXIT.load(Ordering::SeqCst) { break; } - // The server already told us this device is not deployed. Skip - // the whole register / fails / latency / UDP-rebind path until - // DEPLOY_RETRY_INTERVAL elapses, otherwise the loop spins every - // few seconds (log spam + misapplied network-recovery rebind) - // until the operator runs `rustdesk --deploy`. - if deploy_register_throttled().await { - continue; - } let now = Some(Instant::now()); let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true); let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false); @@ -321,22 +289,10 @@ impl RendezvousMediator { Config::set_key_confirmed(true); Config::set_host_key_confirmed(&self.host_prefix, true); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); - NEEDS_DEPLOY.store(false, Ordering::SeqCst); } Ok(register_pk_response::Result::UUID_MISMATCH) => { self.handle_uuid_mismatch(sink).await?; } - Ok(register_pk_response::Result::NOT_DEPLOYED) => { - if !NEEDS_DEPLOY.load(Ordering::SeqCst) { - log::warn!("Server requires deployment. Run `rustdesk --deploy --token ` on this device."); - } - NEEDS_DEPLOY.store(true, Ordering::SeqCst); - // Clear key_confirmed so the UI reflects the truth: this device is - // not currently registered. Covers the case where an online device - // was deleted by an admin while running. - Config::set_key_confirmed(false); - Config::set_host_key_confirmed(&self.host_prefix, false); - } _ => { log::error!("unknown RegisterPkResponse"); } @@ -722,21 +678,6 @@ impl RendezvousMediator { } async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> { - // Throttle register_pk when the device is awaiting deployment: server - // already told us we're not in its db; sending more often than every - // DEPLOY_RETRY_INTERVAL ms is wasted traffic until the operator runs - // `rustdesk --deploy --token `. - if NEEDS_DEPLOY.load(Ordering::SeqCst) { - let mut last = LAST_NOT_DEPLOYED_REGISTER.lock().await; - if let Some(t) = *last { - if (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL { - return Ok(()); - } - } - *last = Some(Instant::now()); - } else { - *LAST_NOT_DEPLOYED_REGISTER.lock().await = None; - } let mut msg_out = Message::new(); let pk = Config::get_key_pair().1; let uuid = hbb_common::get_uuid(); diff --git a/src/server.rs b/src/server.rs index 86f7b5396..dddc762bf 100644 --- a/src/server.rs +++ b/src/server.rs @@ -67,7 +67,6 @@ pub mod input_service { } mod connection; -mod login_failure_check; pub mod display_service; #[cfg(windows)] pub mod portable_service; @@ -732,7 +731,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option { if !synced { if conn.send(&Data::SyncConfig(None)).await.is_ok() { @@ -773,12 +772,6 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option { log::error!("sync config to root failed: {}", e); - match crate::ipc::connect_service(1000).await { + match crate::ipc::connect(1000, "_service").await { Ok(mut _conn) => { conn = _conn; log::info!("reconnected to ipc_service"); diff --git a/src/server/connection.rs b/src/server/connection.rs index 538503d9c..a960daac1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,8 +1,3 @@ -#[cfg(target_os = "windows")] -use super::login_failure_check::try_acquire_os_credential_login_gate; -use super::login_failure_check::{ - evaluate_os_credential_policy, record_os_credential_failure, FailureScope, -}; use super::{input_service::*, *}; #[cfg(feature = "unix-file-copy-paste")] use crate::clipboard::try_empty_clipboard_files; @@ -27,6 +22,8 @@ use crate::{ #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use cidr_utils::cidr::IpCidr; +#[cfg(target_os = "linux")] +use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ @@ -87,9 +84,6 @@ lazy_static::lazy_static! { static ref PENDING_SWITCH_SIDES_UUID: Arc::>> = Default::default(); } -#[cfg(target_os = "windows")] -const TERMINAL_OS_LOGIN_FAILED_MSG: &str = "Incorrect username or password."; - fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; @@ -102,32 +96,6 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { x == 0 } -#[cfg(target_os = "linux")] -fn should_check_linux_headless_os_auth_before_desktop_start( - is_headless_allowed: bool, - username: &str, -) -> bool { - is_headless_allowed - && !username.trim().is_empty() - && linux_desktop_manager::get_username().is_empty() -} - -#[cfg(target_os = "linux")] -fn should_record_linux_headless_os_auth_failure( - is_headless_allowed: bool, - username: &str, - err_msg: &str, -) -> bool { - is_headless_allowed - && !username.trim().is_empty() - && err_msg == crate::client::LOGIN_MSG_PASSWORD_WRONG -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn should_use_terminal_os_login_scope(is_terminal: bool, os_login_username: &str) -> bool { - cfg!(target_os = "windows") && is_terminal && !os_login_username.trim().is_empty() -} - #[cfg(any(target_os = "windows", target_os = "linux"))] lazy_static::lazy_static! { static ref WALLPAPER_REMOVER: Arc>> = Default::default(); @@ -1531,9 +1499,6 @@ impl Connection { // Keep the connection alive so the client can continue with 2FA. return true; } - if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await { - return keep_alive; - } if !self.connect_port_forward_if_needed().await { return false; } @@ -2413,6 +2378,33 @@ impl Connection { o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); } self.terminal_service_id = terminal.service_id; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg) = + self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) + { + self.send_login_error(msg).await; + sleep(1.).await; + return false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(is_user) = + terminal_service::is_service_specified_user(&self.terminal_service_id) + { + if let Some(user_token) = &self.terminal_user_token { + let has_service_token = + user_token.to_terminal_service_token().is_some(); + if is_user != has_service_token { + // This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation. + log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail."); + // No need to translate the following message, because it is in an abnormal case. + self.send_login_error("Terminal service user mismatch detected.") + .await; + sleep(1.).await; + return false; + } + } + } } Some(login_request::Union::PortForward(mut pf)) => { if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) { @@ -2430,43 +2422,8 @@ impl Connection { } } - if !hbb_common::is_ip_str(&lr.username) - && !hbb_common::is_domain_port_str(&lr.username) - && lr.username != Config::get_id() - { - self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) - .await; - return false; - } - - #[cfg(target_os = "windows")] - if self.terminal - && lr.os_login.username.trim().is_empty() - && crate::platform::is_prelogin() - { - self.send_login_error( - "No active console user logged on, please connect and logon first.", - ) - .await; - sleep(1.).await; - return false; - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if !should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { - self.try_start_cm_ipc(); - } - - #[cfg(target_os = "linux")] - if should_check_linux_headless_os_auth_before_desktop_start( - self.linux_headless_handle.is_headless_allowed, - &lr.os_login.username, - ) { - let (_failure, res) = self.check_failure(0).await; - if !res { - return true; - } - } + self.try_start_cm_ipc(); #[cfg(not(target_os = "linux"))] let err_msg = "".to_owned(); @@ -2478,18 +2435,6 @@ impl Connection { // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY { - #[cfg(target_os = "linux")] - if should_record_linux_headless_os_auth_failure( - self.linux_headless_handle.is_headless_allowed, - &lr.os_login.username, - &err_msg, - ) { - let (failure, res) = self.check_failure(0).await; - if !res { - return true; - } - self.update_failure(failure, false, 0); - } self.send_login_error(err_msg).await; return true; } @@ -2518,16 +2463,17 @@ impl Connection { crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" && is_logon(); - if (password::approve_mode() == ApproveMode::Click && !allow_logon_screen_password) + if !hbb_common::is_ip_str(&lr.username) + && !hbb_common::is_domain_port_str(&lr.username) + && lr.username != Config::get_id() + { + self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) + .await; + return false; + } else if (password::approve_mode() == ApproveMode::Click + && !allow_logon_screen_password) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { - if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await - { - return keep_alive; - } - } self.try_start_cm(lr.my_id, lr.my_name, false); if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") @@ -2549,14 +2495,6 @@ impl Connection { } } else if lr.password.is_empty() { if err_msg.is_empty() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { - if let Some(keep_alive) = - self.prepare_terminal_login_for_authorization().await - { - return keep_alive; - } - } self.try_start_cm(lr.my_id, lr.my_name, false); } else { self.send_login_error( @@ -2570,7 +2508,7 @@ impl Connection { return true; } if !self.validate_password(allow_logon_screen_password) { - self.update_failure_with_scope(failure, false, 0, FailureScope::Default); + self.update_failure(failure, false, 0); self.check_update_temporary_password(false); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) @@ -2583,7 +2521,7 @@ impl Connection { .await; } } else { - self.update_failure_with_scope(failure, true, 0, FailureScope::Default); + self.update_failure(failure, true, 0); if err_msg.is_empty() { #[cfg(target_os = "linux")] self.linux_headless_handle.wait_desktop_cm_ready().await; @@ -3548,16 +3486,16 @@ impl Connection { self.terminal_user_token = Some(TerminalUserToken::SelfUser); None } else { - Some(TERMINAL_OS_LOGIN_FAILED_MSG) + Some("The user is not an administrator.") } } Ok(Err(e)) => { log::error!("Failed to check if the user is an administrator: {}", e); - Some(TERMINAL_OS_LOGIN_FAILED_MSG) + Some("Failed to check if the user is an administrator.") } Err(e) => { log::error!("Failed to get logon user token: {}", e); - Some(TERMINAL_OS_LOGIN_FAILED_MSG) + Some("Incorrect username or password.") } } } @@ -3593,146 +3531,6 @@ impl Connection { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - async fn prepare_terminal_login_for_authorization(&mut self) -> Option { - if !self.terminal || self.terminal_user_token.is_some() { - return None; - } - - #[derive(Copy, Clone)] - enum TerminalAuthorizationMode { - OsLogin { - failure: ((i32, i32, i32), i32), - scope: FailureScope, - }, - SessionUser, - } - - let normalized_username = self.lr.os_login.username.trim().to_owned(); - let auth_mode = if should_use_terminal_os_login_scope(self.terminal, &normalized_username) { - // Check failure state - let failure_scope = FailureScope::TerminalOsLogin; - let (failure, res) = self.check_failure_with_scope(0, failure_scope).await; - if !res { - log::warn!( - "OS credential login blocked by failure policy: ip={} conn_id={} scope={:?}", - self.ip, - self.inner.id(), - failure_scope - ); - // Terminal OS login is sensitive. Close this connection instead of keeping it - // alive for retries on the same socket after a rate-limit block. - return Some(false); - } - TerminalAuthorizationMode::OsLogin { - failure, - scope: failure_scope, - } - } else { - TerminalAuthorizationMode::SessionUser - }; - - let is_terminal_os_login = matches!(auth_mode, TerminalAuthorizationMode::OsLogin { .. }); - let failure_scope = match auth_mode { - TerminalAuthorizationMode::OsLogin { scope, .. } => scope, - TerminalAuthorizationMode::SessionUser => FailureScope::Default, - }; - - let username = normalized_username; - let password = self.lr.os_login.password.clone(); - let terminal_login_error = { - #[cfg(target_os = "windows")] - { - // Concurrency gate for terminal OS login with credentials, to prevent brute-force attacks. - let _os_login_concurrency_guard = if is_terminal_os_login { - let guard = try_acquire_os_credential_login_gate(); - if guard.is_err() { - log::warn!( - "OS credential login blocked by concurrency gate: ip={} conn_id={} scope={:?}", - self.ip, - self.inner.id(), - failure_scope - ); - self.send_login_error("Please try 1 minute later").await; - sleep(1.).await; - Self::post_alarm_audit( - AlarmAuditType::TerminalOsLoginConcurrency, - json!({ - "ip": self.ip, - "id": self.lr.my_id.clone(), - "name": self.lr.my_name.clone(), - }), - ); - return Some(false); - } - guard.ok() - } else { - None - }; - self.fill_terminal_user_token(&username, &password) - } - #[cfg(not(target_os = "windows"))] - { - self.fill_terminal_user_token(&username, &password) - } - }; - if let Some(msg) = terminal_login_error { - if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode { - self.update_failure_with_scope(failure, false, 0, scope); - } - let auth_context = if is_terminal_os_login { - "OS credential login verification" - } else { - "Terminal session-user authorization" - }; - log::warn!( - "{} failed: ip={} conn_id={} scope={:?} msg='{}'", - auth_context, - self.ip, - self.inner.id(), - failure_scope, - msg - ); - self.send_login_error(msg).await; - sleep(1.).await; - return Some(false); - } - if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode { - self.update_failure_with_scope(failure, true, 0, scope); - } - - if let Some(is_user) = - terminal_service::is_service_specified_user(&self.terminal_service_id) - { - if let Some(user_token) = &self.terminal_user_token { - let has_service_token = user_token.to_terminal_service_token().is_some(); - if is_user != has_service_token { - log::error!( - "Terminal service user mismatch: ip={} conn_id={} service_is_user={} has_service_token={}. The service ID may have been manually changed in the configuration, causing validation to fail.", - self.ip, - self.inner.id(), - is_user, - has_service_token - ); - // No need to translate the following message, because it is in an abnormal case. - self.send_login_error("Terminal service user mismatch detected.") - .await; - sleep(1.).await; - return Some(false); - } - } - } - if is_terminal_os_login { - self.try_start_cm_ipc(); - } - None - } - - #[cfg(any(target_os = "android", target_os = "ios"))] - async fn prepare_terminal_login_for_authorization(&mut self) -> Option { - None - } - // Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes. // Parsing an IPv4 address just returns None. // note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues @@ -3759,37 +3557,18 @@ impl Connection { Some((p64, p56, p48)) } - fn bump_failure_entry(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { - if cur.0 == time { - cur.1 += 1; - cur.2 += 1; - } else { - cur.0 = time; - cur.1 = 1; - cur.2 += 1; - } - cur - } - - fn update_failure(&self, failure: ((i32, i32, i32), i32), remove: bool, i: usize) { - self.update_failure_with_scope(failure, remove, i, FailureScope::Default); - } - - fn update_failure_with_scope( - &self, - (failure, time): ((i32, i32, i32), i32), - remove: bool, - i: usize, - scope: FailureScope, - ) { - let os_credential_scope = matches!(scope, FailureScope::TerminalOsLogin); - if os_credential_scope { - if !remove { - record_os_credential_failure(scope); + fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { + fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { + if cur.0 == time { + cur.1 += 1; + cur.2 += 1; + } else { + cur.0 = time; + cur.1 = 1; + cur.2 += 1; } - return; + cur } - let map_mutex = &LOGIN_FAILURES[i]; if remove { if failure.0 != 0 { @@ -3810,15 +3589,14 @@ impl Connection { let mut m = map_mutex.lock().unwrap(); for key in [p64, p56, p48] { let cur = m.get(&key).copied().unwrap_or((0, 0, 0)); - m.insert(key, Self::bump_failure_entry(cur, time)); + m.insert(key, bump(cur, time)); } - let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0)); - m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time)); + // Update full IP: bump from the *original* passed-in failure + m.insert(self.ip.clone(), bump(failure, time)); } else { - // Re-read the full IP bucket in case another failed attempt updated it. + // Update full IP: bump from the *original* passed-in failure let mut m = map_mutex.lock().unwrap(); - let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0)); - m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time)); + m.insert(self.ip.clone(), bump(failure, time)); } } @@ -3858,50 +3636,8 @@ impl Connection { } async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) { - self.check_failure_with_scope(i, FailureScope::Default) - .await - } - - async fn check_failure_with_scope( - &mut self, - i: usize, - scope: FailureScope, - ) -> (((i32, i32, i32), i32), bool) { let time = (get_time() / 60_000) as i32; - if matches!(scope, FailureScope::TerminalOsLogin) { - let decision = evaluate_os_credential_policy(scope, get_time()); - let res = if decision.allowed { - true - } else { - log::warn!( - "OS credential login blocked by policy: ip={} conn_id={} i={} msg='{}'", - self.ip, - self.inner.id(), - i, - decision.login_error.as_deref().unwrap_or("") - ); - if let Some(login_error) = decision.login_error { - // Rare branch and currently temporary response copy; translation can be added later if needed. - self.send_login_error(login_error).await; - } - if let Some(audit) = decision.audit { - // For OS blocked/backoff events, we currently emit one alarm report per blocked attempt. - // TODO: Add unified cumulative/aggregation fields across alarm producers. - Self::post_alarm_audit( - audit, - json!({ - "ip": self.ip, - "id": self.lr.my_id.clone(), - "name": self.lr.my_name.clone(), - }), - ); - } - false - }; - return (((0, 0, 0), time), res); - } - // IPv6 addresses are cheap to make so we check prefix/netblock as well if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() { if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await { @@ -5247,9 +4983,6 @@ pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -// IPC bootstrap summary: -// - Resolve target CM socket (headless/non-headless, optional UID-scoped path on Linux). -// - Start CM when missing, then bridge bidirectional messages between this task and CM IPC. async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, @@ -5264,19 +4997,10 @@ async fn start_ipc( } sleep(1.).await; } - #[cfg(target_os = "linux")] - let headless_cm = crate::is_server() - && crate::platform::is_headless_allowed() - && linux_desktop_manager::is_headless(); - #[cfg(not(target_os = "linux"))] - let headless_cm = false; let mut stream = None; - if !headless_cm { - if let Ok(s) = crate::ipc::connect(1000, "_cm").await { - stream = Some(s); - } - } - if stream.is_none() { + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + } else { #[allow(unused_mut)] #[allow(unused_assignments)] let mut args = vec!["--cm"]; @@ -5286,123 +5010,75 @@ async fn start_ipc( // Cm run as user, wait until desktop session is ready. #[cfg(target_os = "linux")] - if headless_cm { + if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() { let mut username = linux_desktop_manager::get_username(); loop { if !username.is_empty() { break; } - // `_rx_desktop_ready` is used as a wake-up signal from desktop/session state changes - // (for example wait_desktop_cm_ready paths). It is not itself a proof of CM readiness. - // TODO: - // When `_rx_desktop_ready` is closed, `recv()` returns - // `None` immediately and this loop may spin if `username` remains empty. - // Keep behavior unchanged for now; if field reports appear, handle `Ok(None)` by - // breaking/returning to avoid hot-looping. let _res = timeout(1_000, _rx_desktop_ready.recv()).await; username = linux_desktop_manager::get_username(); } let uid = { - let username_for_cmd = username.clone(); - let mut uid_cmd = hbb_common::tokio::process::Command::new("id"); - // TODO: - // Keep current behavior for now to minimize change risk. - // If usernames starting with '-' are observed in the field, prefer: - // `id -u -- ` to avoid option-parsing ambiguity. - // Already verified that `id -u -- ` works as expected on macOS and Ubuntu 24.04. - uid_cmd.arg("-u").arg(&username_for_cmd).kill_on_drop(true); - let output = timeout(10_000, uid_cmd.output()) - .await - .map_err(|_| anyhow!("Timed out querying uid for {}", username))? - .map_err(|e| anyhow!("Failed to run `id -u {}`: {}", username, e))?; - if !output.status.success() { - bail!("Failed to query uid for {}", username); - } - let output = String::from_utf8_lossy(&output.stdout); + let output = run_cmds(&format!("id -u {}", &username))?; let output = output.trim(); - if output.parse::().is_err() { - bail!("Invalid uid {}", output); + if output.is_empty() || !output.parse::().is_ok() { + bail!("Invalid username {}", &username); } output.to_string() }; user = Some((uid, username)); args = vec!["--cm-no-ui"]; } - #[cfg(target_os = "linux")] - let cm_uid: Option = match &user { - Some((uid, _)) => Some( - uid.parse::() - .map_err(|_| anyhow!("Invalid uid {}", uid))?, - ), - None => None, - }; - #[cfg(target_os = "linux")] - if let Some(uid) = cm_uid { - if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await { + let run_done; + if crate::platform::is_root() { + let mut res = Ok(None); + for _ in 0..10 { + #[cfg(not(any(target_os = "linux")))] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user(args.clone()); + } + #[cfg(target_os = "linux")] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user( + args.clone(), + user.clone(), + None::<(&str, &str)>, + ); + } + if res.is_ok() { + break; + } + log::error!("Failed to run cm: {res:?}"); + sleep(1.).await; + } + if let Some(task) = res? { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + log::debug!("Start cm"); + super::CHILD_PROCESS + .lock() + .unwrap() + .push(crate::run_me(args)?); + } + for _ in 0..20 { + sleep(0.3).await; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); + break; } } if stream.is_none() { - let run_done; - if crate::platform::is_root() { - let mut res = Ok(None); - for _ in 0..10 { - #[cfg(not(any(target_os = "linux")))] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user(args.clone()); - } - #[cfg(target_os = "linux")] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user( - args.clone(), - user.clone(), - None::<(&str, &str)>, - ); - } - if res.is_ok() { - break; - } - log::error!("Failed to run cm: {res:?}"); - sleep(1.).await; - } - if let Some(task) = res? { - super::CHILD_PROCESS.lock().unwrap().push(task); - } - run_done = true; - } else { - run_done = false; - } - if !run_done { - log::debug!("Start cm"); - super::CHILD_PROCESS - .lock() - .unwrap() - .push(crate::run_me(args)?); - } - for _ in 0..20 { - sleep(0.3).await; - #[cfg(target_os = "linux")] - { - if let Some(uid) = cm_uid { - if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await { - stream = Some(s); - break; - } - continue; - } - } - if let Ok(s) = crate::ipc::connect(1000, "_cm").await { - stream = Some(s); - break; - } - } + bail!("Failed to connect to connection manager"); } } - if stream.is_none() { - bail!("Failed to connect to connection manager"); - } let _res = tx_stream_ready.send(()).await; let mut stream = stream.ok_or(anyhow!("none stream"))?; @@ -5485,8 +5161,6 @@ pub enum AlarmAuditType { // MultipleLoginsAttemptsWithinOneMinute = 4, // MultipleLoginsAttemptsWithinOneHour = 5, ExceedIPv6PrefixAttempts = 6, - TerminalOsLoginBackoff = 7, - TerminalOsLoginConcurrency = 8, } pub enum FileAuditType { diff --git a/src/server/login_failure_check.rs b/src/server/login_failure_check.rs deleted file mode 100644 index 4394213ec..000000000 --- a/src/server/login_failure_check.rs +++ /dev/null @@ -1,231 +0,0 @@ -use crate::AlarmAuditType; -use hbb_common::get_time; -#[cfg(target_os = "windows")] -use hbb_common::tokio::sync::{Mutex as TokioMutex, OwnedMutexGuard}; -use std::sync::Mutex; -#[cfg(target_os = "windows")] -use std::sync::Arc; - -const OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS: i64 = 120 * 60 * 1_000; -const OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS: i64 = 15; -const OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS: i64 = 30 * 60; - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(crate) enum FailureScope { - Default, - TerminalOsLogin, -} - -pub(crate) struct OsCredentialPolicyDecision { - pub allowed: bool, - pub login_error: Option, - pub audit: Option, -} - -#[derive(Copy, Clone, Debug, Default)] -struct OsCredentialFailureState { - total_failures: i32, - backoff_until_ms: Option, - last_failure_ms: Option, -} - -lazy_static::lazy_static! { - static ref OS_CREDENTIAL_LOGIN_FAILURE_STATE: Mutex = - Mutex::new(OsCredentialFailureState::default()); -} - -#[cfg(target_os = "windows")] -lazy_static::lazy_static! { - static ref OS_CREDENTIAL_LOGIN_MUTEX: Arc> = Arc::new(TokioMutex::new(())); -} - -fn is_os_credential_scope(scope: FailureScope) -> bool { - matches!(scope, FailureScope::TerminalOsLogin) -} - -fn state_for_os_credential_scope( - scope: FailureScope, -) -> Option<&'static Mutex> { - if is_os_credential_scope(scope) { - Some(&OS_CREDENTIAL_LOGIN_FAILURE_STATE) - } else { - None - } -} - -fn backoff_audit_type_for_scope(scope: FailureScope) -> Option { - match scope { - FailureScope::TerminalOsLogin => Some(AlarmAuditType::TerminalOsLoginBackoff), - FailureScope::Default => None, - } -} - -fn os_credential_login_backoff_seconds(total_failures: i32) -> i64 { - if total_failures <= 2 { - return 0; - } - let exp = (total_failures - 3).min(7); - let seconds = OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS * (1_i64 << exp); - seconds.min(OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS) -} - -fn normalize_backoff(state: &mut OsCredentialFailureState, now_ms: i64) { - if let Some(until_ms) = state.backoff_until_ms { - if until_ms <= now_ms { - state.backoff_until_ms = None; - } - } -} - -fn reset_totals_on_idle(state: &mut OsCredentialFailureState, now_ms: i64) { - if let Some(last_ms) = state.last_failure_ms { - if now_ms.saturating_sub(last_ms) >= OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS { - state.total_failures = 0; - state.backoff_until_ms = None; - state.last_failure_ms = None; - } - } -} - -fn allow_decision() -> OsCredentialPolicyDecision { - OsCredentialPolicyDecision { - allowed: true, - login_error: None, - audit: None, - } -} - -fn block_decision( - login_error: String, - alarm_type: Option, -) -> OsCredentialPolicyDecision { - OsCredentialPolicyDecision { - allowed: false, - login_error: Some(login_error), - audit: alarm_type, - } -} - -pub(crate) fn evaluate_os_credential_policy( - scope: FailureScope, - now_ms: i64, -) -> OsCredentialPolicyDecision { - if !is_os_credential_scope(scope) { - return allow_decision(); - } - let Some(state_mutex) = state_for_os_credential_scope(scope) else { - return allow_decision(); - }; - let mut state = state_mutex.lock().unwrap(); - reset_totals_on_idle(&mut state, now_ms); - normalize_backoff(&mut state, now_ms); - - if let Some(until_ms) = state.backoff_until_ms { - let remaining_ms = (until_ms - now_ms).max(0); - let remaining_seconds = ((remaining_ms + 999) / 1_000).max(1); - let seconds_label = if remaining_seconds == 1 { - "second" - } else { - "seconds" - }; - block_decision( - format!( - "Please try again in {} {}.", - remaining_seconds, seconds_label - ), - backoff_audit_type_for_scope(scope), - ) - } else { - allow_decision() - } -} - -pub(crate) fn record_os_credential_failure(scope: FailureScope) { - if !is_os_credential_scope(scope) { - return; - } - let Some(state_mutex) = state_for_os_credential_scope(scope) else { - return; - }; - let mut state = state_mutex.lock().unwrap(); - let now_ms = get_time(); - reset_totals_on_idle(&mut state, now_ms); - normalize_backoff(&mut state, now_ms); - state.total_failures = state.total_failures.saturating_add(1); - state.last_failure_ms = Some(now_ms); - let backoff_seconds = os_credential_login_backoff_seconds(state.total_failures); - if backoff_seconds > 0 { - state.backoff_until_ms = Some(now_ms + backoff_seconds * 1_000); - } -} - -#[cfg(target_os = "windows")] -pub(crate) fn try_acquire_os_credential_login_gate() -> Result, ()> { - OS_CREDENTIAL_LOGIN_MUTEX - .clone() - .try_lock_owned() - .map_err(|_| ()) -} - -#[cfg(test)] -mod tests { - use super::*; - - static TEST_MUTEX: Mutex<()> = Mutex::new(()); - - fn clear_os_credential_failure_state(scope: FailureScope) { - if let Some(state_mutex) = state_for_os_credential_scope(scope) { - *state_mutex.lock().unwrap() = OsCredentialFailureState::default(); - } - } - - #[test] - fn os_credential_policy_prioritizes_backoff() { - let _guard = TEST_MUTEX.lock().unwrap(); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - let now_ms = get_time(); - for _ in 0..3 { - record_os_credential_failure(FailureScope::TerminalOsLogin); - } - let decision = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms); - assert!(!decision.allowed); - assert!(decision.login_error.is_some()); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - } - - #[test] - fn os_credential_policy_idle_window_resets_total_counter() { - let _guard = TEST_MUTEX.lock().unwrap(); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - for _ in 0..13 { - record_os_credential_failure(FailureScope::TerminalOsLogin); - } - let blocked = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, get_time()); - assert!(!blocked.allowed); - - let after_failures_ms = get_time(); - let after_idle_ms = after_failures_ms + OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS + 1_000; - let allowed = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, after_idle_ms); - assert!(allowed.allowed); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - } - - #[test] - fn os_credential_policy_audits_every_backoff_block() { - let _guard = TEST_MUTEX.lock().unwrap(); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - - for _ in 0..3 { - record_os_credential_failure(FailureScope::TerminalOsLogin); - } - let now_ms = get_time(); - let first = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms); - let second = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms + 1_000); - assert!(!first.allowed); - assert!(!second.allowed); - assert!(first.audit.is_some()); - assert!(second.audit.is_some()); - - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - } -} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 23b69a70c..6f5695046 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -1,11 +1,3 @@ -use crate::{ - ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN}, - platform::{ - set_path_permission, set_path_permission_for_portable_service_shmem_dir, - set_path_permission_for_portable_service_shmem_file, - validate_path_for_portable_service_shmem_dir, - }, -}; use core::slice; use hbb_common::{ allow_err, @@ -23,26 +15,26 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, Mutex, - }, + path::Path, + sync::{Arc, Mutex}, time::Duration, }; use winapi::{ shared::minwindef::{BOOL, FALSE, TRUE}, um::winuser::{self, CURSORINFO, PCURSORINFO}, }; -use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ}; + +use crate::{ + ipc::{self, new_listener, Connection, Data, DataPortableService}, + platform::set_path_permission, +}; use super::video_qos; const SIZE_COUNTER: usize = size_of::() * 2; const FRAME_ALIGN: usize = 64; -const ADDR_IPC_TOKEN: usize = 0; -const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN; +const ADDR_CURSOR_PARA: usize = 0; const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::(); const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER; @@ -52,186 +44,12 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of:: bool { - !name.is_empty() - && name.len() <= SHMEM_NAME_MAX_LEN - && name - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-') -} - -#[inline] -pub fn portable_service_shmem_arg(name: &str) -> String { - format!("{SHMEM_ARG_PREFIX}{name}") -} - -#[inline] -fn is_valid_portable_service_ipc_token(token: &str) -> bool { - token.len() == IPC_TOKEN_LEN - && token - .bytes() - .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) -} - -#[inline] -fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option { - if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN { - log::error!( - "Portable service shared memory too small: len={}, need>={}", - shmem.len(), - ADDR_IPC_TOKEN + IPC_TOKEN_LEN - ); - return None; - } - unsafe { - let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN); - let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN); - let end = bytes - .iter() - .position(|byte| *byte == 0) - .unwrap_or(IPC_TOKEN_LEN); - if end == 0 { - return None; - } - let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned(); - if is_valid_portable_service_ipc_token(&token) { - Some(token) - } else { - None - } - } -} - -#[inline] -fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> { - if shmem.len() < MIN_RUNTIME_SHMEM_LEN { - bail!( - "Portable service shared memory too small for runtime layout: len={}, need>={}", - shmem.len(), - MIN_RUNTIME_SHMEM_LEN - ); - } - Ok(()) -} - -#[inline] -fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool { - let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME); - frame_len > 0 && frame_len <= frame_capacity -} - -#[inline] -fn shared_memory_flink_path_by_name(name: &str) -> ResultType { - let mut dir = crate::platform::user_accessible_folder()?; - dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); - dir = dir.join(SHMEM_PARENT_DIR); - Ok(dir.join(format!("shared_memory{}", name))) -} - -#[inline] -fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool { - let flink = match shared_memory_flink_path_by_name(name) { - Ok(path) => path, - Err(err) => { - if log_on_error { - log::warn!( - "{} failed to resolve portable service shared-memory flink path for '{}': {}", - log_context, - name, - err - ); - } - return false; - } - }; - match std::fs::remove_file(&flink) { - Ok(()) => { - log::info!( - "{} removed portable service shared-memory flink artifact: {:?}", - log_context, - flink - ); - true - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => true, - Err(err) => { - if log_on_error { - log::warn!( - "{} failed to remove portable service shared-memory flink artifact {:?}: {}", - log_context, - flink, - err - ); - } - false - } - } -} - -#[inline] -fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> { - if !is_valid_portable_service_ipc_token(token) { - bail!("Invalid portable service ipc token"); - } - shmem.write(ADDR_IPC_TOKEN, token.as_bytes()); - Ok(()) -} - -#[inline] -fn clear_ipc_token_in_shmem(shmem: &SharedMemory) { - shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]); -} - -#[inline] -fn portable_service_arg_value_candidate_from_arg<'a>( - arg: &'a str, - prefix: &str, -) -> Option<&'a str> { - let mut value = arg.strip_prefix(prefix)?; - value = value.trim_start(); - value = value - .strip_prefix('"') - .or_else(|| value.strip_prefix('\'')) - .unwrap_or(value); - value = value.split_whitespace().next().unwrap_or_default(); - value = value.trim_matches(|c| c == '"' || c == '\''); - Some(value) -} - -#[inline] -pub fn portable_service_shmem_name_from_args() -> Option { - for arg in std::env::args() { - if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) { - if is_valid_portable_service_shmem_name(value) { - return Some(value.to_owned()); - } - log::error!( - "Invalid portable service shared memory name argument: '{}'", - value - ); - return None; - } - } - None -} - -#[inline] -pub fn has_portable_service_shmem_arg() -> bool { - std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX)) -} - pub struct SharedMemory { inner: Shmem, } @@ -274,27 +92,7 @@ impl SharedMemory { } }; log::info!("Create shared memory, size: {}, flink: {}", size, flink); - if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) { - // Release shmem handle first so best-effort flink cleanup has a chance to succeed. - drop(shmem); - match std::fs::remove_file(&flink) { - Ok(()) => { - log::info!( - "Create cleanup removed portable service shared-memory flink artifact: {}", - flink - ); - } - Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {} - Err(remove_err) => { - log::warn!( - "Create cleanup failed to remove portable service shared-memory flink artifact {}: {}", - flink, - remove_err - ); - } - } - return Err(err); - } + set_path_permission(Path::new(&flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } @@ -322,18 +120,9 @@ impl SharedMemory { fn flink(name: String) -> ResultType { let mut dir = crate::platform::user_accessible_folder()?; dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); - dir = dir.join(SHMEM_PARENT_DIR); - let parent_created = !dir.exists(); - if parent_created { - std::fs::create_dir_all(&dir)?; - } - if parent_created || crate::platform::is_root() { - // Harden parent ACL on first provisioning and periodically on SYSTEM path. - set_path_permission_for_portable_service_shmem_dir(&dir)?; - } else { - // Existing parents still need type/reparse validation. Non-SYSTEM callers may lack - // WRITE_DAC on a valid parent, so avoid rebuilding the ACL here. - validate_path_for_portable_service_shmem_dir(&dir)?; + if !dir.exists() { + std::fs::create_dir(&dir)?; + set_path_permission(&dir, "F").ok(); } Ok(dir .join(format!("shared_memory{}", name)) @@ -443,45 +232,16 @@ pub mod server { lazy_static::lazy_static! { static ref EXIT: Arc> = Default::default(); - static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false); } pub fn run_portable_service() { - let shmem_name = match portable_service_shmem_name_from_args() { - Some(name) => name, - None => { - if has_portable_service_shmem_arg() { - log::error!( - "Invalid portable service shared memory argument, aborting startup" - ); - } else { - log::error!( - "Missing portable service shared memory argument, aborting startup" - ); - } - return; - } - }; - let shmem = match SharedMemory::open_existing(&shmem_name) { + let shmem = match SharedMemory::open_existing(SHMEM_NAME) { Ok(shmem) => Arc::new(shmem), Err(e) => { log::error!("Failed to open existing shared memory: {:?}", e); return; } }; - if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) { - log::error!("{}", e); - return; - } - let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) { - Some(token) => token, - None => { - log::error!( - "Missing portable service ipc token in shared memory, aborting startup" - ); - return; - } - }; let shmem1 = shmem.clone(); let shmem2 = shmem.clone(); let mut threads = vec![]; @@ -491,24 +251,17 @@ pub mod server { threads.push(std::thread::spawn(|| { run_capture(shmem2); })); - threads.push(std::thread::spawn(move || { - run_ipc_client(ipc_token); + threads.push(std::thread::spawn(|| { + run_ipc_client(); })); - // Detached shutdown watchdog: - // - gives graceful shutdown/cleanup a short window - // - force-exits the process if workers are still stuck - std::thread::spawn(|| { + threads.push(std::thread::spawn(|| { run_exit_check(); - }); + })); let record_pos_handle = crate::input_service::try_start_record_cursor_pos(); - // Arm forced-exit watchdog only for worker join phase. - // Once join phase completes, cleanup should not be interrupted by forced exit. - FORCE_EXIT_ARMED.store(true, Ordering::SeqCst); for th in threads.drain(..) { th.join().ok(); log::info!("thread joined"); } - FORCE_EXIT_ARMED.store(false, Ordering::SeqCst); crate::input_service::try_stop_record_cursor_pos(); if let Some(handle) = record_pos_handle { @@ -517,47 +270,16 @@ pub mod server { Err(e) => log::error!("record_pos_handle join error {:?}", &e), } } - drop(shmem); - remove_shared_memory_flink_with_retry(&shmem_name); } fn run_exit_check() { - const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3); loop { if EXIT.lock().unwrap().clone() { - break; + std::thread::sleep(Duration::from_millis(50)); + std::process::exit(0); } std::thread::sleep(Duration::from_millis(50)); } - // Fallback only: normal shutdown path should complete and process should exit naturally. - // This forced exit is a last resort when worker threads are stuck and graceful teardown - // does not finish in time. - std::thread::sleep(FORCED_EXIT_DELAY); - if FORCE_EXIT_ARMED.load(Ordering::SeqCst) { - log::warn!( - "Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}", - FORCED_EXIT_DELAY - ); - std::process::exit(0); - } - } - - fn remove_shared_memory_flink_with_retry(name: &str) { - const MAX_RETRY: usize = 20; - const RETRY_INTERVAL: Duration = Duration::from_millis(200); - for attempt in 0..MAX_RETRY { - let is_last_attempt = attempt + 1 == MAX_RETRY; - if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") { - return; - } - if !is_last_attempt { - std::thread::sleep(RETRY_INTERVAL); - } - } - log::warn!( - "SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry", - name - ); } fn run_get_cursor_info(shmem: Arc) { @@ -664,17 +386,6 @@ pub mod server { match c.as_mut().map(|f| f.frame(spf)) { Some(Ok(f)) => match f { Frame::PixelBuffer(f) => { - let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME); - if f.data().len() > frame_capacity { - log::error!( - "Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}", - f.data().len(), - frame_capacity, - shmem.len() - ); - *EXIT.lock().unwrap() = true; - return; - } utils::set_frame_info( &shmem, FrameInfo { @@ -725,33 +436,17 @@ pub mod server { } #[tokio::main(flavor = "current_thread")] - async fn run_ipc_client(ipc_token: String) { + async fn run_ipc_client() { use DataPortableService::*; let postfix = IPC_SUFFIX; match ipc::connect(1000, postfix).await { Ok(mut stream) => { - if let Err(err) = - ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await - { - log::error!("portable service ipc handshake failed: {}", err); - *EXIT.lock().unwrap() = true; - return; - } let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); let mut nack = 0; loop { - if *EXIT.lock().unwrap() { - log::info!("Portable service EXIT signaled, closing ipc client loop"); - stream - .send(&Data::DataPortableService(WillClose)) - .await - .ok(); - break; - } - tokio::select! { res = stream.next() => { match res { @@ -831,11 +526,7 @@ pub mod client { lazy_static::lazy_static! { static ref RUNNING: Arc> = Default::default(); - static ref STARTING: Arc> = Default::default(); - static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0); static ref SHMEM: Arc>> = Default::default(); - static ref SHMEM_RUNTIME_NAME: Arc>> = Default::default(); - static ref IPC_RUNTIME_TOKEN: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); static ref QUICK_SUPPORT: Arc> = Default::default(); } @@ -845,176 +536,12 @@ pub mod client { Logon(String, String), } - fn has_running_portable_service_process() -> bool { - let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase()); - !crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service") - .is_empty() - } - - #[inline] - fn next_portable_service_shmem_name() -> String { - format!( - "{}_{}_{:08x}", - crate::portable_service::SHMEM_NAME, - std::process::id(), - hbb_common::rand::random::() - ) - } - - #[inline] - fn set_runtime_ipc_token(token: String) { - *IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token); - } - - #[inline] - fn schedule_remove_runtime_shmem_flink_retry(name: String) { - std::thread::spawn(move || { - const MAX_RETRY: usize = 20; - const RETRY_INTERVAL: Duration = Duration::from_millis(200); - for _ in 0..MAX_RETRY { - std::thread::sleep(RETRY_INTERVAL); - if remove_shared_memory_flink_once(&name, false, "Client cleanup") { - return; - } - } - log::warn!( - "Failed to remove portable service shared-memory flink artifact '{}' after retry", - name - ); - }); - } - - #[inline] - fn clear_runtime_shmem_state() { - let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap(); - let mut shmem_lock = SHMEM.lock().unwrap(); - if let Some(shmem) = shmem_lock.as_mut() { - clear_ipc_token_in_shmem(shmem); - } - *shmem_lock = None; - let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take(); - *runtime_token = None; - drop(runtime_token); - drop(shmem_lock); - if let Some(name) = runtime_name.as_deref() { - if !remove_shared_memory_flink_once(name, true, "Client cleanup") { - schedule_remove_runtime_shmem_flink_retry(name.to_owned()); - } - } - } - - #[inline] - fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option) { - let mut token = IPC_RUNTIME_TOKEN.lock().unwrap(); - if !token - .as_deref() - .is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate)) - { - return (false, None); - } - let mut shmem_lock = SHMEM.lock().unwrap(); - let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone(); - *token = None; - if let Some(shmem) = shmem_lock.as_mut() { - clear_ipc_token_in_shmem(shmem); - } - (true, matched_shmem_name) - } - - #[inline] - fn restore_runtime_ipc_token_after_failed_handshake( - token: &str, - expected_shmem_name: Option<&str>, - ) { - let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap(); - if let Some(current) = runtime_token.as_deref() { - if current != token { - log::debug!( - "Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value" - ); - return; - } - } - let mut shmem_lock = SHMEM.lock().unwrap(); - let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone(); - if current_shmem_name.as_deref() != expected_shmem_name { - if runtime_token.as_deref() == Some(token) { - *runtime_token = None; - } - log::debug!( - "Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed" - ); - return; - } - let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() { - write_ipc_token_to_shmem(shmem, token) - .err() - .map(|err| err.to_string()) - } else { - Some("shared memory unavailable".to_owned()) - }; - if let Some(err) = shmem_write_error { - if runtime_token.as_deref() == Some(token) { - *runtime_token = None; - } - log::warn!( - "Failed to restore portable service ipc token after handshake failure: {}", - err - ); - return; - } - *runtime_token = Some(token.to_owned()); - } - - #[inline] - fn schedule_starting_timeout_reset(launch_token: u64) { - std::thread::spawn(move || { - std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT); - let should_reset = { - // Guard against stale watchdogs from previous launches: - // only the watchdog that matches the latest STARTING_TOKEN may reset STARTING. - let current_token = STARTING_TOKEN.load(Ordering::SeqCst); - // Keep lock guards in explicit short scopes to make it obvious - // there is no nested lock ordering (and to avoid Copilot false positives). - let starting = { *STARTING.lock().unwrap() }; - let running = { *RUNNING.lock().unwrap() }; - current_token == launch_token && starting && !running - }; - if should_reset { - log::warn!( - "Portable service startup timeout before IPC ready, reset STARTING state" - ); - *STARTING.lock().unwrap() = false; - } - }); - } - - // Launch flow summary: - // 1) Prepare/reset runtime shared memory + IPC token. - // 2) Start helper process (direct or logon) with shmem argument. - // 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it. pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { log::info!("start portable service"); - let launch_token = { - // Keep lock guards in explicit short scopes to make it obvious - // there is no nested lock ordering (and to avoid Copilot false positives). - let running = { *RUNNING.lock().unwrap() }; - let mut starting = STARTING.lock().unwrap(); - if *starting && !running && !has_running_portable_service_process() { - log::warn!( - "Detected stale portable service STARTING state without running process, reset it" - ); - *starting = false; - } - if *starting || running { - bail!("already running"); - } - *starting = true; - STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1 - }; - let start_result = (|| -> ResultType<()> { - clear_runtime_shmem_state(); - let mut shmem_lock = SHMEM.lock().unwrap(); + if RUNNING.lock().unwrap().clone() { + bail!("already running"); + } + if SHMEM.lock().unwrap().is_none() { let displays = scrap::Display::all()?; if displays.is_empty() { bail!("no display available!"); @@ -1031,153 +558,84 @@ pub mod client { } } } - let shmem_size = - utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN); - let shmem_name = next_portable_service_shmem_name(); - if !is_valid_portable_service_shmem_name(&shmem_name) { - bail!("Generated invalid portable service shared memory name"); - } - let ipc_token = ipc::generate_one_time_ipc_token()?; + let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align); // os error 112, no enough space - *shmem_lock = Some(crate::portable_service::SharedMemory::create( - &shmem_name, + *SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create( + crate::portable_service::SHMEM_NAME, shmem_size, )?); - *SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name); shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory); - let shmem_name = SHMEM_RUNTIME_NAME - .lock() - .unwrap() - .clone() - .ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?; - let init_token_result = if let Some(shmem) = shmem_lock.as_mut() { - unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); - } - write_ipc_token_to_shmem(shmem, &ipc_token) - } else { - Ok(()) - }; - if let Err(e) = init_token_result { - drop(shmem_lock); - clear_runtime_shmem_state(); - bail!( - "Failed to initialize portable service ipc token in shared memory: {}", - e - ); - }; - drop(shmem_lock); - set_runtime_ipc_token(ipc_token.clone()); - let portable_service_arg = format!( - "--portable-service {}", - crate::portable_service::portable_service_shmem_arg(&shmem_name) - ); - { - let _sender = SENDER.lock().unwrap(); - } - match para { - StartPara::Direct => { - match crate::platform::run_background( - &std::env::current_exe()?.to_string_lossy().to_string(), - &portable_service_arg, - ) { - Ok(true) => {} - Ok(false) => { - clear_runtime_shmem_state(); - bail!("Failed to run portable service process"); - } - Err(e) => { - clear_runtime_shmem_state(); - bail!("Failed to run portable service process: {}", e); - } - } - } - StartPara::Logon(username, password) => { - #[allow(unused_mut)] - let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); - #[cfg(feature = "flutter")] - { - if let Some(dir) = Path::new(&exe).parent() { - if let Err(err) = set_path_permission( - Path::new(dir), - FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0, - ) { - clear_runtime_shmem_state(); - bail!("Failed to set permission of {:?}: {}", dir, err); - } - } - } - #[cfg(not(feature = "flutter"))] - if let Some((dir, dst)) = - crate::platform::windows::portable_service_logon_helper_paths() - { - let cleanup_helper_artifacts = || { - if Path::new(&exe) != dst { - std::fs::remove_file(&dst).ok(); - } - std::fs::remove_dir(&dir).ok(); - }; - let mut use_logon_helper_exe = false; - if let Err(err) = std::fs::create_dir_all(&dir) { - log::warn!( - "Failed to create portable service logon helper dir {:?}: {}", - dir, - err - ); - } else if let Err(err) = std::fs::copy(&exe, &dst) { - log::warn!( - "Failed to copy portable service logon helper binary from '{}' to {:?}: {}", - exe, - dst, - err - ); - cleanup_helper_artifacts(); - } else if !dst.exists() { - log::warn!( - "Portable service logon helper binary missing after copy: {:?}", - dst - ); - cleanup_helper_artifacts(); - } else if let Err(err) = - set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) - { - log::warn!( - "Failed to set portable service logon helper path permission for {:?}: {}", - dir, - err - ); - cleanup_helper_artifacts(); - } else { - use_logon_helper_exe = true; - } - if use_logon_helper_exe { - exe = dst.to_string_lossy().to_string(); - } - } - if let Err(e) = crate::platform::windows::create_process_with_logon( - username.as_str(), - password.as_str(), - &exe, - &portable_service_arg, - ) { - clear_runtime_shmem_state(); - bail!("Failed to run portable service process: {}", e); - } - } - } - schedule_starting_timeout_reset(launch_token); - Ok(()) - })(); - if start_result.is_err() { - *STARTING.lock().unwrap() = false; } - start_result + if let Some(shmem) = SHMEM.lock().unwrap().as_mut() { + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + } + } + match para { + StartPara::Direct => { + if let Err(e) = crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = Path::new(&exe).parent() { + if set_path_permission(Path::new(dir), "RX").is_err() { + *SHMEM.lock().unwrap() = None; + bail!("Failed to set permission of {:?}", dir); + } + } + } + #[cfg(not(feature = "flutter"))] + match hbb_common::directories_next::UserDirs::new() { + Some(user_dir) => { + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + if std::fs::create_dir_all(&dir).is_ok() { + let dst = dir.join("rustdesk.exe"); + if std::fs::copy(&exe, &dst).is_ok() { + if dst.exists() { + if set_path_permission(&dir, "RX").is_ok() { + exe = dst.to_string_lossy().to_string(); + } + } + } + } + } + None => {} + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + } + let _sender = SENDER.lock().unwrap(); + Ok(()) } pub extern "C" fn drop_portable_service_shared_memory() { // https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout // Please make sure there is no print in the call stack - clear_runtime_shmem_state(); + let mut lock = SHMEM.lock().unwrap(); + if lock.is_some() { + *lock = None; + } } pub fn set_quick_support(v: bool) { @@ -1197,11 +655,7 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); if let Some(shmem) = option.as_mut() { unsafe { - libc::memset( - shmem.as_ptr().add(ADDR_CURSOR_PARA) as _, - 0, - shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _, - ); + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } utils::set_para( shmem, @@ -1248,19 +702,6 @@ pub mod client { if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) { let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO); let frame_info = frame_info_ptr as *const FrameInfo; - let frame_len = (*frame_info).length; - if !is_valid_capture_frame_length(shmem.len(), frame_len) { - log::error!( - "Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}", - frame_len, - shmem.len(), - ADDR_CAPTURE_FRAME - ); - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid portable service frame length".to_string(), - )); - } if (*frame_info).width != self.width || (*frame_info).height != self.height { log::info!( "skip frame, ({},{}) != ({},{})", @@ -1275,7 +716,7 @@ pub mod client { )); } let frame_ptr = base.add(ADDR_CAPTURE_FRAME); - let data = slice::from_raw_parts(frame_ptr, frame_len); + let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( data, self.width, @@ -1337,49 +778,10 @@ pub mod client { Some(result) = incoming.next() => { match result { Ok(stream) => { - let mut stream = Connection::new(stream); - if !ipc::authorize_windows_portable_service_ipc_connection( - &stream, postfix, - ) { - continue; - } - let mut consumed_token: Option = None; - let mut consumed_token_shmem_name: Option = None; - let handshake_result = - ipc::portable_service_ipc_handshake_as_server( - &mut stream, - |token| { - let (matched, matched_shmem_name) = - consume_runtime_ipc_token_if_match(token); - if matched { - consumed_token = Some(token.to_owned()); - consumed_token_shmem_name = matched_shmem_name; - true - } else { - false - } - }, - ) - .await; - if let Err(err) = handshake_result { - if let Some(token) = consumed_token.as_deref() { - restore_runtime_ipc_token_after_failed_handshake( - token, - consumed_token_shmem_name.as_deref(), - ); - *STARTING.lock().unwrap() = false; - } - log::warn!( - "Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}", - postfix, - err - ); - continue; - } log::info!("Got portable service ipc connection"); let rx_clone = rx.clone(); tokio::spawn(async move { - let mut stream = stream; + let mut stream = Connection::new(stream); let postfix = postfix.to_owned(); let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); let mut nack = 0; @@ -1403,7 +805,6 @@ pub mod client { Pong => { nack = 0; *RUNNING.lock().unwrap() = true; - *STARTING.lock().unwrap() = false; }, ConnCount(None) => { if !quick_support { @@ -1440,7 +841,6 @@ pub mod client { } } *RUNNING.lock().unwrap() = false; - *STARTING.lock().unwrap() = false; }); } Err(err) => { @@ -1590,23 +990,3 @@ pub struct FrameInfo { width: usize, height: usize, } - -#[cfg(test)] -mod tests { - use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME}; - - #[test] - fn test_is_valid_capture_frame_length_rejects_zero_length() { - assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0)); - } - - #[test] - fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() { - assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17)); - } - - #[test] - fn test_is_valid_capture_frame_length_accepts_in_bounds_length() { - assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16)); - } -} diff --git a/src/server/uinput.rs b/src/server/uinput.rs index a1947d79f..a808b4aaa 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -185,13 +185,9 @@ pub mod client { pub mod service { use super::*; use hbb_common::lazy_static; - #[cfg(target_os = "linux")] - use parity_tokio_ipc::Connection as RawIpcConnection; use scrap::wayland::{ pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop, }; - #[cfg(target_os = "linux")] - use std::os::unix::io::AsRawFd; use std::{collections::HashMap, sync::Mutex}; lazy_static::lazy_static! { @@ -606,10 +602,7 @@ pub mod service { } DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { if *code < 8 { - log::error!( - "Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", - code - ); + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); } else { let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); allow_err!(keyboard.emit(&[down_event])); @@ -617,10 +610,7 @@ pub mod service { } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { if *code < 8 { - log::error!( - "Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", - code - ); + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); } else { let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); allow_err!(keyboard.emit(&[up_event])); @@ -919,35 +909,6 @@ pub mod service { }); } - #[cfg(target_os = "linux")] - fn authorize_uinput_peer(postfix: &str, stream: &RawIpcConnection) -> bool { - if !hbb_common::config::is_service_ipc_postfix(postfix) { - return true; - } - let peer_uid = ipc::peer_uid_from_fd(stream.as_raw_fd()); - let active_uid = crate::platform::linux::get_active_userid_fresh() - .trim() - .parse::() - .ok(); - let authorized = - peer_uid.is_some_and(|uid| ipc::is_allowed_service_peer_uid(uid, active_uid)); - if !authorized { - crate::ipc::log_rejected_uinput_connection(postfix, peer_uid, active_uid); - return false; - } - if let Err(err) = - ipc::ensure_peer_executable_matches_current_by_fd(stream.as_raw_fd(), postfix) - { - log::warn!( - "Rejected connection on protected uinput ipc channel due to executable mismatch: postfix={}, err={}", - postfix, - err - ); - return false; - } - true - } - /// Start uinput service. async fn start_service(postfix: &str, handler: F) { match new_listener(postfix).await { @@ -955,10 +916,6 @@ pub mod service { while let Some(result) = incoming.next().await { match result { Ok(stream) => { - #[cfg(target_os = "linux")] - if !authorize_uinput_peer(postfix, &stream) { - continue; - } log::debug!("Got new connection of uinput ipc {}", postfix); handler(Connection::new(stream)); }