Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: wrap all dart:io calls #568

Merged
merged 1 commit into from
Nov 15, 2023
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
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