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/platform/open_directory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ Future<bool> openDirectoryInFileManager(
Future<bool> Function(String path)? opener,
}) async {
final dir = Directory(directoryPath);
if (!await dir.exists()) {
await dir.create(recursive: true);
try {
if (!await dir.exists()) {
await dir.create(recursive: true);
}
} on FileSystemException {
return false;
}

final open = opener ?? _defaultOpener;
Expand Down
79 changes: 53 additions & 26 deletions lib/core/security/ssl_certificate_support.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,36 +167,39 @@ Future<String?> resolveMongoTlsCertificateKeyFile({

final certBytes = await File(certPath).readAsString();
final keyBytes = await File(keyPath).readAsString();
final dir = await Directory.systemTemp.createTemp(kMongoTlsTempPrefix);
if (!Platform.isWindows) {
try {
await Process.run('chmod', ['700', dir.path]);
} catch (e) {
logIgnored('ssl_certificate_support', e);
}
}
// The pid in the name lets another instance's start-up cleanup see that this
// directory is in use.
final dir = await Directory.systemTemp
.createTemp('$kMongoTlsTempPrefix${pid}_');
final merged = File('${dir.path}/client.pem');
await merged.writeAsString('$certBytes\n$keyBytes\n', flush: true);
if (!Platform.isWindows) {
// `Process.run` does not throw on a non-zero exit, so check it: a key
// file that stayed at the default mode must not be handed out silently.
try {
final result = await Process.run('chmod', ['600', merged.path]);
if (result.exitCode != 0) {
throw FileSystemException(
'Could not restrict permissions of the TLS client key file: '
'${result.stderr}'.trim(),
merged.path,
);
}
} catch (_) {
await cleanupMongoTlsTempFile(merged.path);
rethrow;
try {
if (!Platform.isWindows) {
await _chmod('700', dir.path, 'TLS temp directory');
// Empty and 0600 before the key goes in, so it is never readable with
// the default mode.
await merged.create();
await _chmod('600', merged.path, 'TLS client key file');
}
await merged.writeAsString('$certBytes\n$keyBytes\n', flush: true);
} catch (_) {
await cleanupMongoTlsTempFile(merged.path);
rethrow;
}
return merged.path;
}

/// `Process.run` does not throw on a non-zero exit, so check it: a key file
/// that stayed at the default mode must not be handed out silently.
Future<void> _chmod(String mode, String path, String what) async {
final result = await Process.run('chmod', [mode, path]);
if (result.exitCode != 0) {
throw FileSystemException(
'Could not restrict permissions of the $what: ${result.stderr}'.trim(),
path,
);
}
}

/// Reliably deletes a temporary MongoDB TLS certificate file and its parent temp directory.
Future<void> cleanupMongoTlsTempFile(String? filePath) async {
if (filePath == null || filePath.trim().isEmpty) return;
Expand All @@ -220,8 +223,30 @@ Future<void> cleanupMongoTlsTempFile(String? filePath) async {
}
}

/// Cleans up any stale temporary MongoDB TLS directories left from previous sessions.
Future<void> cleanupStaleMongoTlsTempFiles() async {
final _tlsTempOwnerPid = RegExp('^$kMongoTlsTempPrefix(\\d+)_');

Future<bool> _isProcessAlive(int owner) async {
if (owner == pid) return true;
try {
if (Platform.isWindows) {
final result =
await Process.run('tasklist', ['/FI', 'PID eq $owner', '/NH']);
return result.exitCode != 0 || '${result.stdout}'.contains('$owner');
}
return (await Process.run('kill', ['-0', '$owner'])).exitCode == 0;
} catch (_) {
// Cannot tell: leave the directory alone.
return true;
}
}

/// Cleans up stale temporary MongoDB TLS directories left from previous
/// sessions. A directory named for a process that is still running belongs to
/// another Querya instance and is kept; [isProcessAlive] is for tests.
Future<void> cleanupStaleMongoTlsTempFiles({
Future<bool> Function(int pid)? isProcessAlive,
}) async {
final alive = isProcessAlive ?? _isProcessAlive;
try {
final tempDir = Directory.systemTemp;
if (!await tempDir.exists()) return;
Expand All @@ -230,6 +255,8 @@ Future<void> cleanupStaleMongoTlsTempFiles() async {
final name =
entity.uri.pathSegments.where((s) => s.isNotEmpty).lastOrNull ?? '';
if (name.startsWith(kMongoTlsTempPrefix)) {
final owner = _tlsTempOwnerPid.firstMatch(name)?.group(1);
if (owner != null && await alive(int.parse(owner))) continue;
try {
await entity.delete(recursive: true);
} catch (e) {
Expand Down
17 changes: 17 additions & 0 deletions test/core/platform/open_directory_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ void main() {
expect(await Directory(target).exists(), isTrue);
});

test('returns false when the directory cannot be created (#1768)',
() async {
final root = await Directory.systemTemp
.createTemp('querya_open_directory_blocked_test_');
addTearDown(() => root.delete(recursive: true));
final file = File(p.join(root.path, 'a_file'))..writeAsStringSync('x');
var called = false;

final opened = await openDirectoryInFileManager(
p.join(file.path, 'child'),
opener: (_) async => called = true,
);

expect(opened, isFalse);
expect(called, isFalse);
});

test('returns false when opener reports failure', () async {
final root = await Directory.systemTemp
.createTemp('querya_open_directory_fail_test_');
Expand Down
36 changes: 36 additions & 0 deletions test/core/security/ssl_certificate_support_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,42 @@ void main() {
expect(await orphanDir.exists(), isFalse);
});

test('cleanup keeps the directory of a process that is still running (#1768)',
() async {
final live = await Directory.systemTemp
.createTemp('${kMongoTlsTempPrefix}4242_');
final dead = await Directory.systemTemp
.createTemp('${kMongoTlsTempPrefix}4243_');
addTearDown(() async {
if (await live.exists()) await live.delete(recursive: true);
if (await dead.exists()) await dead.delete(recursive: true);
});

await cleanupStaleMongoTlsTempFiles(
isProcessAlive: (pid) async => pid == 4242,
);

expect(await live.exists(), isTrue);
expect(await dead.exists(), isFalse);
});

test('the merged key is created in a directory named for this process (#1768)',
() async {
final input = await Directory.systemTemp.createTemp('querya_test_input_');
addTearDown(() => input.delete(recursive: true));
await File('${input.path}/c.pem').writeAsString('CERT');
await File('${input.path}/k.pem').writeAsString('KEY');

final path = await resolveMongoTlsCertificateKeyFile(
clientCert: '${input.path}/c.pem',
clientKey: '${input.path}/k.pem',
);
addTearDown(() => cleanupMongoTlsTempFile(path));

expect(File(path!).parent.path, contains('$kMongoTlsTempPrefix$pid'));
expect(await File(path).readAsString(), contains('KEY'));
});

test('buildSecurityContext returns null when paths.hasAny is false', () {
const paths = SslCertificatePaths();
final ctx = buildSecurityContext(paths);
Expand Down
Loading