fix(connections): persist SSL settings and honor a URL's sslmode (#36)
Connecting to a managed provider (Neon in the report) failed with "connection is insecure (try using `sslmode=require`)" because the TLS mode never reached the backend: - ConnectionFormData carried no ssl_* fields and both submit handlers (NewConnectionScreen.buildPayload, EditConnectionModal save + test) omitted them, so the SSH/SSL tab's mode was silently dropped before IPC. - SslFields wrote ssl_ca_cert / ssl_client_cert / ssl_client_key — names the Rust ConnectionInput ignores (it expects ssl_ca_path / ssl_cert_path / ssl_key_path). - parseConnectionString ignored query params, so a pasted Neon/Supabase URL's ?sslmode=require was discarded. Pasting a URI now fills every setting it can (sslmode / ssl-mode, including MySQL's REQUIRED and VERIFY_IDENTITY spellings) and offers a derived label (database name, else host/file stem) as a placeholder that Test/Save uses when the field is left blank. Managed presets pre-select their TLS mode (Supabase / Neon → Require, PlanetScale → Verify Full). Backend safety net: db::tls::resolve_ssl_mode defaults *.neon.tech / *.supabase.co / *.psdb.cloud to `require` when a connection has no ssl_mode, so connections saved before this fix keep working; an explicit mode (including disable) always wins. Tests: +15 frontend (parser, form payload, SSL field names, edit modal), +4 Rust (resolve_ssl_mode / tls_decision_for).
This commit is contained in:
@@ -7,6 +7,53 @@ export interface ParsedConnectionString {
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
database: string | null;
|
||||
/** SSL mode lifted from the URL's `sslmode` / `ssl-mode` query param, or
|
||||
* `null` when absent/unsupported. Keeps managed-PostgreSQL URLs (Neon,
|
||||
* Supabase, …) working — they ship with `?sslmode=require` and the server
|
||||
* rejects a plaintext connection with "connection is insecure". */
|
||||
ssl_mode: SslMode | null;
|
||||
}
|
||||
|
||||
/** SSL modes the backend understands (mirrors `DbConfig.ssl_mode`). */
|
||||
export type SslMode = "disable" | "require" | "verify-ca" | "verify-full";
|
||||
|
||||
/** Alternate spellings used by other clients, mapped onto our four modes:
|
||||
* MySQL's `ssl-mode` vocabulary (`REQUIRED`, `VERIFY_IDENTITY`, …) and
|
||||
* plural forms. `allow` / `prefer` are intentionally absent — they permit a
|
||||
* plaintext fallback, which our mode set cannot express, so they return
|
||||
* `null` and the caller keeps its current/default mode. */
|
||||
const SSL_MODE_ALIASES: Record<string, SslMode> = {
|
||||
disable: "disable",
|
||||
disabled: "disable",
|
||||
require: "require",
|
||||
required: "require",
|
||||
"verify-ca": "verify-ca",
|
||||
"verify-full": "verify-full",
|
||||
// MySQL's strongest mode: chain + hostname verification.
|
||||
"verify-identity": "verify-full",
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the TLS mode from a connection URL's query string.
|
||||
*
|
||||
* Accepts libpq's `sslmode` (PostgreSQL) and MySQL's `ssl-mode` spelling,
|
||||
* case-insensitively and hyphen/underscore-insensitively (`verify-full`,
|
||||
* `verify_full`, `VERIFY-FULL`, …). Unsupported values (`allow`, `prefer`)
|
||||
* return `null` so the caller keeps its current/default mode rather than
|
||||
* silently downgrading to plaintext.
|
||||
*/
|
||||
function parseSslMode(url: URL): SslMode | null {
|
||||
let raw: string | null = null;
|
||||
for (const [key, value] of url.searchParams) {
|
||||
const k = key.toLowerCase();
|
||||
if (k === "sslmode" || k === "ssl-mode") {
|
||||
raw = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!raw) return null;
|
||||
const normalized = raw.trim().toLowerCase().replace(/_/g, "-");
|
||||
return SSL_MODE_ALIASES[normalized] ?? null;
|
||||
}
|
||||
|
||||
const DB_PROTOCOLS: Record<string, DbType> = {
|
||||
@@ -68,6 +115,9 @@ export function parseConnectionString(input: string): ParsedConnectionString | n
|
||||
username,
|
||||
password,
|
||||
database,
|
||||
ssl_mode: db_type === "postgresql" || db_type === "mysql" || db_type === "mariadb"
|
||||
? parseSslMode(url)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,6 +145,25 @@ export function detectProviderFromHost(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest a connection label for a freshly pasted connection string, so the
|
||||
* connection can be saved without typing a name first.
|
||||
*
|
||||
* Preference order: the database name, then (for SQLite) the file stem, then
|
||||
* the host. Capped at 100 chars to satisfy `validateConnectionInput`, which
|
||||
* rejects longer names.
|
||||
*/
|
||||
export function suggestConnectionLabel(parsed: ParsedConnectionString): string {
|
||||
let label: string;
|
||||
if (parsed.db_type === "sqlite") {
|
||||
const file = parsed.host.split(/[\\/]/).pop() ?? "";
|
||||
label = file.replace(/\.(sqlite3?|db)$/i, "") || file;
|
||||
} else {
|
||||
label = parsed.database?.replace(/^\/+/, "") || parsed.host;
|
||||
}
|
||||
return label.trim().slice(0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a connection URL string from a saved connection, for copying into
|
||||
* env vars / other tools. `password` is the decrypted keychain value (or
|
||||
|
||||
Reference in New Issue
Block a user