From 61218f9ca37d0e78525f02586eba8e70c600dd8a Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Sun, 3 Sep 2023 16:40:29 +0800 Subject: [PATCH] new: `shutdown | reboot` on rootless user --- lib/core/extension/ssh_client.dart | 44 ++++++++++++++++++++++++++---- lib/core/utils/ui.dart | 17 ------------ lib/data/model/app/shell_func.dart | 29 ++++++++++++++++++-- lib/data/provider/docker.dart | 30 ++++++-------------- lib/data/provider/pkg.dart | 28 ++++++------------- lib/view/page/docker.dart | 1 + lib/view/page/pkg.dart | 3 +- lib/view/page/server/tab.dart | 13 +++++++-- 8 files changed, 95 insertions(+), 70 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 38f85b4f..e6086f2d 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -2,19 +2,24 @@ import 'dart:async'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/extension/uint8list.dart'; +import 'package:toolbox/core/utils/ui.dart'; -typedef OnStd = void Function(String data, StreamSink sink); -typedef OnStdin = void Function(StreamSink sink); +import '../../data/res/misc.dart'; + +typedef _OnStdout = void Function(String data, StreamSink sink); +typedef _OnStdin = void Function(StreamSink sink); typedef PwdRequestFunc = Future Function(String? user); extension SSHClientX on SSHClient { Future exec( String cmd, { - OnStd? onStderr, - OnStd? onStdout, - OnStdin? stdin, + _OnStdout? onStderr, + _OnStdout? onStdout, + _OnStdin? stdin, }) async { final session = await execute(cmd); @@ -49,4 +54,33 @@ extension SSHClientX on SSHClient { session.close(); return session.exitCode; } + + Future execWithPwd( + String cmd, { + BuildContext? context, + _OnStdout? onStdout, + _OnStdout? onStderr, + _OnStdin? stdin, + }) async { + var isRequestingPwd = false; + return await exec( + cmd, + onStderr: (data, sink) async { + onStderr?.call(data, sink); + if (isRequestingPwd) return; + isRequestingPwd = true; + if (data.contains('[sudo] password for ')) { + final user = pwdRequestWithUserReg.firstMatch(data)?.group(1); + if (context == null) return; + final pwd = await showPwdDialog(context, user); + if (pwd == null || pwd.isEmpty) { + return; + } + sink.add('$pwd\n'.uint8List); + } + }, + onStdout: onStdout, + stdin: stdin, + ); + } } diff --git a/lib/core/utils/ui.dart b/lib/core/utils/ui.dart index 69e74e20..1d9b26a1 100644 --- a/lib/core/utils/ui.dart +++ b/lib/core/utils/ui.dart @@ -5,13 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:toolbox/core/extension/context.dart'; -import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/data/model/app/tab.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../data/model/server/snippet.dart'; import '../../data/provider/snippet.dart'; -import '../../data/res/misc.dart'; import '../../data/res/ui.dart'; import '../../locator.dart'; import '../../view/widget/input_field.dart'; @@ -97,21 +95,6 @@ Future showPwdDialog( ); } -Future onPwd( - String event, - StreamSink stdin, - PwdRequestFunc? onPwdReq, -) async { - if (event.contains('[sudo] password for ')) { - final user = pwdRequestWithUserReg.firstMatch(event)?.group(1); - final pwd = await onPwdReq?.call(user); - if (pwd == null || pwd.isEmpty) { - return; - } - stdin.add('$pwd\n'.uint8List); - } -} - Widget buildSwitch( BuildContext context, StorePropertyBase prop, { diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index ee366784..390e15ba 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -11,6 +11,8 @@ enum AppShellFuncType { status, docker, process, + shutdown, + reboot, ; String get flag { @@ -21,6 +23,10 @@ enum AppShellFuncType { return 'd'; case AppShellFuncType.process: return 'p'; + case AppShellFuncType.shutdown: + return 'sd'; + case AppShellFuncType.reboot: + return 'r'; } } @@ -36,6 +42,10 @@ enum AppShellFuncType { return 'dockeR'; case AppShellFuncType.process: return 'process'; + case AppShellFuncType.shutdown: + return 'ShutDown'; + case AppShellFuncType.reboot: + return 'Reboot'; } } @@ -68,10 +78,24 @@ else \tps -ax fi '''; + case AppShellFuncType.shutdown: + return ''' +if [ "\$userId" = "0" ]; then +\tshutdown -h now +else +\tsudo -S shutdown -h now +fi'''; + case AppShellFuncType.reboot: + return ''' +if [ "\$userId" = "0" ]; then +\treboot +else +\tsudo -S reboot +fi'''; } } - static String get shellScript { + static final String shellScript = () { final sb = StringBuffer(); // Write each func for (final func in values) { @@ -98,7 +122,7 @@ ${func.cmd} ;; esac'''); return sb.toString(); - } + }(); } extension EnumX on Enum { @@ -196,6 +220,7 @@ export LANG=en_US.UTF-8 isLinux=\$(uname 2>&1 | grep "Linux") # Link /bin/sh to busybox? isBusybox=\$(ls -l /bin/sh | grep "busybox") +userId=\$(id -u) ${AppShellFuncType.shellScript} """; diff --git a/lib/data/provider/docker.dart b/lib/data/provider/docker.dart index 619768a9..547b98ca 100644 --- a/lib/data/provider/docker.dart +++ b/lib/data/provider/docker.dart @@ -1,12 +1,10 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/core/extension/stringx.dart'; -import 'package:toolbox/core/utils/ui.dart'; import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/docker/image.dart'; import 'package:toolbox/data/model/docker/ps.dart'; @@ -34,34 +32,33 @@ class DockerProvider extends ChangeNotifier { String? version; String? edition; DockerErr? error; - PwdRequestFunc? onPwdReq; String? hostId; String? runLog; - bool isRequestingPwd = false; + BuildContext? context; void init( SSHClient client, String userName, PwdRequestFunc onPwdReq, String hostId, + BuildContext context, ) { this.client = client; this.userName = userName; - this.onPwdReq = onPwdReq; + this.context = context; this.hostId = hostId; } void clear() { - client = userName = error = items = version = edition = onPwdReq = null; - isRequestingPwd = false; + client = userName = error = items = version = edition = context = null; hostId = runLog = images = null; } Future refresh() async { var raw = ''; - await client!.exec( + await client!.execWithPwd( AppShellFuncType.docker.exec, - onStderr: _onPwd, + context: context, onStdout: (data, _) => raw = '$raw$data', ); @@ -145,13 +142,6 @@ class DockerProvider extends ChangeNotifier { // } } - Future _onPwd(String event, StreamSink stdin) async { - if (isRequestingPwd) return; - isRequestingPwd = true; - await onPwd(event, stdin, onPwdReq); - isRequestingPwd = false; - } - Future stop(String id) async => await run('docker stop $id'); Future start(String id) async => await run('docker start $id'); @@ -168,16 +158,14 @@ class DockerProvider extends ChangeNotifier { runLog = ''; final errs = []; - final code = await client!.exec( + final code = await client!.execWithPwd( _wrap(cmd), - onStderr: (data, sink) { - _onPwd(data, sink); - errs.add(data); - }, + context: context, onStdout: (data, _) { runLog = '$runLog$data'; notifyListeners(); }, + onStderr: (data, _) => errs.add(data), ); runLog = null; notifyListeners(); diff --git a/lib/data/provider/pkg.dart b/lib/data/provider/pkg.dart index 4c4a38a7..3e6382e7 100644 --- a/lib/data/provider/pkg.dart +++ b/lib/data/provider/pkg.dart @@ -1,12 +1,10 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/core/extension/uint8list.dart'; -import 'package:toolbox/core/utils/ui.dart'; import 'package:toolbox/data/model/pkg/manager.dart'; import 'package:toolbox/data/model/pkg/upgrade_info.dart'; import 'package:toolbox/data/model/server/dist.dart'; @@ -19,7 +17,7 @@ class PkgProvider extends ChangeNotifier { PkgManager? type; Function()? onUpgrade; Function()? onUpdate; - PwdRequestFunc? onPasswordRequest; + BuildContext? context; String? whoami; List? upgradeable; @@ -27,20 +25,18 @@ class PkgProvider extends ChangeNotifier { String? upgradeLog; String? updateLog; String lastLog = ''; - bool isRequestingPwd = false; Future init( SSHClient client, Dist? dist, Function() onUpgrade, Function() onUpdate, - PwdRequestFunc onPasswordRequest, String user, + BuildContext context, ) async { this.client = client; this.dist = dist; this.onUpgrade = onUpgrade; - this.onPasswordRequest = onPasswordRequest; whoami = user; type = fromDist(dist); @@ -52,9 +48,8 @@ class PkgProvider extends ChangeNotifier { bool get isSU => whoami == 'root'; void clear() { - client = dist = updateLog = upgradeLog = upgradeable = - error = whoami = onUpdate = onUpgrade = onPasswordRequest = null; - isRequestingPwd = false; + client = dist = updateLog = upgradeLog = + upgradeable = error = whoami = onUpdate = onUpgrade = context = null; } Future refresh() async { @@ -78,9 +73,9 @@ class PkgProvider extends ChangeNotifier { Future _update() async { final updateCmd = type?.update; if (updateCmd != null) { - await client!.exec( + await client!.execWithPwd( _wrap(updateCmd), - onStderr: _onPwd, + context: context, onStdout: (data, sink) { updateLog = (updateLog ?? '') + data; if (onUpdate != null) onUpdate!(); @@ -105,9 +100,9 @@ class PkgProvider extends ChangeNotifier { return; } - await client!.exec( + await client!.execWithPwd( _wrap(upgradeCmd), - onStderr: _onPwd, + context: context, onStdout: (log, sink) { if (lastLog == log.trim()) return; upgradeLog = (upgradeLog ?? '') + log; @@ -121,13 +116,6 @@ class PkgProvider extends ChangeNotifier { refresh(); } - Future _onPwd(String event, StreamSink stdin) async { - if (isRequestingPwd) return; - isRequestingPwd = true; - await onPwd(event, stdin, onPasswordRequest); - isRequestingPwd = false; - } - String _wrap(String cmd) => 'export LANG=en_US.utf-8 && ${isSU ? "" : "sudo -S "}$cmd'; } diff --git a/lib/view/page/docker.dart b/lib/view/page/docker.dart index 7de3811a..1c87c2b2 100644 --- a/lib/view/page/docker.dart +++ b/lib/view/page/docker.dart @@ -62,6 +62,7 @@ class _DockerManagePageState extends State { widget.spi.user, (user) async => await showPwdDialog(context, user), widget.spi.id, + context, ); } diff --git a/lib/view/page/pkg.dart b/lib/view/page/pkg.dart index bf20e9d8..5fe4a17e 100644 --- a/lib/view/page/pkg.dart +++ b/lib/view/page/pkg.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; import '../../data/model/pkg/upgrade_info.dart'; import '../../data/model/server/dist.dart'; -import '../../core/utils/ui.dart'; import '../../data/model/server/server_private_info.dart'; import '../../data/provider/pkg.dart'; import '../../data/provider/server.dart'; @@ -61,8 +60,8 @@ class _PkgManagePageState extends State _scrollController.jumpTo(_scrollController.position.maxScrollExtent), () => _scrollControllerUpdate .jumpTo(_scrollController.position.maxScrollExtent), - (user) async => await showPwdDialog(context, user), widget.spi.user, + context, ); _pkgProvider.refresh(); } diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index adff31cb..fe4fb053 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -5,6 +5,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; import 'package:toolbox/core/extension/media_queryx.dart'; +import 'package:toolbox/core/extension/ssh_client.dart'; +import 'package:toolbox/data/model/app/shell_func.dart'; import '../../../core/route.dart'; import '../../../core/utils/misc.dart'; @@ -244,13 +246,18 @@ class _ServerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - // TODO: sudo | on pwd request IconButton( - onPressed: () => srv.client?.run('shutdown -h now'), + onPressed: () => srv.client?.execWithPwd( + AppShellFuncType.shutdown.cmd, + context: context, + ), icon: const Icon(Icons.power_off), ), IconButton( - onPressed: () => srv.client?.run('reboot'), + onPressed: () => srv.client?.execWithPwd( + AppShellFuncType.reboot.cmd, + context: context, + ), icon: const Icon(Icons.refresh), ), IconButton(