105 lines
4.8 KiB
TypeScript
105 lines
4.8 KiB
TypeScript
import type { ConnectionConfig } from "@/types/database";
|
|
|
|
type Translate = (key: string) => string;
|
|
|
|
function normalizeUrlParams(params: string | undefined): URLSearchParams {
|
|
return new URLSearchParams((params || "").trim().replace(/^\?/, ""));
|
|
}
|
|
|
|
function normalizeUrlParamKey(key: string): string {
|
|
return key.trim().toLowerCase().replace(/[-_]/g, "");
|
|
}
|
|
|
|
function urlParamValue(params: URLSearchParams, key: string): string {
|
|
const normalizedKey = normalizeUrlParamKey(key);
|
|
let result = "";
|
|
for (const [paramKey, value] of params.entries()) {
|
|
if (normalizeUrlParamKey(paramKey) === normalizedKey) result = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function jdbcUrlParamValue(params: URLSearchParams, key: string): string {
|
|
const normalizedKey = key.trim().toLowerCase();
|
|
let result = "";
|
|
for (const [paramKey, value] of params.entries()) {
|
|
if (paramKey.trim().toLowerCase() === normalizedKey) result = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function hasJdbcUrlParam(params: URLSearchParams, key: string): boolean {
|
|
const normalizedKey = key.trim().toLowerCase();
|
|
for (const paramKey of params.keys()) {
|
|
if (paramKey.trim().toLowerCase() === normalizedKey) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function mysqlTlsMode(config: ConnectionConfig): string {
|
|
const parsed = normalizeUrlParams(config.url_params);
|
|
// MySQL clients use ssl-mode, sslmode and sslMode spellings; keep hint behavior aligned with backend parsing.
|
|
const mode = urlParamValue(parsed, "ssl-mode").trim().toLowerCase().replace("-", "_");
|
|
if (["disabled", "disable"].includes(mode)) return "disabled";
|
|
if (["preferred", "prefer"].includes(mode)) return "preferred";
|
|
if (["required", "require", "verify_ca", "verify_identity"].includes(mode)) return mode;
|
|
const isTrue = (value: string) => ["true", "1", "yes", "on"].includes(value);
|
|
const isFalse = (value: string) => ["false", "0", "no", "off"].includes(value);
|
|
const nativeRequireSsl = jdbcUrlParamValue(parsed, "require_ssl").trim().toLowerCase();
|
|
if (isFalse(nativeRequireSsl)) return "disabled";
|
|
if (isTrue(nativeRequireSsl)) return "required";
|
|
const jdbcUseSsl = jdbcUrlParamValue(parsed, "useSSL").trim().toLowerCase();
|
|
const jdbcRequireSsl = jdbcUrlParamValue(parsed, "requireSSL").trim().toLowerCase();
|
|
const jdbcVerifyServerCertificate = jdbcUrlParamValue(parsed, "verifyServerCertificate").trim().toLowerCase();
|
|
if (isFalse(jdbcUseSsl)) return "disabled";
|
|
if (isTrue(jdbcVerifyServerCertificate)) return "verify_ca";
|
|
if (isTrue(jdbcRequireSsl)) return "required";
|
|
if (hasJdbcUrlParam(parsed, "useSSL") || hasJdbcUrlParam(parsed, "requireSSL") || hasJdbcUrlParam(parsed, "verifyServerCertificate")) return "preferred";
|
|
if (config.ssl) return "required";
|
|
return "disabled";
|
|
}
|
|
|
|
function isMysqlTlsLikeFailure(message: string): boolean {
|
|
const text = message.toLowerCase();
|
|
return (
|
|
(text.includes("mysql") || text.includes("mariadb") || text.includes("tidb") || text.includes("tls") || text.includes("ssl")) &&
|
|
(text.includes("tls") || text.includes("ssl") || text.includes("handshake") || text.includes("certificate") || text.includes("cert") || text.includes("unknown ca") || text.includes("self signed"))
|
|
);
|
|
}
|
|
|
|
export function isMysqlMissingPasswordFailure(config: ConnectionConfig, message: string): boolean {
|
|
if (config.db_type !== "mysql" || config.password) return false;
|
|
return /access denied for user[\s\S]*using password:\s*no/i.test(message);
|
|
}
|
|
|
|
export function isJdbcMissingRuntimeDependencyError(message: string): boolean {
|
|
return /Missing Java class|NoClassDefFoundError|ClassNotFoundException/i.test(message);
|
|
}
|
|
|
|
function appendHint(message: string, hint: string): string {
|
|
return message.includes(hint) ? message : `${message}\n\n${hint}`;
|
|
}
|
|
|
|
export function appendConnectionErrorHints(config: ConnectionConfig | undefined, message: string, t: Translate): string {
|
|
if (!config) return message;
|
|
let result = message;
|
|
if (config.db_type === "jdbc" && isJdbcMissingRuntimeDependencyError(message)) {
|
|
result = appendHint(result, t("connection.jdbcMissingRuntimeDependencyHint"));
|
|
}
|
|
if (config.db_type !== "mysql") return result;
|
|
// MySQL includes the client's source IP in this error, which is easy to
|
|
// mistake for a host rewritten by sync. When no password was sent, lead
|
|
// with the actionable fix and reserve the native grant error for attempts
|
|
// that actually supplied credentials.
|
|
if (isMysqlMissingPasswordFailure(config, message)) {
|
|
return t("connection.mysqlMissingPasswordHint");
|
|
}
|
|
if (mysqlTlsMode(config) === "disabled") return message;
|
|
if (!isMysqlTlsLikeFailure(message)) return message;
|
|
if (/UnsupportedCertVersion/i.test(message)) {
|
|
return appendHint(message, t("connection.mysqlUnsupportedCertVersionHint"));
|
|
}
|
|
const hint = t("connection.mysqlTlsConnectionFailureHint");
|
|
return appendHint(message, hint);
|
|
}
|