fix(db): dedupe bulk-imported proxies by full credential tuple (#7594) (#7644)

This commit is contained in:
Ronaldo Davi
2026-07-18 15:14:53 -03:00
committed by GitHub
parent 16e481ba3e
commit 589dbde2e6
2 changed files with 119 additions and 5 deletions

View File

@@ -296,9 +296,14 @@ export async function createProxy(payload: ProxyPayload) {
}
/**
* Upsert a proxy by host+port.
* If a proxy with the same host and port already exists, update it.
* Otherwise, create a new one. Used by the bulk import feature.
* Upsert a proxy by its credential tuple (host+port+username+password).
* If a proxy with the same host, port, username AND password already exists,
* update it. Otherwise, create a new one. Used by the bulk import feature.
*
* #7594: host+port alone is NOT a stable identity. Rotating residential/gateway
* proxies route every credential through one shared host:port, so keying only on
* host+port collapsed distinct-credential imports onto the first existing row
* (the same entry got "updated" N times instead of N entries being created).
*/
export async function upsertProxy(payload: ProxyPayload): Promise<{
proxy: ProxyRegistryRecord | null;
@@ -307,10 +312,14 @@ export async function upsertProxy(payload: ProxyPayload): Promise<{
const db = getDbInstance();
const host = (payload.host || "").trim();
const port = Number(payload.port);
const username = (payload.username || "").trim();
const password = (payload.password || "").trim();
const existing = db
.prepare("SELECT id FROM proxy_registry WHERE host = ? AND port = ? LIMIT 1")
.get(host, port) as { id?: string } | undefined;
.prepare(
"SELECT id FROM proxy_registry WHERE host = ? AND port = ? AND username = ? AND password = ? LIMIT 1"
)
.get(host, port, username, password) as { id?: string } | undefined;
if (existing?.id) {
const updated = await updateProxy(existing.id, payload);

View File

@@ -0,0 +1,105 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Regression test for #7594: bulk-importing proxies that share the same
// host:port but differ in username/password must create DISTINCT entries, not
// repeatedly update the first existing row. Rotating residential/gateway proxies
// commonly route every credential through one host:port, so host+port alone is
// not a stable identity — the credential tuple is.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-dedup-7594-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const proxiesDb = await import("../../src/lib/db/proxies.ts");
async function resetStorage() {
core.resetDbInstance();
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
break;
} catch (error) {
const code = (error as NodeJS.ErrnoException)?.code;
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
} else {
throw error;
}
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("upsertProxy creates distinct entries for same host:port with different credentials (#7594)", async () => {
const base = { name: "Gateway", type: "http" as const, host: "gw.proxy.local", port: 8080 };
const first = await proxiesDb.upsertProxy({ ...base, username: "user-1", password: "pass-1" });
const second = await proxiesDb.upsertProxy({ ...base, username: "user-2", password: "pass-2" });
const third = await proxiesDb.upsertProxy({ ...base, username: "user-3", password: "pass-3" });
assert.equal(first.action, "created");
assert.equal(second.action, "created");
assert.equal(third.action, "created");
assert.notEqual(first.proxy?.id, second.proxy?.id);
assert.notEqual(second.proxy?.id, third.proxy?.id);
const listed = await proxiesDb.listProxies({ includeSecrets: true });
assert.equal(listed.length, 3);
assert.deepEqual(
listed.map((p) => p.username).sort(),
["user-1", "user-2", "user-3"]
);
});
test("upsertProxy still updates when the full credential tuple matches (#7594)", async () => {
const payload = {
name: "Gateway",
type: "http" as const,
host: "gw.proxy.local",
port: 8080,
username: "user-1",
password: "pass-1",
};
const created = await proxiesDb.upsertProxy(payload);
const again = await proxiesDb.upsertProxy({ ...payload, name: "Gateway Renamed" });
assert.equal(created.action, "created");
assert.equal(again.action, "updated");
assert.equal(again.proxy?.id, created.proxy?.id);
const listed = await proxiesDb.listProxies({ includeSecrets: true });
assert.equal(listed.length, 1);
assert.equal(listed[0].name, "Gateway Renamed");
});
test("upsertProxy treats auth-less proxies on same host:port as the same entry (#7594)", async () => {
const base = { name: "Open Gateway", type: "http" as const, host: "open.proxy.local", port: 3128 };
const first = await proxiesDb.upsertProxy(base);
const second = await proxiesDb.upsertProxy({ ...base, name: "Open Gateway 2" });
assert.equal(first.action, "created");
assert.equal(second.action, "updated");
assert.equal(first.proxy?.id, second.proxy?.id);
const listed = await proxiesDb.listProxies({ includeSecrets: true });
assert.equal(listed.length, 1);
});