Skip to content

Commit

Permalink
Merge pull request #568 from atsign-foundation/io-mocking
Browse files Browse the repository at this point in the history
refactor: wrap all dart:io calls
  • Loading branch information
XavierChanth authored Nov 15, 2023
2 parents 1f47357 + a6566ae commit aca13fd
Show file tree
Hide file tree
Showing 18 changed files with 143 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:meta/meta.dart';
import 'package:noports_core/src/common/io_types.dart';
import 'package:noports_core/sshnp.dart';
import 'package:noports_core/utils.dart';
import 'package:path/path.dart' as path;
Expand All @@ -14,10 +15,17 @@ class LocalSshKeyUtil implements AtSshKeyUtil {

static final Map<String, AtSshKeyPair> _keyPairCache = {};

@visibleForTesting
final FileSystem fs;

final String homeDirectory;
bool cacheKeys;
LocalSshKeyUtil({String? homeDirectory, this.cacheKeys = true})
: homeDirectory = homeDirectory ?? getHomeDirectory(throwIfNull: true)!;

LocalSshKeyUtil({
String? homeDirectory,
this.cacheKeys = true,
@visibleForTesting this.fs = const LocalFileSystem(),
}) : homeDirectory = homeDirectory ?? getHomeDirectory(throwIfNull: true)!;

bool get isValidPlatform =>
Platform.isLinux || Platform.isMacOS || Platform.isWindows;
Expand All @@ -31,8 +39,8 @@ class LocalSshKeyUtil implements AtSshKeyUtil {

List<File> _filesFromIdentifier({required String identifier}) {
return [
File(path.normalize(identifier)),
File(path.normalize('$identifier.pub')),
fs.file(path.normalize(identifier)),
fs.file(path.normalize('$identifier.pub')),
];
}

Expand Down Expand Up @@ -90,17 +98,18 @@ class LocalSshKeyUtil implements AtSshKeyUtil {
SupportedSshAlgorithm algorithm = DefaultArgs.sshAlgorithm,
String? directory,
String? passphrase,
@visibleForTesting ProcessRunner processRunner = Process.run,
}) async {
String workingDirectory = directory ?? _defaultDirectory;

await Process.run(
await processRunner(
'ssh-keygen',
[..._sshKeygenArgMap[algorithm]!, '-f', identifier, '-q', '-N', ''],
workingDirectory: workingDirectory,
);

String pemText =
await File(path.join(workingDirectory, identifier)).readAsString();
await fs.file(path.join(workingDirectory, identifier)).readAsString();

return AtSshKeyPair.fromPem(
pemText,
Expand All @@ -122,13 +131,13 @@ class LocalSshKeyUtil implements AtSshKeyUtil {
throw ('$sshPublicKey does not look like a public key');
}

if (!Directory(sshHomeDirectory).existsSync()) {
Directory(sshHomeDirectory).createSync();
if (!fs.directory(sshHomeDirectory).existsSync()) {
fs.directory(sshHomeDirectory).createSync();
}

// Check to see if the ssh Publickey is already in the authorized_keys file.
// If not, then append it.
var authKeys = File(path.normalize('$sshHomeDirectory/authorized_keys'));
var authKeys = fs.file(path.normalize('$sshHomeDirectory/authorized_keys'));

var authKeysContent = await authKeys.readAsString();
if (!authKeysContent.endsWith('\n')) {
Expand Down Expand Up @@ -157,7 +166,7 @@ class LocalSshKeyUtil implements AtSshKeyUtil {
Future<void> deauthorizePublicKey(String sessionId) async {
try {
final File file =
File(path.normalize('$sshHomeDirectory/authorized_keys'));
fs.file(path.normalize('$sshHomeDirectory/authorized_keys'));
// read into List of strings
final List<String> lines = await file.readAsLines();
// find the line we want to remove
Expand Down
3 changes: 1 addition & 2 deletions packages/noports_core/lib/src/common/default_args.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:io';

import 'package:noports_core/src/common/io_types.dart';
import 'package:noports_core/src/common/types.dart';
import 'package:noports_core/sshrv.dart';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:io';
import 'package:noports_core/src/common/io_types.dart';
import 'package:path/path.dart' as path;

/// Get the home directory or null if unknown.
Expand Down
23 changes: 23 additions & 0 deletions packages/noports_core/lib/src/common/io_types.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// This file contains all of the dart:io calls in noports_core
/// All io used should be wrapped for the sake of testing and compatibilty
import 'dart:io' show Process, ProcessResult, ProcessStartMode;
import 'package:meta/meta.dart';

export 'dart:io' show Platform, Process, ProcessStartMode, ServerSocket, InternetAddress;
export 'package:file/file.dart';
export 'package:file/local.dart' show LocalFileSystem;

@internal
typedef ProcessRunner = Future<ProcessResult> Function(
String executable,
List<String> arguments, {
String? workingDirectory,
});

@internal
typedef ProcessStarter = Future<Process> Function(
String executable,
List<String> arguments, {
bool runInShell,
ProcessStartMode mode,
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:io';
import 'package:noports_core/src/common/io_types.dart';

const String _windowsOpensshPath = r'C:\Windows\System32\OpenSSH\ssh.exe';
const String _unixOpensshPath = '/usr/bin/ssh';
Expand Down
48 changes: 31 additions & 17 deletions packages/noports_core/lib/src/common/validation_utils.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'dart:convert';
import 'dart:io';

import 'package:at_chops/at_chops.dart';
import 'package:at_client/at_client.dart';
import 'package:at_utils/at_utils.dart';

import 'package:noports_core/src/common/file_system_utils.dart';
import 'package:noports_core/src/common/io_types.dart';
import 'package:path/path.dart' as path;

const String sshnpDeviceNameRegex = r'[a-zA-Z0-9_]{0,15}';
Expand Down Expand Up @@ -66,15 +68,18 @@ String signAndWrapAndJsonEncode(AtClient atClient, Map payload) {
return jsonEncode(envelope);
}

Future<void> verifyEnvelopeSignature(AtClient atClient, String requestingAtsign,
AtSignLogger logger, Map envelope,
{bool useFileStorage = true}) async {
Future<void> verifyEnvelopeSignature(
AtClient atClient,
String requestingAtsign,
AtSignLogger logger,
Map envelope, {
FileSystem? fs,
}) async {
final String signature = envelope['signature'];
Map payload = envelope['payload'];
final hashingAlgo = HashingAlgoType.values.byName(envelope['hashingAlgo']);
final signingAlgo = SigningAlgoType.values.byName(envelope['signingAlgo']);
final pk = await getLocallyCachedPK(atClient, requestingAtsign,
useFileStorage: useFileStorage);
final pk = await getLocallyCachedPK(atClient, requestingAtsign, fs: fs);
AtSigningVerificationInput input = AtSigningVerificationInput(
jsonEncode(payload), base64Decode(signature), pk)
..signingMode = AtSigningMode.data
Expand All @@ -101,12 +106,14 @@ Future<void> verifyEnvelopeSignature(AtClient atClient, String requestingAtsign,
/// `~/.atsign/sshnp/cached_pks/alice`
///
/// Note that for storage, the leading `@` in the atSign is stripped off.
Future<String> getLocallyCachedPK(AtClient atClient, String atSign,
{bool useFileStorage = true}) async {
Future<String> getLocallyCachedPK(
AtClient atClient,
String atSign, {
FileSystem? fs,
}) async {
atSign = AtUtils.fixAtSign(atSign);

String? cachedPK =
await _fetchFromLocalPKCache(atClient, atSign, useFileStorage);
String? cachedPK = await _fetchFromLocalPKCache(atClient, atSign, fs: fs);
if (cachedPK != null) {
return cachedPK;
}
Expand All @@ -117,18 +124,21 @@ Future<String> getLocallyCachedPK(AtClient atClient, String atSign,
throw AtPublicKeyNotFoundException('Failed to retrieve $s');
}

await _storeToLocalPKCache(av.value, atClient, atSign, useFileStorage);
await _storeToLocalPKCache(av.value, atClient, atSign, fs: fs);

return av.value;
}

Future<String?> _fetchFromLocalPKCache(
AtClient atClient, String atSign, bool useFileStorage) async {
AtClient atClient,
String atSign, {
FileSystem? fs,
}) async {
String dontAtMe = atSign.substring(1);
if (useFileStorage) {
if (fs != null) {
String fn = path
.normalize('${getHomeDirectory()}/.atsign/sshnp/cached_pks/$dontAtMe');
File f = File(fn);
File f = fs.file(fn);
if (await f.exists()) {
return (await f.readAsString()).trim();
} else {
Expand All @@ -147,14 +157,18 @@ Future<String?> _fetchFromLocalPKCache(
}

Future<bool> _storeToLocalPKCache(
String pk, AtClient atClient, String atSign, bool useFileStorage) async {
String pk,
AtClient atClient,
String atSign, {
FileSystem? fs,
}) async {
String dontAtMe = atSign.substring(1);
if (useFileStorage) {
if (fs != null) {
String dirName =
path.normalize('${getHomeDirectory()}/.atsign/sshnp/cached_pks');
String fileName = path.normalize('$dirName/$dontAtMe');

File f = File(fileName);
File f = fs.file(fileName);
if (!await f.exists()) {
await f.create(recursive: true);
await Process.run('chmod', ['-R', 'go-rwx', dirName]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'dart:async';
import 'dart:io';

import 'package:at_client/at_client.dart';
import 'package:meta/meta.dart';
import 'package:noports_core/src/common/io_types.dart';
import 'package:noports_core/sshnp_foundation.dart';

class SshnpOpensshLocalImpl extends SshnpCore
Expand Down Expand Up @@ -34,10 +35,30 @@ class SshnpOpensshLocalImpl extends SshnpCore
@override
Future<void> initialize() async {
if (!isSafeToInitialize) return;
await findLocalPortIfRequired();
await super.initialize();
completeInitialization();
}

@visibleForTesting
Future<void> findLocalPortIfRequired() async {
// find a spare local port
if (localPort == 0) {
logger.info('Finding a spare local port');
try {
ServerSocket serverSocket =
await ServerSocket.bind(InternetAddress.loopbackIPv4, 0)
.catchError((e) => throw e);
localPort = serverSocket.port;
await serverSocket.close().catchError((e) => throw e);
} catch (e, s) {
logger.info('Unable to find a spare local port');
throw SshnpError('Unable to find a spare local port',
error: e, stackTrace: s);
}
}
}

@override
Future<SshnpResult> run() async {
/// Ensure that sshnp is initialized
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:at_client/at_client.dart';
import 'package:noports_core/src/common/io_types.dart';
import 'package:noports_core/sshnp_foundation.dart';

class SshnpUnsignedImpl extends SshnpCore with SshnpLocalSshKeyHandler {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:io';

import 'package:meta/meta.dart';
import 'package:noports_core/src/common/file_system_utils.dart';
import 'package:noports_core/src/common/io_types.dart';
import 'package:noports_core/src/sshnp/models/sshnp_params.dart';
import 'package:noports_core/src/sshnp/models/sshnp_arg.dart';
import 'package:path/path.dart' as path;
Expand Down Expand Up @@ -30,19 +30,25 @@ class ConfigFileRepository {
);
}

static Future<Directory> createConfigDirectory({String? directory}) async {
static Future<Directory> createConfigDirectory({
String? directory,
@visibleForTesting FileSystem fs = const LocalFileSystem(),
}) async {
directory ??= getDefaultSshnpConfigDirectory(getHomeDirectory()!);
var dir = Directory(directory);
var dir = fs.directory(directory);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
return dir;
}

static Future<Iterable<String>> listProfiles({String? directory}) async {
static Future<Iterable<String>> listProfiles({
String? directory,
@visibleForTesting FileSystem fs = const LocalFileSystem(),
}) async {
var profileNames = <String>{};
directory ??= getDefaultSshnpConfigDirectory(getHomeDirectory()!);
var files = Directory(directory).list();
var files = fs.directory(directory).list();

await files.forEach((file) {
if (file is! File) return;
Expand All @@ -62,15 +68,19 @@ class ConfigFileRepository {
return SshnpParams.fromFile(fileName);
}

static Future<File> putParams(SshnpParams params,
{String? directory, bool overwrite = false}) async {
static Future<File> putParams(
SshnpParams params, {
String? directory,
bool overwrite = false,
@visibleForTesting FileSystem fs = const LocalFileSystem(),
}) async {
if (params.profileName == null || params.profileName!.isEmpty) {
throw Exception('profileName is null or empty');
}

var fileName =
await fromProfileName(params.profileName!, directory: directory);
var file = File(fileName);
var file = fs.file(fileName);

var exists = await file.exists();

Expand All @@ -87,15 +97,18 @@ class ConfigFileRepository {
);
}

static Future<FileSystemEntity> deleteParams(SshnpParams params,
{String? directory}) async {
static Future<FileSystemEntity> deleteParams(
SshnpParams params, {
String? directory,
@visibleForTesting FileSystem fs = const LocalFileSystem(),
}) async {
if (params.profileName == null || params.profileName!.isEmpty) {
throw Exception('profileName is null or empty');
}

var fileName =
await fromProfileName(params.profileName!, directory: directory);
var file = File(fileName);
var file = fs.file(fileName);

var exists = await file.exists();

Expand All @@ -106,8 +119,11 @@ class ConfigFileRepository {
return file.delete();
}

static Map<String, dynamic> parseConfigFile(String fileName) {
File file = File(fileName);
static Map<String, dynamic> parseConfigFile(
String fileName, {
@visibleForTesting FileSystem fs = const LocalFileSystem(),
}) {
File file = fs.file(fileName);

if (!file.existsSync()) {
throw Exception('Config file does not exist: $fileName');
Expand Down
3 changes: 1 addition & 2 deletions packages/noports_core/lib/src/sshnp/models/sshnp_result.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:io';

import 'package:meta/meta.dart';
import 'package:noports_core/src/common/io_types.dart';
import 'package:socket_connector/socket_connector.dart';

abstract class SshnpResult {}
Expand Down
Loading

0 comments on commit aca13fd

Please sign in to comment.