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
107 changes: 101 additions & 6 deletions lib/core/database/mongodb_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_config.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_manager.dart';
import 'package:querya_desktop/core/security/ssl_certificate_support.dart';
import 'package:querya_desktop/core/storage/connection_secrets_store.dart';
import 'package:querya_desktop/core/storage/local_db.dart';

/// MongoDB connection configuration and state.
class MongoConnection {
Expand All @@ -19,9 +22,30 @@ class MongoConnection {
this.useSSL = false,
this.replicaSet,
String? connectionString,
this.sshConfig,
this.sshSecrets,
}) : _password = password,
_connectionString = connectionString;

factory MongoConnection.fromConnectionRow(
ConnectionRow row, {
String? database,
}) {
return MongoConnection(
id: row.id ?? 0,
name: row.name,
host: row.host ?? 'localhost',
port: row.port ?? 27017,
username: row.username,
password: row.password,
database: database ?? row.databaseName,
authSource: row.authSource,
useSSL: row.useSSL,
connectionString: row.connectionString,
sshConfig: row.sshTunnelConfig,
);
}

final int id;
final String name;
final String host;
Expand All @@ -34,6 +58,12 @@ class MongoConnection {
final String? replicaSet;
String? _connectionString;

final SshTunnelConfig? sshConfig;
final SshTunnelSecrets? sshSecrets;
SshTunnelHandle? _sshTunnelHandle;

SshTunnelHandle? get sshTunnelHandle => _sshTunnelHandle;

/// Handshake URI for this live session (includes auth). Not persisted; not
/// exposed via [password] / [connectionString] after [scrubCredentials].
String? _sessionUri;
Expand Down Expand Up @@ -79,12 +109,24 @@ class MongoConnection {
}

/// Builds MongoDB connection URI from configuration.
String buildConnectionUri({String? pass, String? connStr}) {
String buildConnectionUri({
String? pass,
String? connStr,
String? hostOverride,
int? portOverride,
}) {
final effectiveConnStr = connStr ?? _connectionString;
if (effectiveConnStr != null && effectiveConnStr.isNotEmpty) {
if (hostOverride != null) {
final parsed = Uri.parse(effectiveConnStr);
return parsed
.replace(host: hostOverride, port: portOverride)
.toString();
}
return effectiveConnStr;
}
if (pass == null &&
hostOverride == null &&
_sessionUri != null &&
_sessionUri!.isNotEmpty &&
(_password == null || _password!.isEmpty)) {
Expand All @@ -104,9 +146,10 @@ class MongoConnection {
}

// Add host and port
buffer.write(host);
if (port != 27017) {
buffer.write(':$port');
buffer.write(hostOverride ?? host);
final effectivePort = portOverride ?? port;
if (effectivePort != 27017 || hostOverride != null) {
buffer.write(':$effectivePort');
}

// Add database
Expand Down Expand Up @@ -187,9 +230,41 @@ class MongoConnection {
}

try {
if (sshConfig != null && sshConfig!.enabled) {
var sec = sshSecrets;
if (sec == null && id > 0) {
final stored =
await ConnectionSecretsStore.readSshSecretsForConnection(id);
sec = SshTunnelSecrets(
password: stored.password,
privateKey: stored.privateKey,
passphrase: stored.passphrase,
jumpPassword: stored.jumpPassword,
);
}
String targetHost = host;
int targetPort = port;
if (effectiveConnectionString != null &&
effectiveConnectionString.trim().isNotEmpty) {
final uri = Uri.tryParse(effectiveConnectionString.trim());
if (uri != null && uri.host.isNotEmpty) {
targetHost = uri.host;
targetPort = uri.hasPort ? uri.port : 27017;
}
}
_sshTunnelHandle = await SshTunnelManager.instance.openTunnel(
config: sshConfig!,
secrets: sec ?? SshTunnelSecrets(),
remoteHost: targetHost,
remotePort: targetPort,
);
}

final uri = await _effectiveMongoUri(
pass: effectivePassword,
connStr: effectiveConnectionString,
hostOverride: _sshTunnelHandle?.localHost,
portOverride: _sshTunnelHandle?.localPort,
);
_db = await Db.create(uri);
await _db!.open();
Expand All @@ -204,12 +279,26 @@ class MongoConnection {
_isConnected = false;
_db = null;
await _cleanupTempTlsKey();
try {
await _sshTunnelHandle?.release();
} catch (_) {}
_sshTunnelHandle = null;
rethrow;
}
}

Future<String> _effectiveMongoUri({String? pass, String? connStr}) async {
final base = buildConnectionUri(pass: pass, connStr: connStr);
Future<String> _effectiveMongoUri({
String? pass,
String? connStr,
String? hostOverride,
int? portOverride,
}) async {
final base = buildConnectionUri(
pass: pass,
connStr: connStr,
hostOverride: hostOverride,
portOverride: portOverride,
);
final parsed = Uri.parse(base);
final paths = extractSslCertificatePaths(parsed);
final params = Map<String, String>.from(parsed.queryParameters);
Expand Down Expand Up @@ -261,6 +350,12 @@ class MongoConnection {
}
}
await _cleanupTempTlsKey();
try {
await _sshTunnelHandle?.release();
} catch (e) {
debugPrint('MongoConnection.sshRelease: $e');
}
_sshTunnelHandle = null;
}

Future<void> _cleanupTempTlsKey() async {
Expand Down
13 changes: 1 addition & 12 deletions lib/core/database/mongodb_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,7 @@ class MongoService {
existing.disconnect(); // fire-and-forget; disconnect is safe
}

final connection = MongoConnection(
id: id,
name: row.name,
host: row.host ?? 'localhost',
port: row.port ?? 27017,
username: row.username,
password: row.password,
database: row.databaseName,
authSource: row.authSource,
useSSL: row.useSSL,
connectionString: row.connectionString,
);
final connection = MongoConnection.fromConnectionRow(row);

_connections[connection.id] = connection;
return connection;
Expand Down
62 changes: 56 additions & 6 deletions lib/core/database/mysql_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import 'package:mysql_client/mysql_client.dart';
import 'package:querya_desktop/core/database/mysql_result_cells.dart';
import 'package:querya_desktop/core/database/table_schema_meta.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_config.dart';
import 'package:querya_desktop/core/security/ssh_tunnel_manager.dart';
import 'package:querya_desktop/core/security/ssl_certificate_support.dart';
import 'package:querya_desktop/core/storage/connection_secrets_store.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
Expand Down Expand Up @@ -99,6 +101,8 @@ class MysqlConnection {
this.database,
this.useSSL = true,
String? connectionString,
this.sshConfig,
this.sshSecrets,
}) : _password = password,
_connectionString = connectionString;

Expand All @@ -116,6 +120,7 @@ class MysqlConnection {
database: database ?? row.databaseName,
useSSL: row.useSSL,
connectionString: row.connectionString,
sshConfig: row.sshTunnelConfig,
);
}

Expand All @@ -129,6 +134,12 @@ class MysqlConnection {
final bool useSSL;
String? _connectionString;

final SshTunnelConfig? sshConfig;
final SshTunnelSecrets? sshSecrets;
SshTunnelHandle? _sshTunnelHandle;

SshTunnelHandle? get sshTunnelHandle => _sshTunnelHandle;

String? get password => _password;
String? get connectionString => _connectionString;

Expand Down Expand Up @@ -170,6 +181,26 @@ class MysqlConnection {
}

try {
if (sshConfig != null && sshConfig!.enabled) {
var sec = sshSecrets;
if (sec == null && id > 0) {
final stored =
await ConnectionSecretsStore.readSshSecretsForConnection(id);
sec = SshTunnelSecrets(
password: stored.password,
privateKey: stored.privateKey,
passphrase: stored.passphrase,
jumpPassword: stored.jumpPassword,
);
}
_sshTunnelHandle = await SshTunnelManager.instance.openTunnel(
config: sshConfig!,
secrets: sec ?? SshTunnelSecrets(),
remoteHost: host,
remotePort: port,
);
}

final user = username ?? '';
final pass = effectivePassword ?? '';
if (_usesConnectionString(effectiveConnectionString)) {
Expand All @@ -188,26 +219,29 @@ class MysqlConnection {
}
validateMysqlSslMode(sslMode, sslPaths);
final securityContext = buildSecurityContext(sslPaths);
final host = parsed.host;
final effectiveHost = _sshTunnelHandle?.localHost ?? parsed.host;
final effectivePort = _sshTunnelHandle?.localPort ?? parsed.port;
_conn = await MySQLConnection.createConnection(
host: host,
port: parsed.port,
host: effectiveHost,
port: effectivePort,
userName: parsed.userName,
password: parsed.password,
secure: sslMode.secure,
databaseName: parsed.databaseName,
securityContext: securityContext,
sslVerifyCertificates: sslMode.verifyCertificates,
sslServerName: sslMode.verifyIdentity && host is String ? host : null,
sslServerName: sslMode.verifyIdentity && parsed.host is String ? parsed.host : null,
);
await _conn!.connect(timeoutMs: connectTimeoutMs);
} else {
final sslPaths =
extractSslCertificatePathsFromString(effectiveConnectionString);
final securityContext = buildSecurityContext(sslPaths);
final effectiveHost = _sshTunnelHandle?.localHost ?? host;
final effectivePort = _sshTunnelHandle?.localPort ?? port;
_conn = await MySQLConnection.createConnection(
host: host,
port: port,
host: effectiveHost,
port: effectivePort,
userName: user,
password: pass,
secure: useSSL || sslPaths.hasAny,
Expand All @@ -221,6 +255,10 @@ class MysqlConnection {
} catch (e) {
_isConnected = false;
_conn = null;
try {
await _sshTunnelHandle?.release();
} catch (_) {}
_sshTunnelHandle = null;
rethrow;
}
}
Expand Down Expand Up @@ -315,6 +353,12 @@ class MysqlConnection {
} catch (e) {
debugPrint('MysqlConnection.disconnect: $e');
}
try {
await _sshTunnelHandle?.release();
} catch (e) {
debugPrint('MysqlConnection.sshRelease: $e');
}
_sshTunnelHandle = null;
}

/// Best-effort close. The `mysql_client` driver may not allow graceful [close]
Expand All @@ -332,6 +376,12 @@ class MysqlConnection {
} catch (e) {
debugPrint('MysqlConnection.forceClose: $e');
}
try {
await _sshTunnelHandle?.release();
} catch (e) {
debugPrint('MysqlConnection.forceClose ssh: $e');
}
_sshTunnelHandle = null;
}

/// Session hint for Table Browser / tree (`MysqlSessionMode.readOnly`).
Expand Down
Loading
Loading