mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-01 00:25:38 +00:00
feat(connections): import Navicat NCX files
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import type { ConnectionConfig, DatabaseType } from "@/types/database";
|
||||
import { uuid } from "@/lib/utils";
|
||||
|
||||
type PartialConnection = Omit<ConnectionConfig, "id">;
|
||||
|
||||
type ParsedNode = {
|
||||
tag: string;
|
||||
values: Record<string, string>;
|
||||
};
|
||||
|
||||
const typeMap: Record<string, { dbType: DatabaseType; profile: string; label: string; port: number; user: string }> = {
|
||||
mysql: { dbType: "mysql", profile: "mysql", label: "MySQL", port: 3306, user: "root" },
|
||||
mariadb: { dbType: "mysql", profile: "mariadb", label: "MariaDB", port: 3306, user: "root" },
|
||||
postgresql: { dbType: "postgres", profile: "postgres", label: "PostgreSQL", port: 5432, user: "postgres" },
|
||||
postgres: { dbType: "postgres", profile: "postgres", label: "PostgreSQL", port: 5432, user: "postgres" },
|
||||
sqlite: { dbType: "sqlite", profile: "sqlite", label: "SQLite", port: 0, user: "" },
|
||||
sqlserver: { dbType: "sqlserver", profile: "sqlserver", label: "SQL Server", port: 1433, user: "sa" },
|
||||
mssql: { dbType: "sqlserver", profile: "sqlserver", label: "SQL Server", port: 1433, user: "sa" },
|
||||
oracle: { dbType: "oracle", profile: "oracle", label: "Oracle", port: 1521, user: "system" },
|
||||
redis: { dbType: "redis", profile: "redis", label: "Redis", port: 6379, user: "" },
|
||||
mongodb: { dbType: "mongodb", profile: "mongodb", label: "MongoDB", port: 27017, user: "" },
|
||||
mongo: { dbType: "mongodb", profile: "mongodb", label: "MongoDB", port: 27017, user: "" },
|
||||
};
|
||||
|
||||
const unsupportedTypes = new Set(["http", "https", "ftp", "sftp", "ssh"]);
|
||||
|
||||
function normalizeKey(value: string) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
||||
function getAny(values: Record<string, string>, keys: string[]) {
|
||||
for (const key of keys) {
|
||||
const value = values[normalizeKey(key)];
|
||||
if (value?.trim()) return value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
const clean = hex.trim();
|
||||
if (!clean || clean.length % 2 !== 0 || /[^0-9a-f]/i.test(clean)) return null;
|
||||
const bytes = new Uint8Array(clean.length / 2);
|
||||
for (let i = 0; i < clean.length; i += 2) {
|
||||
bytes[i / 2] = Number.parseInt(clean.slice(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function stripPkcs7(bytes: Uint8Array) {
|
||||
const pad = bytes[bytes.length - 1];
|
||||
if (!pad || pad > 16 || pad > bytes.length) return bytes;
|
||||
for (let i = bytes.length - pad; i < bytes.length; i++) {
|
||||
if (bytes[i] !== pad) return bytes;
|
||||
}
|
||||
return bytes.slice(0, bytes.length - pad);
|
||||
}
|
||||
|
||||
async function decryptNavicatPassword(value: string) {
|
||||
const encrypted = hexToBytes(value);
|
||||
if (!encrypted?.length) return "";
|
||||
|
||||
const key = new TextEncoder().encode("libcckeylibcckey");
|
||||
const iv = new TextEncoder().encode("libcciv libcciv ");
|
||||
try {
|
||||
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-CBC" }, false, ["decrypt"]);
|
||||
const decrypted = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-CBC", iv }, cryptoKey, encrypted));
|
||||
return new TextDecoder().decode(stripPkcs7(decrypted));
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function inferProfile(rawType: string, tag: string) {
|
||||
const key = normalizeKey(rawType || tag);
|
||||
for (const [needle, profile] of Object.entries(typeMap)) {
|
||||
if (key.includes(needle)) return profile;
|
||||
}
|
||||
if (unsupportedTypes.has(key)) return null;
|
||||
return typeMap.mysql;
|
||||
}
|
||||
|
||||
function readNode(element: Element): ParsedNode {
|
||||
const values: Record<string, string> = {};
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
values[normalizeKey(attr.name)] = attr.value;
|
||||
}
|
||||
|
||||
for (const child of Array.from(element.children)) {
|
||||
const key = getAny(valuesFromElement(child), ["name", "key", "property", "field"]);
|
||||
const value = getAny(valuesFromElement(child), ["value", "val", "text", "data"]) || child.textContent?.trim() || "";
|
||||
if (key && value) values[normalizeKey(key)] = value;
|
||||
|
||||
const tag = normalizeKey(child.tagName);
|
||||
const text = child.children.length === 0 ? child.textContent?.trim() || "" : "";
|
||||
if (text && !values[tag]) values[tag] = text;
|
||||
for (const attr of Array.from(child.attributes)) {
|
||||
values[`${tag}${normalizeKey(attr.name)}`] = attr.value;
|
||||
}
|
||||
}
|
||||
|
||||
return { tag: element.tagName, values };
|
||||
}
|
||||
|
||||
function valuesFromElement(element: Element) {
|
||||
const values: Record<string, string> = {};
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
values[normalizeKey(attr.name)] = attr.value;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function isConnectionCandidate(node: ParsedNode) {
|
||||
const type = getAny(node.values, ["type", "connType", "connectionType", "databaseType", "driver"]);
|
||||
const name = getAny(node.values, ["name", "connectionName", "connName", "caption", "title"]);
|
||||
const host = getAny(node.values, ["host", "server", "hostname", "serverHost", "address"]);
|
||||
const file = getAny(node.values, ["databaseFile", "filename", "path", "databasePath"]);
|
||||
return !!(name || host || file) && !!(type || host || file);
|
||||
}
|
||||
|
||||
async function parseConnection(node: ParsedNode): Promise<ConnectionConfig | null> {
|
||||
const rawType = getAny(node.values, ["type", "connType", "connectionType", "databaseType", "driver", "dbType"]);
|
||||
const profile = inferProfile(rawType, node.tag);
|
||||
if (!profile) return null;
|
||||
|
||||
const name =
|
||||
getAny(node.values, ["name", "connectionName", "connName", "caption", "title"]) ||
|
||||
getAny(node.values, ["host", "server", "hostname"]) ||
|
||||
profile.label;
|
||||
const host =
|
||||
getAny(node.values, ["host", "server", "hostname", "serverHost", "address"]) ||
|
||||
getAny(node.values, ["databaseFile", "filename", "path", "databasePath"]) ||
|
||||
(profile.dbType === "sqlite" ? "" : "127.0.0.1");
|
||||
const portValue = Number(getAny(node.values, ["port", "serverPort"]));
|
||||
const database = getAny(node.values, ["database", "databaseName", "initialDatabase", "serviceName", "sid", "schema"]);
|
||||
const username = getAny(node.values, ["user", "username", "userName", "uid"]) || profile.user;
|
||||
const password = await decryptNavicatPassword(getAny(node.values, ["password"]));
|
||||
|
||||
const config: PartialConnection = {
|
||||
name,
|
||||
db_type: profile.dbType,
|
||||
driver_profile: profile.profile,
|
||||
driver_label: profile.label,
|
||||
url_params: "",
|
||||
host,
|
||||
port: Number.isFinite(portValue) && portValue > 0 ? portValue : profile.port,
|
||||
username,
|
||||
password,
|
||||
database: database || undefined,
|
||||
color: "",
|
||||
ssh_enabled: false,
|
||||
ssh_host: "",
|
||||
ssh_port: 22,
|
||||
ssh_user: "",
|
||||
ssh_password: "",
|
||||
ssh_key_path: "",
|
||||
ssh_key_passphrase: "",
|
||||
ssh_expose_lan: false,
|
||||
ssh_connect_timeout_secs: 5,
|
||||
ssl: false,
|
||||
connection_string: undefined,
|
||||
jdbc_driver_class: undefined,
|
||||
jdbc_driver_paths: [],
|
||||
};
|
||||
|
||||
return { ...config, id: uuid() };
|
||||
}
|
||||
|
||||
export async function parseNavicatConnections(content: string): Promise<ConnectionConfig[]> {
|
||||
const doc = new DOMParser().parseFromString(content, "application/xml");
|
||||
const parserError = doc.querySelector("parsererror");
|
||||
if (parserError) throw new Error("Invalid Navicat connection file");
|
||||
|
||||
const seen = new Set<string>();
|
||||
const configs: ConnectionConfig[] = [];
|
||||
for (const element of Array.from(doc.querySelectorAll("*"))) {
|
||||
const node = readNode(element);
|
||||
if (!isConnectionCandidate(node)) continue;
|
||||
const config = await parseConnection(node);
|
||||
if (!config) continue;
|
||||
const key = [config.name, config.db_type, config.host, config.port, config.database || ""].join("\u0000");
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
configs.push(config);
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
Reference in New Issue
Block a user