mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 08:18:52 +00:00
fix(connection): keep equals readable in copied connection urls
This commit is contained in:
@@ -17,7 +17,10 @@ function config(overrides: Partial<ConnectionUrlCopyConfig>): ConnectionUrlCopyC
|
||||
}
|
||||
|
||||
const ASCII_SYMBOLS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
|
||||
const ENCODED_ASCII_SYMBOLS = "%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~";
|
||||
// `=` stays literal (issue #12304): userinfo and query values split on their
|
||||
// first separator, so escaping it only made copied passwords differ from the
|
||||
// stored credential for clients that never percent-decode.
|
||||
const ENCODED_ASCII_SYMBOLS = "%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~";
|
||||
|
||||
describe("connectionUrlCopyFormats", () => {
|
||||
it("lists all formats for a PostgreSQL connection with a stored password", () => {
|
||||
@@ -104,6 +107,20 @@ describe("buildConnectionUrlCopy standard URL", () => {
|
||||
expect(buildConnectionUrlCopy(config({ password: "W^j.U" }), "urlWithPassword")).toBe("postgresql://app_user:W%5Ej.U@db.example.com:5432/appdb");
|
||||
});
|
||||
|
||||
it("keeps `=` readable in copied URLs instead of escaping it as %3D", () => {
|
||||
const password = "p=ss=word";
|
||||
const mysql = config({ db_type: "mysql", port: 3306, password });
|
||||
const mysqlUrl = buildConnectionUrlCopy(mysql, "urlWithPassword");
|
||||
expect(mysqlUrl).toBe("mysql://app_user:p=ss=word@db.example.com:3306/appdb");
|
||||
expect(parseConnectionUrl(mysqlUrl ?? "").password).toBe(password);
|
||||
|
||||
const redis = config({ db_type: "redis", port: 6379, username: "", password, database: "0" });
|
||||
expect(buildConnectionUrlCopy(redis, "urlWithPassword")).toBe("redis://:p=ss=word@db.example.com:6379/0");
|
||||
|
||||
const oceanbase = config({ db_type: "mysql", port: 2883, driver_profile: "oceanbase", password });
|
||||
expect(buildConnectionUrlCopy(oceanbase, "urlWithPassword")).toBe("oceanbase://app_user:p=ss=word@db.example.com:2883/appdb");
|
||||
});
|
||||
|
||||
it("preserves credentials that already look percent-encoded", () => {
|
||||
const password = "%25%40%5E";
|
||||
const text = buildConnectionUrlCopy(config({ password }), "urlWithPassword");
|
||||
@@ -193,6 +210,19 @@ describe("buildConnectionUrlCopy JDBC URL", () => {
|
||||
expect(parseConnectionUrl(text ?? "").password).toBe(password);
|
||||
});
|
||||
|
||||
it("keeps `=` readable in MySQL-family JDBC credentials", () => {
|
||||
const password = "p=ss=word";
|
||||
const mysql = config({ db_type: "mysql", port: 3306, password });
|
||||
const mysqlUrl = buildConnectionUrlCopy(mysql, "jdbcUrlWithCredentials");
|
||||
expect(mysqlUrl).toBe("jdbc:mysql://db.example.com:3306/appdb?user=app_user&password=p=ss=word");
|
||||
expect(parseConnectionUrl(mysqlUrl ?? "").password).toBe(password);
|
||||
|
||||
const oceanbase = config({ db_type: "mysql", port: 2883, driver_profile: "oceanbase", password });
|
||||
const oceanbaseUrl = buildConnectionUrlCopy(oceanbase, "jdbcUrlWithCredentials");
|
||||
expect(oceanbaseUrl).toBe("jdbc:oceanbase://db.example.com:2883/appdb?user=app_user&password=p=ss=word");
|
||||
expect(parseConnectionUrl(oceanbaseUrl ?? "").password).toBe(password);
|
||||
});
|
||||
|
||||
it("keeps Oracle Easy Connect credentials literal instead of URI-encoding them", () => {
|
||||
const oracle = config({ db_type: "oracle", host: "ora.example.com", port: 1521, database: "ORCLPDB1", password: "p^ss%40#y" });
|
||||
expect(buildConnectionUrlCopy(oracle, "jdbcUrlWithCredentials")).toBe("jdbc:oracle:thin:@//ora.example.com:1521/ORCLPDB1?user=app_user&password=p^ss%40#y");
|
||||
|
||||
@@ -124,6 +124,9 @@ const POSTGRES_WIRE_DB_TYPES = new Set<DatabaseType>(["postgres", "redshift", "g
|
||||
/** PG-wire families whose JDBC driver accepts the `ssl`/`sslmode` properties. */
|
||||
const PG_JDBC_PREFIXES = new Set(["jdbc:postgresql", "jdbc:redshift", "jdbc:gaussdb", "jdbc:opengauss", "jdbc:kingbase8", "jdbc:highgo"]);
|
||||
|
||||
/** MySQL-family prefixes that share Connector/J's `?key=value&...` query grammar. */
|
||||
const MYSQL_QUERY_JDBC_PREFIXES = new Set(["jdbc:mysql", "jdbc:mariadb", "jdbc:oceanbase"]);
|
||||
|
||||
function normalizedDriverProfile(config: ConnectionUrlCopyConfig): string {
|
||||
return config.driver_profile?.trim().toLowerCase() ?? "";
|
||||
}
|
||||
@@ -188,9 +191,16 @@ function shouldAppendPort(config: ConnectionUrlCopyConfig): boolean {
|
||||
* RFC 3986 component encoding for credentials. `encodeURIComponent` leaves
|
||||
* `!'()*` unescaped, so encode those too; only unreserved characters remain
|
||||
* literal and every encoded credential can be decoded back byte-for-byte.
|
||||
*
|
||||
* `=` is also kept literal: userinfo and query values split on their first
|
||||
* separator, so an embedded `=` cannot change how the string is read, while
|
||||
* escaping it made copied passwords differ from the stored one for clients
|
||||
* that never percent-decode (#12304).
|
||||
*/
|
||||
function encodeCredential(value: string): string {
|
||||
return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
||||
return encodeURIComponent(value)
|
||||
.replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
|
||||
.replace(/%3D/g, "=");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,13 +442,13 @@ function buildSpecialJdbcUrl(config: ConnectionUrlCopyConfig, database: string,
|
||||
function jdbcQuery(config: ConnectionUrlCopyConfig, prefix: string, withCredentials: boolean): string {
|
||||
const raw = (config.url_params ?? "").trim().replace(/^[?&]+/, "");
|
||||
// Strict encoding is enabled only for verified query-style drivers.
|
||||
const encode = prefix === "jdbc:mysql" || prefix === "jdbc:postgresql" ? encodeCredential : encodeURIComponent;
|
||||
const encode = MYSQL_QUERY_JDBC_PREFIXES.has(prefix) || prefix === "jdbc:postgresql" ? encodeCredential : encodeURIComponent;
|
||||
const parts = [...jdbcCredentialParams(config, withCredentials, encode)];
|
||||
if (raw) parts.push(raw);
|
||||
if (config.ssl) {
|
||||
if (PG_JDBC_PREFIXES.has(prefix) && !queryHasParam(raw, ["ssl", "sslmode"])) {
|
||||
parts.push("ssl=true");
|
||||
} else if ((prefix === "jdbc:mysql" || prefix === "jdbc:mariadb" || prefix === "jdbc:oceanbase") && !queryHasParam(raw, ["usessl", "sslmode", "requiressl"])) {
|
||||
} else if (MYSQL_QUERY_JDBC_PREFIXES.has(prefix) && !queryHasParam(raw, ["usessl", "sslmode", "requiressl"])) {
|
||||
parts.push("sslMode=REQUIRED");
|
||||
} else if (prefix === "jdbc:clickhouse" && !queryHasParam(raw, ["ssl", "sslmode", "secure"])) {
|
||||
parts.push("ssl=true");
|
||||
|
||||
Reference in New Issue
Block a user