feat(proxy): 增加数据库和 AI 代理支持

This commit is contained in:
t8y2 committed 2026-05-11 15:05:25 +08:00
1 parent 1d5a2b2220
commit 79277c660f
20 files changed
+712 -19

No files matched your search

+16
View File
@@ -15,6 +15,12 @@ export interface ConnectionConfig {
database?: string;
url_params?: string;
ssh_enabled: boolean;
proxy_enabled?: boolean;
proxy_type?: "socks5" | "http";
proxy_host?: string;
proxy_port?: number;
proxy_username?: string;
proxy_password?: string;
ssl: boolean;
}
@@ -50,6 +56,7 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
const config: ConnectionConfig = JSON.parse(row.config_json);
config.id = row.id;
if (!config.password) config.password = getSecret(db, row.id, "password");
if (!config.proxy_password) config.proxy_password = getSecret(db, row.id, "proxy_password");
configs.push(config);
}
@@ -90,6 +97,12 @@ export async function addConnection(config: Omit<ConnectionConfig, "id">): Promi
ssh_key_path: "",
ssh_key_passphrase: "",
ssh_expose_lan: false,
proxy_enabled: config.proxy_enabled ?? false,
proxy_type: config.proxy_type ?? "socks5",
proxy_host: config.proxy_host ?? "",
proxy_port: config.proxy_port ?? 1080,
proxy_username: config.proxy_username ?? "",
proxy_password: "",
ssl: config.ssl ?? false,
sysdba: false,
connection_string: null,
@@ -101,6 +114,9 @@ export async function addConnection(config: Omit<ConnectionConfig, "id">): Promi
if (config.password) {
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "password", config.password);
}
if (config.proxy_password) {
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "proxy_password", config.proxy_password);
}
});
insert();
db.close();
+123 -5
View File
@@ -1,4 +1,5 @@
import type { ConnectionConfig } from "./connections.js";
import { createServer, connect as netConnect, type Server, type Socket } from "node:net";
export interface TableInfo {
name: string;
@@ -31,6 +32,7 @@ interface PoolEntry {
}
const pools = new Map<string, PoolEntry>();
const proxyTunnels = new Map<string, { server: Server; port: number }>();
function poolKey(config: ConnectionConfig): string {
return `${config.id}:${config.database || ""}`;
@@ -59,8 +61,9 @@ async function getPgPool(config: ConnectionConfig): Promise<import("pg").Pool> {
}
const pg = await import("pg");
const endpoint = await connectionEndpoint(config);
const pool = new pg.default.Pool({
connectionString: buildConnectionUrl(config),
connectionString: buildConnectionUrl(config, endpoint),
max: 3,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 10_000,
@@ -81,8 +84,9 @@ async function getMysqlPool(config: ConnectionConfig): Promise<import("mysql2/pr
}
const mysql = await import("mysql2/promise");
const endpoint = await connectionEndpoint(config);
const pool = mysql.default.createPool({
uri: buildConnectionUrl(config),
uri: buildConnectionUrl(config, endpoint),
connectionLimit: 3,
idleTimeout: 30_000,
connectTimeout: 10_000,
@@ -93,14 +97,128 @@ async function getMysqlPool(config: ConnectionConfig): Promise<import("mysql2/pr
return pool;
}
function buildConnectionUrl(config: ConnectionConfig): string {
async function connectionEndpoint(config: ConnectionConfig): Promise<{ host: string; port: number }> {
if (!config.proxy_enabled || !config.proxy_host) return { host: config.host, port: config.port };
const existing = proxyTunnels.get(config.id);
if (existing) return { host: "127.0.0.1", port: existing.port };
const server = createServer((inbound) => {
connectViaProxy(config)
.then((outbound) => {
inbound.pipe(outbound);
outbound.pipe(inbound);
})
.catch(() => inbound.destroy());
});
const port = await new Promise<number>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (address && typeof address === "object") resolve(address.port);
else reject(new Error("Failed to bind proxy tunnel"));
});
});
proxyTunnels.set(config.id, { server, port });
return { host: "127.0.0.1", port };
}
function buildConnectionUrl(config: ConnectionConfig, endpoint: { host: string; port: number }): string {
const db = config.database || "";
const params = config.url_params || "";
const suffix = params ? `?${params}` : "";
if (isMysqlType(config.db_type)) {
return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
}
return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
}
function connectViaProxy(config: ConnectionConfig): Promise<Socket> {
return new Promise((resolve, reject) => {
const socket = netConnect(config.proxy_port || 1080, config.proxy_host || "127.0.0.1");
socket.once("error", reject);
socket.once("connect", () => {
if ((config.proxy_type || "socks5") === "http") {
httpConnect(socket, config, resolve, reject);
} else {
socks5Connect(socket, config, resolve, reject);
}
});
});
}
function httpConnect(socket: Socket, config: ConnectionConfig, resolve: (socket: Socket) => void, reject: (err: Error) => void) {
const target = `${config.host}:${config.port}`;
const lines = [`CONNECT ${target} HTTP/1.1`, `Host: ${target}`];
if (config.proxy_username || config.proxy_password) {
const token = Buffer.from(`${config.proxy_username || ""}:${config.proxy_password || ""}`).toString("base64");
lines.push(`Proxy-Authorization: Basic ${token}`);
}
socket.write(`${lines.join("\r\n")}\r\n\r\n`);
let buffer = Buffer.alloc(0);
socket.on("data", function onData(chunk: Buffer) {
buffer = Buffer.concat([buffer, chunk]);
const end = buffer.indexOf("\r\n\r\n");
if (end < 0) return;
socket.off("data", onData);
const head = buffer.subarray(0, end).toString("utf8");
if (!/^HTTP\/1\.[01] 200\b/.test(head)) {
reject(new Error(`HTTP proxy CONNECT failed: ${head.split("\r\n")[0] || "invalid response"}`));
socket.destroy();
return;
}
const rest = buffer.subarray(end + 4);
if (rest.length) socket.unshift(rest);
resolve(socket);
});
}
function socks5Connect(socket: Socket, config: ConnectionConfig, resolve: (socket: Socket) => void, reject: (err: Error) => void) {
const wantsAuth = !!(config.proxy_username || config.proxy_password);
socket.write(Buffer.from(wantsAuth ? [0x05, 0x02, 0x00, 0x02] : [0x05, 0x01, 0x00]));
socket.once("data", (method) => {
if (method.length < 2 || method[0] !== 0x05) {
reject(new Error("Invalid SOCKS greeting"));
socket.destroy();
return;
}
if (method[1] === 0x02) {
const user = Buffer.from(config.proxy_username || "");
const pass = Buffer.from(config.proxy_password || "");
socket.write(Buffer.concat([Buffer.from([0x01, user.length]), user, Buffer.from([pass.length]), pass]));
socket.once("data", (auth) => {
if (auth.length < 2 || auth[1] !== 0x00) {
reject(new Error("SOCKS proxy authentication failed"));
socket.destroy();
return;
}
sendSocksConnect(socket, config, resolve, reject);
});
} else if (method[1] === 0x00) {
sendSocksConnect(socket, config, resolve, reject);
} else {
reject(new Error("SOCKS proxy rejected authentication methods"));
socket.destroy();
}
});
}
function sendSocksConnect(socket: Socket, config: ConnectionConfig, resolve: (socket: Socket) => void, reject: (err: Error) => void) {
const host = Buffer.from(config.host);
socket.write(Buffer.concat([Buffer.from([0x05, 0x01, 0x00, 0x03, host.length]), host, portBytes(config.port)]));
socket.once("data", (res) => {
if (res.length < 4 || res[0] !== 0x05 || res[1] !== 0x00) {
reject(new Error(`SOCKS proxy connect failed with code ${res[1] ?? "unknown"}`));
socket.destroy();
return;
}
resolve(socket);
});
}
function portBytes(port: number): Buffer {
const buf = Buffer.alloc(2);
buf.writeUInt16BE(port);
return buf;
}
function isMysqlType(dbType: string): boolean {