Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J SM Mobile
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / SM Mobile
134 Dateien
sm-mobile-20261016T050549Z-3678f2a05fa9
Hinzugefügt: · Erstellt:
1
import 'dart:async';2
import 'dart:io';3
import 'dart:typed_data';4
import 'package:flutter_test/flutter_test.dart';5
import 'package:sm_mobile/core/failure.dart';6
import 'package:sm_mobile/models/server_profile.dart';7
import 'package:sm_mobile/services/monitor.dart';8
import 'package:sm_mobile/services/sftp_service.dart';9
import 'package:sm_mobile/services/ssh_service.dart';10
import 'package:sm_mobile/services/vault.dart';11
import '../support.dart';13
void main() {14
TestWidgetsFlutterBinding.ensureInitialized();15
final enabled = Platform.environment['SM_OPENSSH_TEST'] == '1';16
late Vault vault;17
late SshService ssh;18
late ServerProfile profile;19
late String key, root;20
setUpAll(() async {21
if (!enabled) return;22
vault = Vault(MemoryStore());23
await vault.create('isolated integration master password');24
ssh = SshService(vault);25
profile = ServerProfile(26
name: 'Isolated OpenSSH',27
host: '127.0.0.1',28
username: Platform.environment['USER']!,29
port: 22229,30
auth: AuthMethod.privateKey,31
);32
key = await File(Platform.environment['SM_TEST_KEY']!).readAsString();33
root = Platform.environment['SM_TEST_REMOTE_DIR']!;34
await Directory(root).create(recursive: true);35
root = await Directory(root).resolveSymbolicLinks();36
});37
tearDownAll(() {38
if (enabled) {39
ssh.dispose();40
vault.lock();41
}42
});43
test(44
'real OpenSSH: encrypted key, PTY, resize, streaming SFTP, symlink rejection and disconnect',45
() async {46
var prompts = 0;47
await ssh.connect(48
profile,49
credential: key,50
passphrase: 'isolated-test-passphrase',51
prompt: (endpoint, type, fingerprint) async {52
prompts++;53
expect(fingerprint, startsWith('SHA256:'));54
return true;55
},56
);57
expect(ssh.error, null);58
expect(ssh.connected, true);59
expect(prompts, 1);60
final nonce = newId();61
final marker = 'SM_PTY_$nonce';62
ssh.terminal.resize(100, 35);63
ssh.terminal.onOutput!(64
'printf "\\033[32mSM_PTY_%s\\033[0m\\n" "$nonce"\r',65
);66
final deadline = DateTime.now().add(const Duration(seconds: 10));67
while (!ssh.terminal.buffer.getText().contains(marker) &&68
DateTime.now().isBefore(deadline)) {69
await Future<void>.delayed(const Duration(milliseconds: 50));70
}71
expect(ssh.terminal.buffer.getText(), contains(marker));72
final sftp = SftpService(await ssh.client.sftp());73
final dir = '$root/run-${newId()}';74
await sftp.mkdir(dir);75
final payload = Uint8List.fromList(76
List.generate(256 * 1024, (i) => i % 251),77
);78
final temp = await Directory.systemTemp.createTemp('sm-download-test-');79
try {80
var progress = 0;81
await sftp.upload(82
Stream.value(payload),83
payload.length,84
'$dir/input.bin',85
TransferToken(),86
(n, size) {87
progress = n;88
},89
);90
expect(progress, payload.length);91
expect(92
(await sftp.list(dir)).map((e) => e.filename),93
contains('input.bin'),94
);95
await expectLater(96
sftp.upload(97
Stream.value(payload),98
payload.length,99
'$dir/input.bin',100
TransferToken(),101
(a, b) {},102
),103
throwsA(isA<Failure>().having((e) => e.code, 'code', 'fileExists')),104
);105
final download = File('${temp.path}/output.bin');106
await sftp.download(107
'$dir/input.bin',108
download,109
TransferToken(),110
(a, b) {},111
);112
expect(await download.readAsBytes(), payload);113
await sftp.rename('$dir/input.bin', '$dir/renamed.bin');114
await Link('$dir/link.bin').create('$dir/renamed.bin');115
await expectLater(116
sftp.download(117
'$dir/link.bin',118
File('${temp.path}/bad.bin'),119
TransferToken(),120
(a, b) {},121
),122
throwsA(isA<Failure>()),123
);124
await sftp.client.remove('$dir/link.bin');125
final token = TransferToken();126
Stream<Uint8List> slow() async* {127
for (var i = 0; i < 20; i++) {128
yield payload;129
await Future<void>.delayed(const Duration(milliseconds: 10));130
}131
}133
final upload = sftp.upload(134
slow(),135
payload.length * 20,136
'$dir/cancelled.bin',137
token,138
(n, size) {139
if (n > 0) token.cancel();140
},141
);142
await expectLater(upload, throwsA(anything));143
expect(144
(await sftp.list(dir)).map((e) => e.filename),145
isNot(contains('cancelled.bin')),146
);147
expect(148
(await sftp.list(dir)).where((e) => e.filename.endsWith('.part')),149
isEmpty,150
);151
final cancelledFile = File('${temp.path}/cancelled-download.bin');152
final downloadToken = TransferToken();153
await expectLater(154
sftp.download('$dir/renamed.bin', cancelledFile, downloadToken, (155
n,156
size,157
) {158
if (n > 0) downloadToken.cancel();159
}),160
throwsA(anything),161
);162
expect(await cancelledFile.exists(), false);163
await expectLater(164
sftp.upload(165
Stream<Uint8List>.error(const Failure('fileError')),166
0,167
'$dir/source-error.bin',168
TransferToken(),169
(a, b) {},170
),171
throwsA(isA<Failure>()),172
);173
expect(174
(await sftp.list(dir)).where((e) => e.filename.endsWith('.part')),175
isEmpty,176
);177
await sftp.remove('$dir/renamed.bin');178
await sftp.remove(dir);179
// macOS fixture deliberately verifies unsupported Linux adapter behavior.180
if (Platform.isLinux) {181
final metrics = await LinuxMonitor().sample(ssh.client);182
expect(183
metrics.keys,184
containsAll([185
'cpu',186
'ram',187
'disk',188
'uptime',189
'load',190
'system',191
'release',192
'rtt',193
]),194
);195
} else {196
await expectLater(197
LinuxMonitor().sample(ssh.client),198
throwsA(199
isA<Failure>().having((e) => e.code, 'code', 'unsupportedSystem'),200
),201
);202
}203
} finally {204
await temp.delete(recursive: true);205
sftp.client.close();206
}207
ssh.disconnect();208
expect(ssh.connected, false);209
await ssh.connect(210
profile,211
credential: key,212
passphrase: 'isolated-test-passphrase',213
prompt: (a, b, c) async {214
fail('known key must not prompt');215
},216
);217
expect(ssh.connected, true);218
ssh.terminal.onOutput!('exit\r');219
final closed = DateTime.now().add(const Duration(seconds: 5));220
while (ssh.connected && DateTime.now().isBefore(closed)) {221
await Future<void>.delayed(const Duration(milliseconds: 30));222
}223
expect(ssh.connected, false);224
},225
skip: !enabled,226
timeout: const Timeout(Duration(minutes: 2)),227
);229
test(230
'SSH timeout and lock during a pending connection close actual sockets',231
() async {232
final server = await ServerSocket.bind('127.0.0.1', 0);233
final sockets = <Socket>[];234
final subscription = server.listen(sockets.add);235
final service = SshService(236
vault,237
handshakeTimeout: const Duration(milliseconds: 200),238
);239
final target = ServerProfile(240
name: 'Silent peer',241
host: '127.0.0.1',242
username: 'test',243
port: server.port,244
);245
try {246
await service.connect(247
target,248
credential: 'not logged',249
prompt: (a, b, c) async => false,250
);251
expect(service.connected, false);252
expect(service.connecting, false);253
expect(service.error, 'timeout');254
final pending = service.connect(255
target,256
credential: 'not logged',257
prompt: (a, b, c) async => false,258
);259
await Future<void>.delayed(const Duration(milliseconds: 50));260
service.disconnect(clear: true);261
await pending;262
expect(service.connected, false);263
expect(service.profile, null);264
} finally {265
service.dispose();266
for (final s in sockets) {267
s.destroy();268
}269
await subscription.cancel();270
await server.close();271
}272
},273
skip: !enabled,274
);276
test(277
'real host-key change blocks before authentication; bad credentials fail',278
() async {279
final original = vault.fingerprint(profile.endpoint)!;280
await vault.trust(281
profile.endpoint,282
'SHA256:${List.filled(43, 'z').join()}',283
);284
await ssh.connect(285
profile,286
credential: key,287
passphrase: 'isolated-test-passphrase',288
prompt: (a, b, c) async {289
fail('Changed keys must never be offered as first use.');290
},291
);292
expect(ssh.connected, false);293
expect(ssh.error, 'hostChanged');294
await vault.trust(profile.endpoint, original);295
final invalid = ServerProfile(296
name: 'Bad login',297
host: profile.host,298
port: profile.port,299
username: 'nonexistent_sm_${newId()}',300
auth: AuthMethod.password,301
);302
await ssh.connect(303
invalid,304
credential: 'dummy-test-password',305
prompt: (a, b, c) async => false,306
);307
expect(ssh.connected, false);308
expect(ssh.error, 'authFailed');309
},310
skip: !enabled,311
);313
test('transport loss closes the live SSH session', () async {314
final proxy = await ServerSocket.bind('127.0.0.1', 0);315
final peers = <Socket>[];316
final sub = proxy.listen((incoming) async {317
peers.add(incoming);318
final outgoing = await Socket.connect('127.0.0.1', 22229);319
peers.add(outgoing);320
incoming.listen(321
outgoing.add,322
onError: (Object _) {},323
onDone: outgoing.destroy,324
);325
outgoing.listen(326
incoming.add,327
onError: (Object _) {},328
onDone: incoming.destroy,329
);330
});331
try {332
final target = ServerProfile(333
name: 'Loss test',334
host: '127.0.0.1',335
port: proxy.port,336
username: profile.username,337
auth: AuthMethod.privateKey,338
);339
await ssh.connect(340
target,341
credential: key,342
passphrase: 'isolated-test-passphrase',343
prompt: (a, b, c) async => true,344
);345
expect(ssh.connected, true);346
for (final peer in peers) {347
peer.destroy();348
}349
for (var i = 0; i < 100 && ssh.connected; i++) {350
await Future<void>.delayed(const Duration(milliseconds: 20));351
}352
expect(ssh.connected, false);353
expect(() => ssh.client, throwsA(isA<Failure>()));354
} finally {355
for (final peer in peers) {356
peer.destroy();357
}358
await sub.cancel();359
await proxy.close();360
}361
}, skip: !enabled);362
}SHA-256
832d7f22207f680e45ccd7da0d7f16b675a084fdbfe1f776c13f812f0e01c4ceCH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65