diff --git a/lib/core/platform/open_directory.dart b/lib/core/platform/open_directory.dart index 692b6fcb..4e8dd71f 100644 --- a/lib/core/platform/open_directory.dart +++ b/lib/core/platform/open_directory.dart @@ -9,8 +9,12 @@ Future openDirectoryInFileManager( Future 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; diff --git a/lib/core/security/ssl_certificate_support.dart b/lib/core/security/ssl_certificate_support.dart index b51022f4..f4169753 100644 --- a/lib/core/security/ssl_certificate_support.dart +++ b/lib/core/security/ssl_certificate_support.dart @@ -167,36 +167,39 @@ Future 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 _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 cleanupMongoTlsTempFile(String? filePath) async { if (filePath == null || filePath.trim().isEmpty) return; @@ -220,8 +223,30 @@ Future cleanupMongoTlsTempFile(String? filePath) async { } } -/// Cleans up any stale temporary MongoDB TLS directories left from previous sessions. -Future cleanupStaleMongoTlsTempFiles() async { +final _tlsTempOwnerPid = RegExp('^$kMongoTlsTempPrefix(\\d+)_'); + +Future _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 cleanupStaleMongoTlsTempFiles({ + Future Function(int pid)? isProcessAlive, +}) async { + final alive = isProcessAlive ?? _isProcessAlive; try { final tempDir = Directory.systemTemp; if (!await tempDir.exists()) return; @@ -230,6 +255,8 @@ Future 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) { diff --git a/test/core/platform/open_directory_test.dart b/test/core/platform/open_directory_test.dart index b458ebeb..d69bbc16 100644 --- a/test/core/platform/open_directory_test.dart +++ b/test/core/platform/open_directory_test.dart @@ -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_'); diff --git a/test/core/security/ssl_certificate_support_test.dart b/test/core/security/ssl_certificate_support_test.dart index 239e5450..5f57f02c 100644 --- a/test/core/security/ssl_certificate_support_test.dart +++ b/test/core/security/ssl_certificate_support_test.dart @@ -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);