mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 16:24:33 +00:00
189 lines
7.5 KiB
TypeScript
189 lines
7.5 KiB
TypeScript
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: "" },
|
|
dameng: { dbType: "dameng", profile: "dm", label: "DM (Dameng)", port: 5236, user: "SYSDBA" },
|
|
dm: { dbType: "dameng", profile: "dm", label: "DM (Dameng)", port: 5236, user: "SYSDBA" },
|
|
};
|
|
|
|
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;
|
|
}
|