Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/core/chan.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ abstract final class MethodChans {
_channel.invokeMethod('stopService');
}

static void updateHomeWidget() async {
static Future<void> updateHomeWidget() async {
if (!isIOS && !isAndroid) return;
if (!Stores.setting.autoUpdateHomeWidget.fetch()) return;
await _channel.invokeMethod('updateHomeWidget');
try {
await _channel.invokeMethod('updateHomeWidget');
} catch (e, s) {
Loggers.app.warning('Failed to update home widget', e, s);
}
}

/// Update Android foreground service notifications for SSH sessions
Expand Down
18 changes: 12 additions & 6 deletions lib/core/utils/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ import 'package:server_box/data/res/store.dart';
/// Because of this function is called by [compute].
///
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
List<SSHKeyPair> loadIndentity(String key) {
List<SSHKeyPair> loadIdentity(String key) {
return SSHKeyPair.fromPem(key);
}

/// Decrypts an encrypted PEM private key.
///
/// Must also be a top-level function because it is called via [Computer]
/// (isolate) — see comment on [loadIdentity].
///
/// [args] : [key, pwd]
String decyptPem(List<String> args) {
String decryptPem(List<String> args) {
/// skip when the key is not encrypted, or will throw exception
if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];
final sshKey = SSHKeyPair.fromPem(args[0], args[1]);
Expand Down Expand Up @@ -214,7 +219,7 @@ Future<SSHClient> genClient(
socket,
username: spi.user,
// Must use [compute] here, instead of [Computer.shared.start]
identities: await compute(loadIndentity, privateKey),
identities: await compute(loadIdentity, privateKey),
onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call,
);
Expand Down Expand Up @@ -249,10 +254,11 @@ bool _isJumpFailoverError(Object error) {
errStr.contains('connection reset') ||
errStr.contains('connection closed') ||
errStr.contains('no route to host') ||
errStr.contains('network') ||
errStr.contains('socket') ||
errStr.contains('network unreachable') ||
errStr.contains('network is unreachable') ||
errStr.contains('socketexception') ||
errStr.contains('failed host lookup') ||
errStr.contains('forward') ||
errStr.contains('forwardlocal') ||
Comment thread
coderabbitai[bot] marked this conversation as resolved.
errStr.contains('proxycommand exited') ||
errStr.contains('proxycommand timed out');
}
Expand Down
26 changes: 18 additions & 8 deletions lib/core/utils/server_dedup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import 'package:server_box/data/store/server.dart';
class ServerDeduplication {
/// Remove duplicate servers from the import list based on existing servers
/// Returns the deduplicated list
static List<Spi> deduplicateServers(List<Spi> importedServers) {
final existingServers = ServerStore.instance.fetch();
static List<Spi> deduplicateServers(
List<Spi> importedServers, {
List<Spi>? existingServers,
}) {
final existing = existingServers ?? ServerStore.instance.fetch();
final deduplicated = <Spi>[];

for (final imported in importedServers) {
if (!_isDuplicate(imported, existingServers)) {
if (!_isDuplicate(imported, existing)) {
deduplicated.add(imported);
}
}
Expand All @@ -33,9 +36,12 @@ class ServerDeduplication {
}

/// Resolve name conflicts by appending suffixes
static List<Spi> resolveNameConflicts(List<Spi> importedServers) {
final existingServers = ServerStore.instance.fetch();
final existingNames = existingServers.map((s) => s.name).toSet();
static List<Spi> resolveNameConflicts(
List<Spi> importedServers, {
List<Spi>? existingServers,
}) {
final existing = existingServers ?? ServerStore.instance.fetch();
final existingNames = existing.map((s) => s.name).toSet();
final processedNames = <String>{};
final result = <Spi>[];

Expand Down Expand Up @@ -111,8 +117,12 @@ class ServerDeduplication {
}

static List<Spi> _resolveServers(List<Spi> servers) {
final deduplicated = deduplicateServers(servers);
final resolved = resolveNameConflicts(deduplicated);
final existing = ServerStore.instance.fetch();
final deduplicated = deduplicateServers(servers, existingServers: existing);
final resolved = resolveNameConflicts(
deduplicated,
existingServers: existing,
);
return resolved;
}
}
Expand Down
7 changes: 5 additions & 2 deletions lib/core/utils/sftp_sudo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,13 @@ final class SftpSudoHelper {
}

static String _buildSudoCommand(String command, String password) {
final pwdBase64 = base64Encode(utf8.encode(password));
final wrapped = '($command) 2>&1';
final escapedWrapped = wrapped.replaceAll("'", "'\\''");
return 'echo "$pwdBase64" | base64 -d | sudo -S -- sh -c \'$escapedWrapped\'';
// Use shell builtin printf to pipe password to sudo -S.
// printf is a shell builtin so the password does not appear in
// the process argument list (unlike external `echo`).
final escapedPwd = password.replaceAll("'", "'\\''");
return "printf '%s\\n' '$escapedPwd' | sudo -S -- sh -c '$escapedWrapped'";
}

static SftpFileMode _buildMode(String typeChar, int permOct) {
Expand Down
2 changes: 1 addition & 1 deletion lib/data/model/container/ps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ final class DockerPs implements ContainerPs {
/// CONTAINER ID NAMES IMAGE STATUS
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
factory DockerPs.parse(String raw) {
final parts = raw.split(Miscs.multiBlankreg);
final parts = raw.split(Miscs.multiBlankReg);
return DockerPs(
id: parts[0],
state: parts[1],
Expand Down
3 changes: 3 additions & 0 deletions lib/data/model/server/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,8 @@ enum ServerConn {
/// Status parsing finished
finished;

/// Orders by declaration index: failed < disconnected < connecting <
/// connected < loading < finished. Do NOT reorder the enum values
/// above without auditing all call sites that rely on this ordering.
bool operator <(ServerConn other) => index < other.index;
}
11 changes: 2 additions & 9 deletions lib/data/model/server/server_private_info.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert';

import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart' show listEquals;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/custom.dart';
Expand Down Expand Up @@ -148,7 +149,7 @@ extension Spix on Spi {
port == other.port &&
pwd == other.pwd &&
keyId == other.keyId &&
_sameStringList(resolvedJumpIds, other.resolvedJumpIds) &&
listEquals(resolvedJumpIds, other.resolvedJumpIds) &&
proxyCommand == other.proxyCommand;
}

Expand Down Expand Up @@ -208,11 +209,3 @@ extension Spix on Spi {
/// Returns true if the user is 'root'.
bool get isRoot => user == 'root';
}

bool _sameStringList(List<String> a, List<String> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
15 changes: 3 additions & 12 deletions lib/data/provider/server/all.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart' show listEquals;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/sync.dart';
Expand Down Expand Up @@ -335,18 +336,8 @@ class ServersNotifier extends _$ServersNotifier {
}

bool _isSameOrder(List<String> a, List<String> b) {
if (identical(a, b)) {
return true;
}
if (a.length != b.length) {
return false;
}
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
if (identical(a, b)) return true;
return listEquals(a, b);
}

Future<void> updateServer(Spi old, Spi newSpi) async {
Expand Down
2 changes: 1 addition & 1 deletion lib/data/res/misc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:convert';

abstract final class Miscs {
static final blankReg = RegExp(r'\s+');
static final multiBlankreg = RegExp(r'\s{2,}');
static final multiBlankReg = RegExp(r'\s{2,}');

/// RegExp for password request
static final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):');
Expand Down
10 changes: 0 additions & 10 deletions lib/data/ssh/persistent_shell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,6 @@ printf '\\n$_donePrefix$commandId:%s\\n' "\$__server_box_exit"
}
}

static PersistentShellCommandResult? tryParseCompletedOutput(
String raw, {
required String expectedCommandId,
}) {
return _parseCompletedOutput(
raw,
expectedCommandId: expectedCommandId,
)?.result;
}

static _ParsedCompletedOutput? _parseCompletedOutput(
String raw, {
required String expectedCommandId,
Expand Down
2 changes: 1 addition & 1 deletion lib/data/store/setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ class SettingStore extends HiveStore {
late final noNotiPerm = propertyDefault('noNotiPerm', false);

/// The backup password
late final backupasswd = SecureProp('bakPasswd');
late final backupPassword = SecureProp('bakPasswd');

/// Whether to read SSH config from ~/.ssh/config on first time
late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);
Expand Down
4 changes: 2 additions & 2 deletions lib/view/page/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class _HomePageState extends ConsumerState<HomePage>
final serverNotifier = _notifier;
unawaited(serverNotifier.startAutoRefresh());
unawaited(serverNotifier.refresh());
MethodChans.updateHomeWidget();
unawaited(MethodChans.updateHomeWidget());
_syncFullscreenSystemUi();
break;
case AppLifecycleState.paused:
Expand Down Expand Up @@ -274,7 +274,7 @@ class _HomePageState extends ConsumerState<HomePage>
context: context,
);
}
MethodChans.updateHomeWidget();
unawaited(MethodChans.updateHomeWidget());
await _notifier.refresh();

bakSync.sync(milliDelay: 1000);
Expand Down
2 changes: 1 addition & 1 deletion lib/view/page/private_key/edit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class _PrivateKeyEditPageState extends ConsumerState<PrivateKeyEditPage> {
FocusScope.of(context).unfocus();
_loading.value = SizedLoading.medium;
try {
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
final decrypted = await Computer.shared.start(decryptPem, [key, pwd]);
final pki = PrivateKeyInfo(id: name, key: decrypted);
final originPki = this.pki;
if (originPki != null) {
Expand Down
2 changes: 1 addition & 1 deletion lib/view/page/setting/entries/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ extension _App on _AppSettingsPageState {
String? passwordUsed;

Future<String?> resolvePassword() async {
final saved = await _setting.backupasswd.read();
final saved = await _setting.backupPassword.read();
if (saved?.isNotEmpty == true) return saved;
final backupPwd = await SecureStoreProps.bakPwd.read();
if (backupPwd?.isNotEmpty == true) return backupPwd;
Expand Down
11 changes: 9 additions & 2 deletions lib/view/page/setting/entries/ssh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,15 @@ extension _SSH on _AppSettingsPageState {
}

Future<void> _processSSHServers(List<Spi> servers) async {
final deduplicated = ServerDeduplication.deduplicateServers(servers);
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
final existing = Stores.server.fetch();
final deduplicated = ServerDeduplication.deduplicateServers(
servers,
existingServers: existing,
);
final resolved = ServerDeduplication.resolveNameConflicts(
deduplicated,
existingServers: existing,
);
final summary = ServerDeduplication.getImportSummary(servers, resolved);

if (!summary.hasItemsToImport) {
Expand Down
Loading