From 87d7feb823f11663a82fb9d81787cebfa9c68e94 Mon Sep 17 00:00:00 2001 From: GT610 <79314033+GT-610@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:47:06 +0800 Subject: [PATCH] ref(systemd): Fix safety bugs and improve performance (#1020) * fix(systemd): Fix the issue of special characters in unit names In systemd unit processing, filtering of special characters in unit names has been added to prevent command injection and security issues. Additionally, the rendering performance of the unit list has been optimized by merging unnecessary watch calls and removing confirmation dialogs to simplify the operation process. * feat(Systemd): Add a confirmation dialog for systemd unit operations Display a confirmation dialog when stopping or restarting systemd units to prevent accidental operations. For other operations, directly navigate to the SSH page to execute commands. * fix(systemd): Fix the range of characters allowed in unit names Extend the regular expression to allow more valid characters, including dots, @, and colons, in system unit names, to support a broader range of unit naming conventions * fix(systemd): Fix the issue of parsing service names with dots When dealing with service IDs containing multiple dots (such as org.cups.cupsd.service), correctly extract the service name and type. When there are no dots in the service ID, set the type to an empty string. --- lib/data/model/server/systemd.dart | 2 +- lib/data/provider/systemd.dart | 24 +++++++++---- lib/view/page/systemd.dart | 55 ++++++++++++++++++------------ 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/lib/data/model/server/systemd.dart b/lib/data/model/server/systemd.dart index d68e2d76..518f22e6 100644 --- a/lib/data/model/server/systemd.dart +++ b/lib/data/model/server/systemd.dart @@ -96,7 +96,7 @@ final class SystemdUnit { String getCmd({required SystemdUnitFunc func, required bool isRoot}) { final prefix = scope.getCmdPrefix(isRoot); - return '$prefix ${func.name} $name'; + return '$prefix ${func.name} ${name.replaceAll(RegExp(r'[^a-zA-Z0-9\-_.@:]'), '')}'; } List get availableFuncs { diff --git a/lib/data/provider/systemd.dart b/lib/data/provider/systemd.dart index 3247bdf3..3c61d2aa 100644 --- a/lib/data/provider/systemd.dart +++ b/lib/data/provider/systemd.dart @@ -77,11 +77,16 @@ class SystemdNotifier extends _$SystemdNotifier { } Future> _parseUnitObj(List unitNames, SystemdUnitScope scope) async { - final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList(); + final unitNames_ = unitNames.map((e) { + final fullName = e.trim().split('/').last; + final lastDot = fullName.lastIndexOf('.'); + final name = lastDot > 0 ? fullName.substring(0, lastDot) : fullName; + return name.replaceAll(RegExp(r'[^a-zA-Z0-9\-_.@:]'), ''); + }).toList(); final script = ''' -for unit in ${unitNames_.join(' ')}; do - state=\$(systemctl show --no-pager \$unit) +for unit in ${unitNames_.map((e) => '"$e"').join(' ')}; do + state=\$(systemctl show --no-pager -- "\$unit") echo "\$state" echo -n "\n${ScriptConstants.separator}\n" done @@ -102,10 +107,15 @@ done if (part.startsWith('Id=')) { final val = _getIniVal(part); if (val == null) continue; - // Id=sshd.service - final vals = val.split('.'); - name = vals.first; - type = vals.last; + // Id=org.cups.cupsd.service + final lastDot = val.lastIndexOf('.'); + if (lastDot > 0) { + name = val.substring(0, lastDot); + type = val.substring(lastDot + 1); + } else { + name = val; + type = ''; + } continue; } if (part.startsWith('ActiveState=')) { diff --git a/lib/view/page/systemd.dart b/lib/view/page/systemd.dart index 4c081ca8..9f86b0fb 100644 --- a/lib/view/page/systemd.dart +++ b/lib/view/page/systemd.dart @@ -73,8 +73,7 @@ final class _SystemdPageState extends ConsumerState { } Widget _buildUnitList() { - ref.watch(_pro.select((p) => p.units)); - ref.watch(_pro.select((p) => p.scopeFilter)); + ref.watch(_pro.select((p) => (p.units, p.scopeFilter))); final filteredUnits = _notifier.filteredUnits; if (filteredUnits.isEmpty) { return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13)); @@ -97,28 +96,42 @@ final class _SystemdPageState extends ConsumerState { Widget _buildUnitFuncs(SystemdUnit unit) { return PopupMenu( items: unit.availableFuncs.map(_buildUnitFuncBtn).toList(), - onSelected: (val) async { - final cmd = unit.getCmd(func: val, isRoot: widget.args.spi.isRoot); - final sure = await context.showRoundDialog( - title: libL10n.attention, - child: SimpleMarkdown(data: '```shell\n$cmd\n```'), - actions: [ - CountDownBtn( - seconds: 1, - onTap: () => context.pop(true), - text: libL10n.ok, - afterColor: Colors.red, - ), - ], - ); - if (sure != true) return; - - final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd); - SSHPage.route.go(context, args); - }, + onSelected: (val) => _handleUnitFuncSelected(unit, val), ); } + void _handleUnitFuncSelected(SystemdUnit unit, SystemdUnitFunc func) { + final cmd = unit.getCmd(func: func, isRoot: widget.args.spi.isRoot); + + if (func == SystemdUnitFunc.stop || func == SystemdUnitFunc.restart) { + _showConfirmDialog(cmd); + } else { + _navigateToSsh(cmd); + } + } + + void _showConfirmDialog(String cmd) async { + final sure = await context.showRoundDialog( + title: libL10n.attention, + child: SimpleMarkdown(data: '```shell\n$cmd\n```'), + actions: [ + Btn.cancel(), + CountDownBtn( + seconds: 3, + onTap: () => context.pop(true), + text: libL10n.ok, + afterColor: Colors.red, + ), + ], + ); + if (sure == true) _navigateToSsh(cmd); + } + + void _navigateToSsh(String cmd) { + final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd); + SSHPage.route.go(context, args); + } + PopupMenuEntry _buildUnitFuncBtn(SystemdUnitFunc func) { return PopupMenuItem( value: func,