diff --git a/electron/src/apps/local-app-launcher.ts b/electron/src/apps/local-app-launcher.ts index 161d8f764..2d1fb1cdb 100644 --- a/electron/src/apps/local-app-launcher.ts +++ b/electron/src/apps/local-app-launcher.ts @@ -46,6 +46,15 @@ function dbxUrl(payload) { return `dbx://connection/new?${query.toString().replaceAll("*", "%2A")}`; } +function mongodbUri(payload) { + const user = encodeURIComponent(username(payload)); + const password = encodeURIComponent(String(payload.token?.value || "")); + const database = encodeURIComponent(String(payload.asset?.info?.db_name || "")); + const host = String(payload.endpoint?.host || ""); + const port = String(payload.endpoint?.port || ""); + return `mongodb://${user}:${password}@${host}:${port}/${database}?authSource=admin&loadBalanced=true&retryWrites=false`; +} + function valuesFor(payload): Record { const dmConnection = `${username(payload)}/${String(payload.token?.value || "")}@${String(payload.endpoint?.host || "")}:${String(payload.endpoint?.port || "")}`; return { @@ -57,6 +66,7 @@ function valuesFor(payload): Record { port: String(payload.endpoint?.port || ""), dbname: payload.protocol === "oracle" ? username(payload) : String(payload.asset?.info?.db_name || ""), dameng_disql: shellQuote(dmConnection), + mongodb_shell_uri: payload.protocol === "mongodb" ? shellQuote(mongodbUri(payload)) : "", dbeaver_protocol: payload.protocol === "sqlserver" ? "mssql_jdbc_ms_new" : payload.protocol, dbx_url: dbxUrl(payload), url: navicatUrl(payload) diff --git a/electron/tests/application-config.test.ts b/electron/tests/application-config.test.ts index 34511d28d..5fe56c4f5 100644 --- a/electron/tests/application-config.test.ts +++ b/electron/tests/application-config.test.ts @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import { once } from "node:events"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { createServer } from "node:net"; @@ -492,6 +493,58 @@ test("launches RDP connection files without endpoint fields", async () => { assert.deepEqual(launchedPayload, payload); }); +for (const platform of ["macos", "linux"]) { + test(`${platform}/terminal-db quotes and encodes MongoDB connection values`, async (context) => { + if (process.platform === "win32") return context.skip("POSIX shell test"); + const config = JSON.parse( + await readFile(path.join(projectRoot, "plugins", platform, `${platform}.terminal-db`, "connect.json"), "utf8") + ); + const application = { + name: "terminal-db", + protocol: ["mongodb"], + is_set: true, + match_first: ["mongodb"], + launch_type: config.launch.type, + launch_driver: config.launch.driver, + arg_format: config.launch.template, + protocol_templates: config.launch.protocol_templates + }; + const launcher = new LocalApplicationLauncher( + { isPackaged: false }, + projectRoot, + { getConfig: async () => ({ databases: [application] }) }, + null + ); + const user = "token:id@/"; + const password = ["space value", "$HOME", "`printf injected`", "'", '"', ";", "&", "/", "\\", "?", "#"].join(""); + const database = "data /?#"; + const expectedUri = `mongodb://${encodeURIComponent(user)}:${encodeURIComponent(password)}@gateway.example.com:5525/${encodeURIComponent(database)}?authSource=admin&loadBalanced=true&retryWrites=false`; + let launched = false; + launcher.launchTerminal = async (_selected, command) => { + const result = spawnSync("bash", ["-lc", `mongosh() { printf '%s\\n' "$@"; }\n${command}`], { + encoding: "utf8" + }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, `${expectedUri}\n`); + const parsed = new URL(expectedUri); + assert.equal(decodeURIComponent(parsed.username), user); + assert.equal(decodeURIComponent(parsed.password), password); + assert.equal(decodeURIComponent(parsed.pathname.slice(1)), database); + assert.equal(parsed.searchParams.get("authSource"), "admin"); + launched = true; + }; + const payload = { + protocol: "mongodb", + name: "MongoDB", + endpoint: { host: "gateway.example.com", port: 5525 }, + token: { id: user, value: password }, + asset: { info: { db_name: database } } + }; + await launcher.launch(`jms2://${Buffer.from(JSON.stringify(payload)).toString("base64")}`); + assert.equal(launched, true); + }); +} + for (const [platform, plugin, expected] of [ ["macos", "dbeaver", "driver=mysql"], ["linux", "dbeaver", "driver=mysql"], diff --git a/plugins/linux/linux.terminal-db/connect.json b/plugins/linux/linux.terminal-db/connect.json index 8ef725566..9a77c68f0 100644 --- a/plugins/linux/linux.terminal-db/connect.json +++ b/plugins/linux/linux.terminal-db/connect.json @@ -20,7 +20,7 @@ "mysql": "mysql -u {username} -p{value} -h {host} -P {port} {dbname}", "mariadb": "mysql -u {username} -p{value} -h {host} -P {port} {dbname}", "sqlserver": "sqlcmd -S {host},{port} -U {username} -P {value} -d {dbname}", - "mongodb": "mongosh mongodb://{username}:{value}@{host}:{port}/{dbname}" + "mongodb": "mongosh {mongodb_shell_uri}" } }, "display_name": "Terminal" diff --git a/plugins/macos/macos.terminal-db/connect.json b/plugins/macos/macos.terminal-db/connect.json index ea04f5780..018cefc8a 100644 --- a/plugins/macos/macos.terminal-db/connect.json +++ b/plugins/macos/macos.terminal-db/connect.json @@ -21,7 +21,7 @@ "mysql": "mysql -u {username} -p{value} -h {host} -P {port} {dbname}", "mariadb": "mysql -u {username} -p{value} -h {host} -P {port} {dbname}", "sqlserver": "sqlcmd -S {host},{port} -U {username} -P {value} -d {dbname}", - "mongodb": "mongosh mongodb://{username}:{value}@{host}:{port}/{dbname}" + "mongodb": "mongosh {mongodb_shell_uri}" } }, "display_name": "Terminal"