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
92 changes: 77 additions & 15 deletions lib/features/connections/connection_url_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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(':');
Expand All @@ -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,
Expand All @@ -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',
Expand All @@ -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';
}
50 changes: 49 additions & 1 deletion test/features/connections/connection_url_parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand Down
Loading