diff --git a/lib/features/connections/connection_url_parser.dart b/lib/features/connections/connection_url_parser.dart index 1634cbd7..82ca3723 100644 --- a/lib/features/connections/connection_url_parser.dart +++ b/lib/features/connections/connection_url_parser.dart @@ -11,6 +11,13 @@ const _supportedSchemes = { 'rediss', }; +const _validPostgresSslModes = { + 'disable', + 'require', + 'verify-ca', + 'verify-full', +}; + /// Parses a database connection URL into a [ConnectionRow], or returns an error message. ({ConnectionRow? row, String? error}) parseConnectionUrlInput(String input) { final trimmed = input.trim(); @@ -32,14 +39,67 @@ const _supportedSchemes = { ); } - final row = _buildConnectionRow(trimmed, uri, scheme); + final sslResult = _resolveSslForScheme(scheme, uri); + if (sslResult.error != null) { + return (row: null, error: sslResult.error); + } + + final row = _buildConnectionRow(trimmed, uri, scheme, sslResult.useSSL); if (row == null) { return (row: null, error: 'Failed to parse connection URL.'); } return (row: row, error: null); } -ConnectionRow? _buildConnectionRow(String url, Uri uri, String scheme) { +({bool? useSSL, String? error}) _resolveSslForScheme(String scheme, Uri uri) { + final type = _schemeToType(scheme); + if (type == null) return (useSSL: null, error: null); + + var useSSL = scheme == 'rediss'; + + if (type == 'postgresql') { + final sslMode = uri.queryParameters['sslmode']?.toLowerCase() ?? + uri.queryParameters['ssl']?.toLowerCase(); + if (sslMode != null && sslMode.isNotEmpty) { + if (!_validPostgresSslModes.contains(sslMode)) { + return ( + useSSL: null, + error: + 'Unsupported sslmode "$sslMode" for PostgreSQL. ' + 'Supported: disable, require, verify-ca, verify-full.', + ); + } + useSSL = sslMode != 'disable'; + } + } else if (type != 'sqlite') { + final sslQuery = uri.queryParameters['sslmode'] ?? + uri.queryParameters['ssl']; + if (sslQuery != null) { + final lowerSsl = sslQuery.toLowerCase(); + if (lowerSsl == 'true' || lowerSsl == 'require') { + useSSL = true; + } + } + } + + return (useSSL: useSSL, error: null); +} + +String? _schemeToType(String scheme) { + if (scheme == 'postgresql' || scheme == 'postgres') return 'postgresql'; + if (scheme == 'mysql') return 'mysql'; + if (scheme == 'sqlite') return 'sqlite'; + if (scheme == 'mongodb' || scheme == 'mongodb+srv') return 'mongodb'; + if (scheme == 'redis' || scheme == 'rediss') return 'redis'; + return null; +} + +ConnectionRow? _buildConnectionRow( + String url, + Uri uri, + String scheme, + bool? resolvedUseSSL, +) { String type; int? defaultPort; @@ -68,7 +128,7 @@ ConnectionRow? _buildConnectionRow(String url, Uri uri, String scheme) { String? databaseName; String? authSource; String? connectionString; - var useSSL = scheme == 'rediss'; + var useSSL = resolvedUseSSL ?? (scheme == 'rediss'); if (type == 'sqlite') { String path; @@ -86,7 +146,7 @@ ConnectionRow? _buildConnectionRow(String url, Uri uri, String scheme) { host = path; } else { host = uri.host.isEmpty ? null : uri.host; - port = uri.hasPort ? uri.port : defaultPort; + port = uri.hasPort ? uri.port : null; if (uri.userInfo.isNotEmpty) { final parts = uri.userInfo.split(':'); @@ -106,26 +166,18 @@ ConnectionRow? _buildConnectionRow(String url, Uri uri, String scheme) { authSource = uri.queryParameters['authSource'] ?? uri.queryParameters['authsource']; - final sslQuery = uri.queryParameters['sslmode'] ?? uri.queryParameters['ssl']; - if (sslQuery != null) { - final lowerSsl = sslQuery.toLowerCase(); - if (lowerSsl == 'true' || lowerSsl == 'require' || lowerSsl == 'prefer') { - useSSL = true; - } - } - if (type == 'postgresql' || type == 'mysql' || type == 'mongodb') { connectionString = url; } } - final name = _connectionName(type, host, databaseName); + final name = _connectionName(type, host, port, databaseName, defaultPort); return ConnectionRow( type: type, name: name, host: host, - port: port, + port: port ?? defaultPort, username: username, password: password, databaseName: databaseName, @@ -136,12 +188,19 @@ ConnectionRow? _buildConnectionRow(String url, Uri uri, String scheme) { ); } -String _connectionName(String type, String? host, String? databaseName) { +String _connectionName( + String type, + String? host, + int? port, + String? databaseName, + int? defaultPort, +) { if (type == 'sqlite') { return host == ':memory:' ? 'SQLite (Memory)' : 'SQLite (${host!.split('/').last})'; } final cleanHost = host ?? 'localhost'; + final cleanPort = port ?? defaultPort; final cleanDb = databaseName ?? ''; final typeName = switch (type) { 'postgresql' => 'PostgreSQL', @@ -152,5 +211,8 @@ String _connectionName(String type, String? host, String? databaseName) { if (cleanDb.isNotEmpty) { return '$typeName: $cleanDb'; } + if (cleanPort != null) { + return '$typeName: $cleanHost:$cleanPort'; + } return '$typeName: $cleanHost'; } diff --git a/test/features/connections/connection_url_parser_test.dart b/test/features/connections/connection_url_parser_test.dart index 1e0d384c..49c9905f 100644 --- a/test/features/connections/connection_url_parser_test.dart +++ b/test/features/connections/connection_url_parser_test.dart @@ -54,6 +54,47 @@ void main() { expect(result.row!.useSSL, true); }); + test('parses postgresql sslmode=verify-full as SSL enabled', () { + final result = parseConnectionUrlInput( + 'postgresql://localhost/postgres?sslmode=verify-full', + ); + expect(result.error, isNull); + expect(result.row!.useSSL, true); + }); + + test('parses postgresql sslmode=verify-ca as SSL enabled', () { + final result = parseConnectionUrlInput( + 'postgresql://localhost/postgres?sslmode=verify-ca', + ); + expect(result.error, isNull); + expect(result.row!.useSSL, true); + }); + + test('parses postgresql sslmode=disable as SSL disabled', () { + final result = parseConnectionUrlInput( + 'postgresql://localhost/postgres?sslmode=disable', + ); + expect(result.error, isNull); + expect(result.row!.useSSL, false); + }); + + test('returns error for postgresql sslmode=prefer', () { + final result = parseConnectionUrlInput( + 'postgresql://localhost/postgres?sslmode=prefer', + ); + expect(result.row, isNull); + expect(result.error, contains('Unsupported sslmode')); + expect(result.error, contains('prefer')); + }); + + test('returns error for invalid postgresql sslmode', () { + final result = parseConnectionUrlInput( + 'postgresql://localhost/postgres?sslmode=invalid', + ); + expect(result.row, isNull); + expect(result.error, contains('Unsupported sslmode')); + }); + test('parses mysql URL', () { final result = parseConnectionUrlInput( 'mysql://root:p%40ss@127.0.0.1:3307/sakila', @@ -112,11 +153,18 @@ void main() { expect(result.error, isNull); final row = result.row!; expect(row.type, 'redis'); - expect(row.name, 'Redis: localhost'); + expect(row.name, 'Redis: localhost:6379'); expect(row.password, 'password'); expect(row.connectionString, isNull); }); + test('uses default driver port when URI omits port', () { + final result = parseConnectionUrlInput('postgresql://localhost/mydb'); + expect(result.error, isNull); + expect(result.row!.port, 5432); + expect(result.row!.name, 'PostgreSQL: mydb'); + }); + test('parses rediss URL with SSL enabled', () { final result = parseConnectionUrlInput('rediss://localhost'); expect(result.error, isNull);