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
90 changes: 47 additions & 43 deletions lib/core/database/mongodb_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:querya_desktop/core/database/database_error_mapper.dart';
import 'package:querya_desktop/core/database/mongodb_uri.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';
Expand Down Expand Up @@ -88,13 +89,8 @@ class MongoConnection {
}
final rawUri = _sessionUri ?? _connectionString;
if (rawUri != null && rawUri.isNotEmpty) {
try {
final parsed = Uri.parse(rawUri);
final path = parsed.path.replaceFirst('/', '').trim();
if (path.isNotEmpty) {
return path;
}
} catch (_) {}
final name = MongoUri.tryParse(rawUri)?.databaseName?.trim();
if (name != null && name.isNotEmpty) return name;
}
return null;
}
Expand All @@ -119,10 +115,14 @@ class MongoConnection {
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();
// Through an SSH tunnel: the one local endpoint, whatever seeds the
// string lists (#1309).
final parsed = MongoUri.tryParse(effectiveConnStr);
if (parsed == null) {
throw FormatException(
'Not a mongodb:// connection string', effectiveConnStr);
}
return parsed.forTunnel(hostOverride, portOverride ?? 27017).toString();
}
return effectiveConnStr;
}
Expand Down Expand Up @@ -185,30 +185,28 @@ class MongoConnection {
/// one the user was created in.
String buildUriForDatabase(String databaseName, {String? pass, String? connStr}) {
final baseUri = buildConnectionUri(pass: pass, connStr: connStr);
final uri = Uri.parse(baseUri);
final uri = MongoUri.tryParse(baseUri);
if (uri == null) {
throw FormatException('Not a mongodb:// connection string', baseUri);
}

// Determine the authSource that should be used.
// 1) Already present in the query → keep it.
// 2) Not present but credentials exist → use the original path db, or
// fall back to "admin" (Mongo's default authSource).
final existingAuthSource = uri.queryParameters['authSource'];
final hasCredentials =
uri.userInfo.isNotEmpty || (username != null && username!.isNotEmpty);

Map<String, String>? newQueryParams;
if (existingAuthSource == null && hasCredentials) {
// Original db from the URI path (strip leading '/')
final origDb = uri.path.replaceFirst('/', '');
final source = (origDb.isNotEmpty) ? origDb : 'admin';
newQueryParams = Map<String, String>.from(uri.queryParameters)
..['authSource'] = source;
}

final newUri = uri.replace(
path: '/$databaseName',
queryParameters: newQueryParams ?? uri.queryParameters,
);
return newUri.toString();
final hasCredentials = uri.userInfo != null ||
(username != null && username!.isNotEmpty);
final params = Map<String, String>.of(uri.params);
if (!params.containsKey('authSource') && hasCredentials) {
params['authSource'] = uri.databaseName ?? 'admin';
}

return uri
.copyWith(
path: '/${Uri.encodeComponent(databaseName)}',
params: params,
)
.toString();
}

/// Connects to MongoDB server.
Expand Down Expand Up @@ -247,10 +245,13 @@ class MongoConnection {
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;
// The first seed; an SRV string has none a tunnel could reach and
// is refused when the URI is built.
final uri = MongoUri.tryParse(effectiveConnectionString.trim());
if (uri != null) {
final seed = uri.firstHost;
targetHost = seed.host;
targetPort = seed.port ?? 27017;
}
}
_sshTunnelHandle = await SshTunnelManager.instance.openTunnel(
Expand Down Expand Up @@ -300,9 +301,16 @@ class MongoConnection {
hostOverride: hostOverride,
portOverride: portOverride,
);
final parsed = Uri.parse(base);
final paths = extractSslCertificatePaths(parsed);
final params = Map<String, String>.from(parsed.queryParameters);
final parsed = MongoUri.tryParse(base);
if (parsed == null) {
throw FormatException('Not a mongodb:// connection string', base);
}
final paths = SslCertificatePaths(
rootCert: parsed.params[kSslRootCertParam],
clientCert: parsed.params[kSslCertParam],
clientKey: parsed.params[kSslKeyParam],
);
final params = Map<String, String>.from(parsed.params);
params.remove(kSslRootCertParam);
params.remove(kSslCertParam);
params.remove(kSslKeyParam);
Expand All @@ -327,9 +335,7 @@ class MongoConnection {
params['ssl'] = 'true';
}

return parsed
.replace(queryParameters: params.isEmpty ? null : params)
.toString();
return parsed.copyWith(params: params).toString();
}

/// Disconnects from MongoDB server.
Expand Down Expand Up @@ -405,9 +411,7 @@ class MongoConnection {
}

static String? _databaseNameFromUri(String uri) {
final path = Uri.tryParse(uri)?.path.replaceFirst(RegExp(r'^/'), '') ?? '';
if (path.isEmpty) return null;
return path.split('/').first;
return MongoUri.tryParse(uri)?.databaseName;
}

/// Checks if connection is active.
Expand Down
175 changes: 175 additions & 0 deletions lib/core/database/mongodb_uri.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/// A MongoDB connection string, split without `Uri.parse`.
///
/// `mongodb://h1:27017,h2:27017/db?replicaSet=rs0` is valid MongoDB but not a
/// URI Dart accepts: the host list reads as a port (`Uri.tryParse` is null,
/// `Uri.parse` throws), so replica-set strings could not be imported, opened
/// or tunnelled (#1309). The pieces here are kept as written: the user info
/// stays percent-encoded.
class MongoUri {
const MongoUri._({
required this.scheme,
required this.userInfo,
required this.hosts,
required this.path,
required this.params,
});

/// `mongodb` or `mongodb+srv`, lower case.
final String scheme;

/// `user:password` as written (percent-encoded), or null.
final String? userInfo;

/// `host`, `host:port` or `[::1]:port`, in order.
final List<String> hosts;

/// `''` or `/database`.
final String path;

/// Query parameters, decoded, in order.
final Map<String, String> params;

bool get isSrv => scheme == 'mongodb+srv';

/// The database of the path, if there is one.
String? get databaseName {
final name = path.replaceFirst(RegExp(r'^/'), '').split('/').first;
if (name.isEmpty) return null;
try {
return Uri.decodeComponent(name);
} catch (_) {
return name;
}
}

/// Null when [raw] is not a `mongodb://` / `mongodb+srv://` string with at
/// least one host.
static MongoUri? tryParse(String raw) {
final m = RegExp(r'^(mongodb(?:\+srv)?)://(.*)$',
caseSensitive: false, dotAll: true)
.firstMatch(raw.trim());
if (m == null) return null;
var rest = m.group(2)!;
final hash = rest.indexOf('#');
if (hash >= 0) rest = rest.substring(0, hash);

var end = rest.length;
for (final stop in ['/', '?']) {
final i = rest.indexOf(stop);
if (i >= 0 && i < end) end = i;
}
var authority = rest.substring(0, end);
final tail = rest.substring(end);

String? userInfo;
final at = authority.lastIndexOf('@');
if (at >= 0) {
userInfo = authority.substring(0, at);
authority = authority.substring(at + 1);
}
final hosts = [
for (final h in authority.split(','))
if (h.trim().isNotEmpty) h.trim(),
];
if (hosts.isEmpty) return null;

var path = '';
var query = '';
final q = tail.indexOf('?');
if (q >= 0) {
path = tail.substring(0, q);
query = tail.substring(q + 1);
} else {
path = tail;
}

final params = <String, String>{};
for (final pair in query.split('&')) {
if (pair.isEmpty) continue;
final eq = pair.indexOf('=');
final key = eq < 0 ? pair : pair.substring(0, eq);
final value = eq < 0 ? '' : pair.substring(eq + 1);
params[_decode(key)] = _decode(value);
}
return MongoUri._(
scheme: m.group(1)!.toLowerCase(),
userInfo: userInfo != null && userInfo.isNotEmpty ? userInfo : null,
hosts: hosts,
path: path,
params: params,
);
}

static String _decode(String v) {
try {
return Uri.decodeQueryComponent(v);
} catch (_) {
return v;
}
}

/// The first seed: host and port (null when none is written).
({String host, int? port}) get firstHost {
final h = hosts.first;
if (h.startsWith('[')) {
final close = h.indexOf(']');
if (close > 0) {
final port = h.length > close + 2 && h[close + 1] == ':'
? int.tryParse(h.substring(close + 2))
: null;
return (host: h.substring(1, close), port: port);
}
}
final colon = h.lastIndexOf(':');
if (colon > 0 && !h.substring(0, colon).contains(':')) {
return (host: h.substring(0, colon), port: int.tryParse(h.substring(colon + 1)));
}
return (host: h, port: null);
}

MongoUri copyWith({
String? scheme,
List<String>? hosts,
String? path,
Map<String, String>? params,
}) =>
MongoUri._(
scheme: scheme ?? this.scheme,
userInfo: userInfo,
hosts: hosts ?? this.hosts,
path: path ?? this.path,
params: params ?? this.params,
);

/// This string pointing at one local tunnel endpoint instead of its seeds.
///
/// The tunnel reaches one server, so the driver must not go looking for the
/// replica set's other members: `directConnection=true` unless the string
/// says otherwise. An SRV string names a DNS record, not a host and port a
/// tunnel could reach, so it cannot be tunnelled.
MongoUri forTunnel(String host, int port) {
if (isSrv) {
throw UnsupportedError(
'An SSH tunnel cannot be used with a mongodb+srv:// connection '
'string: it names a DNS record that resolves to hosts the tunnel '
'cannot know. Use a mongodb:// string with the server\'s host and '
'port.',
);
}
final bracket = host.contains(':') && !host.startsWith('[');
return copyWith(
hosts: ['${bracket ? '[$host]' : host}:$port'],
params: {'directConnection': 'true', ...params},
);
}

@override
String toString() {
final query = params.entries
.map((e) => '${Uri.encodeQueryComponent(e.key)}='
'${Uri.encodeQueryComponent(e.value)}')
.join('&');
return '$scheme://${userInfo == null ? '' : '$userInfo@'}'
'${hosts.join(',')}$path${query.isEmpty ? '' : '?$query'}';
}
}
20 changes: 18 additions & 2 deletions lib/core/security/ssl_certificate_support.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,24 @@ SslCertificatePaths extractSslCertificatePaths(Uri uri) {
SslCertificatePaths extractSslCertificatePathsFromString(String? raw) {
if (raw == null || raw.trim().isEmpty) return const SslCertificatePaths();
final uri = Uri.tryParse(raw.trim());
if (uri == null) return const SslCertificatePaths();
return extractSslCertificatePaths(uri);
if (uri != null) return extractSslCertificatePaths(uri);
// Not a URI Dart accepts (a MongoDB host list, `h1:27017,h2:27017`): the
// certificate paths are still in its query.
final q = raw.indexOf('?');
if (q < 0) return const SslCertificatePaths();
var query = raw.substring(q + 1);
final hash = query.indexOf('#');
if (hash >= 0) query = query.substring(0, hash);
try {
final params = Uri.splitQueryString(query);
return SslCertificatePaths(
rootCert: params[kSslRootCertParam],
clientCert: params[kSslCertParam],
clientKey: params[kSslKeyParam],
);
} catch (_) {
return const SslCertificatePaths();
}
}

Map<String, String> sslCertificateQueryParams(SslCertificatePaths paths) {
Expand Down
14 changes: 13 additions & 1 deletion lib/features/connections/connection_url_parser.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:querya_desktop/core/database/mongodb_uri.dart';
import 'package:querya_desktop/core/storage/local_db.dart';

const _supportedSchemes = {
Expand Down Expand Up @@ -25,7 +26,18 @@ const _validPostgresSslModes = {
return (row: null, error: 'URL/URI is required.');
}

final uri = Uri.tryParse(trimmed);
var uri = Uri.tryParse(trimmed);
if (uri == null) {
// A MongoDB host list (`mongodb://h1:27017,h2:27017/db?replicaSet=rs0`)
// is valid MongoDB but not a URI Dart parses: read the first seed, the
// row keeps the whole string (#1309).
final mongo = MongoUri.tryParse(trimmed);
if (mongo != null) {
uri = Uri.tryParse(
mongo.copyWith(hosts: [mongo.hosts.first]).toString(),
);
}
}
if (uri == null || uri.scheme.isEmpty) {
return (row: null, error: 'Invalid URL/URI format.');
}
Expand Down
Loading
Loading