feat(sqlite): support RC4 Legacy databases
This commit is contained in:
19 files changed
+187
-49
No files matched your search
@@ -1661,7 +1661,7 @@ async function openDbFilePath(path: string) {
|
||||
password: "",
|
||||
};
|
||||
await connectionStore.addConnection(config);
|
||||
void connectionStore.connect(config);
|
||||
await connectionStore.connect(config);
|
||||
toast(t("welcome.fileOpened", { name }));
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
|
||||
|
||||
@@ -109,10 +109,10 @@ export function useFileDrop() {
|
||||
};
|
||||
try {
|
||||
await connectionStore.addConnection(config);
|
||||
void connectionStore.connect(config);
|
||||
await connectionStore.connect(config);
|
||||
toast(t("welcome.fileOpened", { name }));
|
||||
} catch (e: any) {
|
||||
toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("welcome.fileOpenFailed", { name, message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -436,7 +436,7 @@ export default {
|
||||
createDuckDbFile: "Create DuckDB file",
|
||||
createSqliteFile: "Create SQLite database",
|
||||
memoryDatabasePathHint: "Use :memory: to create an in-memory SQLite or DuckDB database.",
|
||||
sqliteCipherKey: "SQLCipher Key",
|
||||
sqliteCipherKey: "SQLite Encryption Key",
|
||||
sqliteCipherKeyPlaceholder: "Leave empty for unencrypted SQLite",
|
||||
sqliteExtensions: "SQLite Extensions",
|
||||
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -438,7 +438,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "Crear archivo DuckDB",
|
||||
createSqliteFile: "Crear base de datos SQLite",
|
||||
memoryDatabasePathHint: "Usa :memory: para crear una base de datos SQLite o DuckDB en memoria.",
|
||||
sqliteCipherKey: "Clave SQLCipher",
|
||||
sqliteCipherKey: "Clave de cifrado de SQLite",
|
||||
sqliteCipherKeyPlaceholder: "Déjalo vacío para SQLite sin cifrar",
|
||||
sqliteExtensions: "Extensiones de SQLite",
|
||||
sqliteExtensionsPlaceholder: "/ruta/a/regexp.dylib\n/ruta/a/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -437,7 +437,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "Crea file DuckDB",
|
||||
createSqliteFile: "Crea database SQLite",
|
||||
memoryDatabasePathHint: "Usa :memory: per creare un database SQLite o DuckDB in memoria.",
|
||||
sqliteCipherKey: "Chiave SQLCipher",
|
||||
sqliteCipherKey: "Chiave di crittografia SQLite",
|
||||
sqliteCipherKeyPlaceholder: "Lascia vuoto per SQLite non cifrato",
|
||||
sqliteExtensions: "Estensioni SQLite",
|
||||
sqliteExtensionsPlaceholder: "/percorso/per/regexp.dylib\n/percorso/per/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -437,7 +437,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "DuckDBファイルを作成",
|
||||
createSqliteFile: "SQLiteデータベースを作成",
|
||||
memoryDatabasePathHint: "インメモリのSQLiteまたはDuckDBデータベースを作成するには :memory: を使用してください。",
|
||||
sqliteCipherKey: "SQLCipherキー",
|
||||
sqliteCipherKey: "SQLite暗号化キー",
|
||||
sqliteCipherKeyPlaceholder: "暗号化されていないSQLiteでは空のままにします",
|
||||
sqliteExtensions: "SQLite拡張",
|
||||
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -430,7 +430,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "DuckDB 파일 만들기",
|
||||
createSqliteFile: "SQLite 데이터베이스 만들기",
|
||||
memoryDatabasePathHint: "인메모리 SQLite 또는 DuckDB 데이터베이스를 만들려면 :memory:를 사용하세요.",
|
||||
sqliteCipherKey: "SQLCipher 키",
|
||||
sqliteCipherKey: "SQLite 암호화 키",
|
||||
sqliteCipherKeyPlaceholder: "암호화하지 않은 SQLite는 비워 두세요",
|
||||
sqliteExtensions: "SQLite 확장",
|
||||
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -438,7 +438,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "Criar arquivo DuckDB",
|
||||
createSqliteFile: "Criar banco de dados SQLite",
|
||||
memoryDatabasePathHint: "Use :memory: para criar um banco de dados SQLite ou DuckDB na memória.",
|
||||
sqliteCipherKey: "Chave SQLCipher",
|
||||
sqliteCipherKey: "Chave de criptografia do SQLite",
|
||||
sqliteCipherKeyPlaceholder: "Deixe vazio para SQLite não criptografado",
|
||||
sqliteExtensions: "Extensões SQLite",
|
||||
sqliteExtensionsPlaceholder: "/caminho/para/regexp.dylib\n/caminho/para/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -361,7 +361,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "新建 DuckDB 文件",
|
||||
createSqliteFile: "新建 SQLite 数据库",
|
||||
memoryDatabasePathHint: "输入 :memory: 可创建 SQLite 或 DuckDB 内存数据库。",
|
||||
sqliteCipherKey: "SQLCipher 密码(密钥)",
|
||||
sqliteCipherKey: "SQLite 加密密码",
|
||||
sqliteCipherKeyPlaceholder: "未加密 SQLite 留空",
|
||||
sqliteExtensions: "SQLite 扩展库",
|
||||
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -438,7 +438,7 @@ export default withEnglishFallback({
|
||||
createDuckDbFile: "建立 DuckDB 檔案",
|
||||
createSqliteFile: "建立 SQLite 資料庫",
|
||||
memoryDatabasePathHint: "輸入 :memory: 可建立 SQLite 或 DuckDB 記憶體資料庫。",
|
||||
sqliteCipherKey: "SQLCipher 密碼(密鑰)",
|
||||
sqliteCipherKey: "SQLite 加密密碼",
|
||||
sqliteCipherKeyPlaceholder: "未加密 SQLite 留空",
|
||||
sqliteExtensions: "SQLite 擴充庫",
|
||||
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { appendConnectionErrorHints, isSqliteMissingEncryptionPasswordFailure } from "@/lib/connection/connectionErrorHints";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
function mysqlConfig(urlParams: string | undefined): ConnectionConfig {
|
||||
@@ -40,6 +40,14 @@ const t = (key: string) => {
|
||||
};
|
||||
|
||||
describe("appendConnectionErrorHints", () => {
|
||||
it("recognizes an encrypted SQLite file that was first opened without a password", () => {
|
||||
const config = { ...mysqlConfig(undefined), db_type: "sqlite" as const, host: "/tmp/encrypted.db", port: 0 };
|
||||
|
||||
expect(isSqliteMissingEncryptionPasswordFailure(config, "Selected file is not a valid SQLite database file.")).toBe(true);
|
||||
expect(isSqliteMissingEncryptionPasswordFailure({ ...config, password: "wrong" }, "Selected file is not a valid SQLite database file.")).toBe(false);
|
||||
expect(isSqliteMissingEncryptionPasswordFailure(config, "SQLite connection failed: unable to open database file")).toBe(false);
|
||||
});
|
||||
|
||||
it("adds a MySQL TLS hint for non-disabled TLS failures", () => {
|
||||
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=preferred"), "MySQL connection failed: TLS handshake failed", t);
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ export function isMysqlMissingPasswordFailure(config: ConnectionConfig, message:
|
||||
return /access denied for user[\s\S]*using password:\s*no/i.test(message);
|
||||
}
|
||||
|
||||
export function isSqliteMissingEncryptionPasswordFailure(config: ConnectionConfig, message: string): boolean {
|
||||
if (config.db_type !== "sqlite" || config.password) return false;
|
||||
return /Selected file is not a valid SQLite database file/i.test(message);
|
||||
}
|
||||
|
||||
export function isJdbcMissingRuntimeDependencyError(message: string): boolean {
|
||||
return /Missing Java class|NoClassDefFoundError|ClassNotFoundException/i.test(message);
|
||||
}
|
||||
|
||||
@@ -297,6 +297,35 @@ describe("connectionStore save_password opt-out", () => {
|
||||
expect(store.getConfig("mysql-1")?.password).toBe("typed-pw");
|
||||
});
|
||||
|
||||
it("prompts and retries when an encrypted SQLite file is opened without a password", async () => {
|
||||
const connectDb = vi.fn().mockRejectedValueOnce(new Error("Selected file is not a valid SQLite database file.")).mockResolvedValueOnce("sqlite-1");
|
||||
installApiMocks({ connectDb });
|
||||
installPasswordPromptMock();
|
||||
requestPassword.mockResolvedValue({ password: "123456", rememberPassword: true });
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection({
|
||||
id: "sqlite-1",
|
||||
name: "Encrypted SQLite",
|
||||
db_type: "sqlite",
|
||||
host: "/tmp/encrypted.db",
|
||||
port: 0,
|
||||
username: "",
|
||||
database: undefined,
|
||||
save_password: true,
|
||||
password: "",
|
||||
});
|
||||
store.connections = [connection];
|
||||
|
||||
await store.connect(connection);
|
||||
|
||||
expect(requestPassword).toHaveBeenCalledWith({ connectionId: "sqlite-1", connectionName: "Encrypted SQLite" });
|
||||
expect(connectDb).toHaveBeenCalledTimes(2);
|
||||
expect(connectDb).toHaveBeenNthCalledWith(1, expect.objectContaining({ password: "" }), expect.any(Number));
|
||||
expect(connectDb).toHaveBeenNthCalledWith(2, expect.objectContaining({ password: "123456" }), expect.any(Number));
|
||||
expect(store.getConfig("sqlite-1")?.password).toBe("123456");
|
||||
});
|
||||
|
||||
it("does not persist a recovered password after the connection config changes", async () => {
|
||||
const connected = deferred<string>();
|
||||
const connectDb = vi
|
||||
|
||||
@@ -141,7 +141,7 @@ import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
|
||||
import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVersion, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
|
||||
import { appendConnectionErrorHints, isMysqlMissingPasswordFailure } from "@/lib/connection/connectionErrorHints";
|
||||
import { appendConnectionErrorHints, isMysqlMissingPasswordFailure, isSqliteMissingEncryptionPasswordFailure } from "@/lib/connection/connectionErrorHints";
|
||||
import { connectionNeedsPasswordPrompt } from "@/lib/connection/connectionPassword";
|
||||
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import { buildXuguTypeMemberNodes, isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
||||
@@ -3630,12 +3630,13 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
rebuildTreeNodes();
|
||||
}
|
||||
|
||||
async function connectDbWithMissingPasswordRetry(config: ConnectionConfig, localAttempt: number): Promise<{ config: ConnectionConfig; id: string; rememberPassword: boolean }> {
|
||||
async function connectDbWithPasswordRetry(config: ConnectionConfig, localAttempt: number): Promise<{ config: ConnectionConfig; id: string; rememberPassword: boolean }> {
|
||||
try {
|
||||
const id = await withConnectionAttemptTimeout(api.connectDb(config, localAttempt), config);
|
||||
return { config, id, rememberPassword: false };
|
||||
} catch (error) {
|
||||
if (!isMysqlMissingPasswordFailure(config, connectionErrorMessage(error))) throw error;
|
||||
const message = connectionErrorMessage(error);
|
||||
if (!isMysqlMissingPasswordFailure(config, message) && !isSqliteMissingEncryptionPasswordFailure(config, message)) throw error;
|
||||
const prompted = await ensureConnectionPassword(config, true);
|
||||
config = prompted.config;
|
||||
ensureLocalConnectionAttemptActive(config.id, localAttempt);
|
||||
@@ -3661,7 +3662,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureSqlServerLegacyCompatibilityComponentInstalled(config);
|
||||
}
|
||||
ensureLocalConnectionAttemptActive(config.id, localAttempt);
|
||||
const connection = await connectDbWithMissingPasswordRetry(config, localAttempt);
|
||||
const connection = await connectDbWithPasswordRetry(config, localAttempt);
|
||||
config = connection.config;
|
||||
rememberPassword ||= connection.rememberPassword;
|
||||
const id = connection.id;
|
||||
@@ -3904,7 +3905,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureSqlServerLegacyCompatibilityComponentInstalled(config);
|
||||
}
|
||||
ensureLocalConnectionAttemptActive(connectionId, localAttempt);
|
||||
const connection = await connectDbWithMissingPasswordRetry(config, localAttempt);
|
||||
const connection = await connectDbWithPasswordRetry(config, localAttempt);
|
||||
config = connection.config;
|
||||
rememberPassword ||= connection.rememberPassword;
|
||||
const id = connection.id;
|
||||
|
||||
Reference in new issue
Block a user