feat: keyboard-interactive auth (#349)
This commit is contained in:
@@ -61,13 +61,16 @@ extension DialogX on BuildContext {
|
||||
|
||||
static final _recoredPwd = <String, String>{};
|
||||
|
||||
/// Show a dialog to input password
|
||||
///
|
||||
/// [hostId] set it to null to skip remembering the password
|
||||
Future<String?> showPwdDialog({
|
||||
String? user,
|
||||
required String hostId,
|
||||
String? hostId,
|
||||
String? title,
|
||||
}) async {
|
||||
if (!mounted) return null;
|
||||
return await showRoundDialog<String>(
|
||||
title: Text(user ?? l10n.pwd),
|
||||
title: Text(title ?? hostId ?? l10n.pwd),
|
||||
child: Input(
|
||||
controller: TextEditingController(text: _recoredPwd[hostId]),
|
||||
autoFocus: true,
|
||||
@@ -75,7 +78,7 @@ extension DialogX on BuildContext {
|
||||
obscureText: true,
|
||||
onSubmitted: (val) {
|
||||
pop(val);
|
||||
if (Stores.setting.rememberPwdInMem.fetch()) {
|
||||
if (hostId != null && Stores.setting.rememberPwdInMem.fetch()) {
|
||||
_recoredPwd[hostId] = val;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -81,9 +81,9 @@ extension SSHClientX on SSHClient {
|
||||
isRequestingPwd = true;
|
||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||
if (context == null) return;
|
||||
final pwd = await context.showPwdDialog(user: user, hostId: id);
|
||||
final pwd = await context.showPwdDialog(title: user, hostId: id);
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
session.kill(SSHSignal.INT);
|
||||
session.kill(SSHSignal.TERM);
|
||||
} else {
|
||||
session.stdin.add('$pwd\n'.uint8List);
|
||||
}
|
||||
|
||||
20
lib/core/utils/auth.dart
Normal file
20
lib/core/utils/auth.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
abstract final class KeybordInteractive {
|
||||
static FutureOr<List<String>?> defaultHandle(
|
||||
ServerPrivateInfo spi, {
|
||||
BuildContext? ctx,
|
||||
}) async {
|
||||
try {
|
||||
final res = await (ctx ?? Pros.app.ctx)?.showPwdDialog(title: spi.id);
|
||||
return res == null ? null : [res];
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,9 @@ Future<SSHClient> genClient(
|
||||
///
|
||||
/// Must pass this param when use multi-thread and key login
|
||||
ServerPrivateInfo? jumpSpi,
|
||||
|
||||
/// Handle keyboard-interactive authentication
|
||||
FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
|
||||
}) async {
|
||||
onStatus?.call(GenSSHClientStatus.socket);
|
||||
|
||||
@@ -109,6 +112,9 @@ Future<SSHClient> genClient(
|
||||
socket,
|
||||
username: spi.user,
|
||||
onPasswordRequest: () => spi.pwd,
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
printDebug: debugPrint,
|
||||
printTrace: debugPrint,
|
||||
);
|
||||
}
|
||||
privateKey ??= getPrivateKey(keyId);
|
||||
@@ -119,5 +125,8 @@ Future<SSHClient> genClient(
|
||||
username: spi.user,
|
||||
// Must use [compute] here, instead of [Computer.shared.start]
|
||||
identities: await compute(loadIndentity, privateKey),
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
printDebug: debugPrint,
|
||||
printTrace: debugPrint,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ class AppProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool moveBg = true;
|
||||
|
||||
BuildContext? ctx;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/utils/auth.dart';
|
||||
import 'package:toolbox/core/utils/platform/path.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/server/system.dart';
|
||||
@@ -278,15 +279,14 @@ class ServerProvider extends ChangeNotifier {
|
||||
s.client = await genClient(
|
||||
spi,
|
||||
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
|
||||
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
|
||||
);
|
||||
final time2 = DateTime.now();
|
||||
final spentTime = time2.difference(time1).inMilliseconds;
|
||||
if (spi.jumpId == null) {
|
||||
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
||||
} else {
|
||||
Loggers.app.info(
|
||||
'Connected to ${spi.name} via jump server in $spentTime ms.',
|
||||
);
|
||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||
}
|
||||
} catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
@@ -316,6 +316,11 @@ class ServerProvider extends ChangeNotifier {
|
||||
s.status.err = e.toString();
|
||||
_setServerState(s, ServerState.failed);
|
||||
return;
|
||||
} on SSHAuthFailError catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
s.status.err = e.toString();
|
||||
_setServerState(s, ServerState.failed);
|
||||
return;
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Write script to ${spi.name} by shell', e);
|
||||
|
||||
@@ -351,6 +356,8 @@ class ServerProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
if (s.state == ServerState.connecting) return;
|
||||
|
||||
/// Keep [finished] state, or the UI will be refreshed to [loading] state
|
||||
/// instead of the '$Temp | $Uptime'.
|
||||
/// eg: '32C | 7 days'
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 876;
|
||||
static const int build = 877;
|
||||
static const String engine = "3.19.6";
|
||||
static const String buildAt = "2024-04-26 23:44:14";
|
||||
static const int modifications = 4;
|
||||
static const String buildAt = "2024-05-07 15:17:19";
|
||||
static const int modifications = 15;
|
||||
static const int script = 45;
|
||||
}
|
||||
|
||||
@@ -70,5 +70,6 @@ abstract final class GithubIds {
|
||||
'zj1123581321',
|
||||
'pctoolsx',
|
||||
'pgs666',
|
||||
'FHU-yezi',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ class _HomePageState extends State<HomePage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
Pros.app.ctx = context;
|
||||
|
||||
return Scaffold(
|
||||
drawer: _buildDrawer(),
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/server/try_limiter.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/percent_circle.dart';
|
||||
@@ -432,8 +433,48 @@ class _ServerPageState extends State<ServerPage>
|
||||
}
|
||||
|
||||
Widget _buildServerCardTitle(Server s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
|
||||
child: Text(
|
||||
s.spi.name,
|
||||
style: UIs.text13Bold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
size: 17,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildTopRightText(s),
|
||||
_buildTopRightWidget(s),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopRightWidget(Server s) {
|
||||
Widget rightCorner = UIs.placeholder;
|
||||
if (s.state == ServerState.failed) {
|
||||
if (s.state == ServerState.connecting) {
|
||||
rightCorner = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: SizedBox(
|
||||
width: 21,
|
||||
height: 21,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(primaryColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (s.state == ServerState.failed) {
|
||||
rightCorner = InkWell(
|
||||
onTap: () {
|
||||
TryLimiter.reset(s.spi.id);
|
||||
@@ -464,31 +505,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
} else if (Stores.setting.serverTabUseOldUI.fetch()) {
|
||||
rightCorner = ServerFuncBtnsTopRight(spi: s.spi);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
|
||||
child: Text(
|
||||
s.spi.name,
|
||||
style: UIs.text13Bold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
size: 17,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildTopRightText(s),
|
||||
rightCorner,
|
||||
],
|
||||
),
|
||||
);
|
||||
return rightCorner;
|
||||
}
|
||||
|
||||
Widget _buildTopRightText(Server s) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/utils/auth.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/server.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
@@ -384,9 +385,16 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
Future<void> _initTerminal() async {
|
||||
_writeLn('Connecting...\r\n');
|
||||
_client ??= await genClient(widget.spi);
|
||||
_writeLn('Starting shell...\r\n');
|
||||
_client ??= await genClient(
|
||||
widget.spi,
|
||||
onStatus: (p0) {
|
||||
_writeLn(p0.toString());
|
||||
},
|
||||
onKeyboardInteractive: (_) =>
|
||||
KeybordInteractive.defaultHandle(widget.spi),
|
||||
);
|
||||
|
||||
_writeLn('Starting shell...\r\n');
|
||||
final session = await _client?.shell(
|
||||
pty: SSHPtyConfig(
|
||||
width: _terminal.viewWidth,
|
||||
|
||||
Reference in New Issue
Block a user