mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
- URL-encode proxy credentials to handle special characters in passwords
- Decode URL-encoded credentials during legacy proxy migration
- Fix HTTPS proxy default port (443 instead of 8080) in frontend and migration
- Add dispatcher cache invalidation when proxy config changes
- Cast proxy port to number for SQLite INTEGER column in proxy logger
- Fix redundant .replace("//", "") in migration protocol parsing
- Copy postinstall script in Dockerfile before npm install
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
240 lines
7.2 KiB
TypeScript
240 lines
7.2 KiB
TypeScript
/**
|
|
* Proxy Logger — Hybrid in-memory + SQLite persistence
|
|
*
|
|
* Keeps a fast in-memory ring buffer for real-time dashboard AND
|
|
* persists to SQLite so logs survive server restarts.
|
|
*
|
|
* Pattern follows callLogs.js (T-15 decomposition).
|
|
*/
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { getDbInstance, isCloud, isBuildPhase } from "./db/core";
|
|
|
|
const shouldPersistToDisk = !isCloud && !isBuildPhase;
|
|
|
|
const MAX_ENTRIES = parseInt(process.env.PROXY_LOG_MAX_ENTRIES || "200", 10);
|
|
|
|
interface ProxyInfo {
|
|
type: string;
|
|
host: string;
|
|
port: number | string;
|
|
}
|
|
|
|
interface ProxyLogEntry {
|
|
id: string;
|
|
timestamp: string;
|
|
status: string;
|
|
proxy: ProxyInfo | null;
|
|
level: string;
|
|
levelId: string | null;
|
|
provider: string | null;
|
|
targetUrl: string | null;
|
|
publicIp: string | null;
|
|
latencyMs: number;
|
|
error: string | null;
|
|
connectionId: string | null;
|
|
comboId: string | null;
|
|
account: string | null;
|
|
tlsFingerprint: boolean;
|
|
}
|
|
|
|
interface ProxyLogFilters {
|
|
status?: string;
|
|
type?: string;
|
|
provider?: string;
|
|
level?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
const proxyLogs: ProxyLogEntry[] = [];
|
|
|
|
// ──────────────── Startup: hydrate from DB ────────────────
|
|
|
|
function loadFromDb() {
|
|
if (!shouldPersistToDisk) return;
|
|
try {
|
|
const db = getDbInstance();
|
|
const rows = db
|
|
.prepare("SELECT * FROM proxy_logs ORDER BY timestamp DESC LIMIT ?")
|
|
.all(MAX_ENTRIES) as any[];
|
|
|
|
for (const row of rows) {
|
|
proxyLogs.push({
|
|
id: row.id,
|
|
timestamp: row.timestamp,
|
|
status: row.status || "success",
|
|
proxy: row.proxy_host
|
|
? { type: row.proxy_type, host: row.proxy_host, port: row.proxy_port }
|
|
: null,
|
|
level: row.level || "direct",
|
|
levelId: row.level_id || null,
|
|
provider: row.provider || null,
|
|
targetUrl: row.target_url || null,
|
|
publicIp: row.public_ip || null,
|
|
latencyMs: row.latency_ms || 0,
|
|
error: row.error || null,
|
|
connectionId: row.connection_id || null,
|
|
comboId: row.combo_id || null,
|
|
account: row.account || null,
|
|
tlsFingerprint: row.tls_fingerprint === 1,
|
|
});
|
|
}
|
|
|
|
if (proxyLogs.length > 0) {
|
|
console.log(`[proxyLogger] Loaded ${proxyLogs.length} proxy logs from SQLite`);
|
|
}
|
|
} catch (err: any) {
|
|
console.warn("[proxyLogger] Failed to load from DB:", err.message);
|
|
}
|
|
}
|
|
|
|
loadFromDb();
|
|
|
|
// ──────────────── Log a proxy event ────────────────
|
|
|
|
export function logProxyEvent(entry: Partial<ProxyLogEntry>) {
|
|
const log: ProxyLogEntry = {
|
|
id: uuidv4(),
|
|
timestamp: new Date().toISOString(),
|
|
status: entry.status || "success",
|
|
proxy: entry.proxy || null,
|
|
level: entry.level || "direct",
|
|
levelId: entry.levelId || null,
|
|
provider: entry.provider || null,
|
|
targetUrl: entry.targetUrl || null,
|
|
publicIp: entry.publicIp || null,
|
|
latencyMs: entry.latencyMs || 0,
|
|
error: entry.error || null,
|
|
connectionId: entry.connectionId || null,
|
|
comboId: entry.comboId || null,
|
|
account: entry.account || null,
|
|
tlsFingerprint: entry.tlsFingerprint || false,
|
|
};
|
|
|
|
// 1. In-memory ring buffer (newest first)
|
|
proxyLogs.unshift(log);
|
|
if (proxyLogs.length > MAX_ENTRIES) {
|
|
proxyLogs.length = MAX_ENTRIES;
|
|
}
|
|
|
|
// 2. Persist to SQLite
|
|
if (shouldPersistToDisk) {
|
|
try {
|
|
const db = getDbInstance();
|
|
db.prepare(
|
|
`INSERT INTO proxy_logs (id, timestamp, status, proxy_type, proxy_host, proxy_port,
|
|
level, level_id, provider, target_url, public_ip, latency_ms, error,
|
|
connection_id, combo_id, account, tls_fingerprint)
|
|
VALUES (@id, @timestamp, @status, @proxyType, @proxyHost, @proxyPort,
|
|
@level, @levelId, @provider, @targetUrl, @publicIp, @latencyMs, @error,
|
|
@connectionId, @comboId, @account, @tlsFingerprint)`
|
|
).run({
|
|
id: log.id,
|
|
timestamp: log.timestamp,
|
|
status: log.status,
|
|
proxyType: log.proxy?.type || null,
|
|
proxyHost: log.proxy?.host || null,
|
|
proxyPort: log.proxy?.port ? Number(log.proxy.port) : null,
|
|
level: log.level,
|
|
levelId: log.levelId,
|
|
provider: log.provider,
|
|
targetUrl: log.targetUrl,
|
|
publicIp: log.publicIp,
|
|
latencyMs: log.latencyMs,
|
|
error: log.error,
|
|
connectionId: log.connectionId,
|
|
comboId: log.comboId,
|
|
account: log.account,
|
|
tlsFingerprint: log.tlsFingerprint ? 1 : 0,
|
|
});
|
|
|
|
// Trim old entries
|
|
const count = (db.prepare("SELECT COUNT(*) as cnt FROM proxy_logs").get() as any)?.cnt || 0;
|
|
if (count > MAX_ENTRIES) {
|
|
db.prepare(
|
|
`DELETE FROM proxy_logs WHERE id IN (
|
|
SELECT id FROM proxy_logs ORDER BY timestamp ASC LIMIT ?
|
|
)`
|
|
).run(count - MAX_ENTRIES);
|
|
}
|
|
} catch (err: any) {
|
|
console.warn("[proxyLogger] Failed to persist:", err.message);
|
|
}
|
|
}
|
|
|
|
return log;
|
|
}
|
|
|
|
// ──────────────── Query ────────────────
|
|
|
|
/**
|
|
* Get proxy logs with optional filters.
|
|
* Reads from in-memory for speed (already hydrated from DB on startup).
|
|
*/
|
|
export function getProxyLogs(filters: ProxyLogFilters = {}) {
|
|
let logs = [...proxyLogs];
|
|
|
|
if (filters.status) {
|
|
if (filters.status === "ok") {
|
|
logs = logs.filter((l) => l.status === "success");
|
|
} else {
|
|
logs = logs.filter((l) => l.status === filters.status);
|
|
}
|
|
}
|
|
|
|
if (filters.type) {
|
|
logs = logs.filter((l) => l.proxy?.type === filters.type);
|
|
}
|
|
|
|
if (filters.provider) {
|
|
logs = logs.filter((l) => l.provider === filters.provider);
|
|
}
|
|
|
|
if (filters.level) {
|
|
logs = logs.filter((l) => l.level === filters.level);
|
|
}
|
|
|
|
if (filters.search) {
|
|
const q = filters.search.toLowerCase();
|
|
logs = logs.filter(
|
|
(l) =>
|
|
(l.proxy?.host || "").toLowerCase().includes(q) ||
|
|
(l.provider || "").toLowerCase().includes(q) ||
|
|
(l.targetUrl || "").toLowerCase().includes(q) ||
|
|
(l.publicIp || "").toLowerCase().includes(q) ||
|
|
(l.level || "").toLowerCase().includes(q) ||
|
|
(l.error || "").toLowerCase().includes(q) ||
|
|
(l.account || "").toLowerCase().includes(q)
|
|
);
|
|
}
|
|
|
|
const limit = filters.limit || 300;
|
|
return logs.slice(0, limit);
|
|
}
|
|
|
|
// ──────────────── Clear ────────────────
|
|
|
|
export function clearProxyLogs() {
|
|
proxyLogs.length = 0;
|
|
|
|
if (shouldPersistToDisk) {
|
|
try {
|
|
const db = getDbInstance();
|
|
db.prepare("DELETE FROM proxy_logs").run();
|
|
} catch (err: any) {
|
|
console.warn("[proxyLogger] Failed to clear DB:", err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ──────────────── Stats ────────────────
|
|
|
|
export function getProxyLogStats() {
|
|
const total = proxyLogs.length;
|
|
const success = proxyLogs.filter((l) => l.status === "success").length;
|
|
const error = proxyLogs.filter((l) => l.status === "error").length;
|
|
const timeout = proxyLogs.filter((l) => l.status === "timeout").length;
|
|
const direct = proxyLogs.filter((l) => l.level === "direct").length;
|
|
return { total, success, error, timeout, direct };
|
|
}
|