Improve server stats and tunnel stability

This commit is contained in:
LukeGus
2025-08-27 22:58:08 -05:00
parent 200428498f
commit 9130eb68a8
6 changed files with 84 additions and 35 deletions

View File

@@ -405,7 +405,6 @@ const migrateSchema = () => {
addColumnIfNotExists('users', 'token_url', 'TEXT'); addColumnIfNotExists('users', 'token_url', 'TEXT');
try { try {
sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run(); sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run();
logger.info('Removed redirect_uri column from users table');
} catch (e) { } catch (e) {
} }

View File

@@ -79,7 +79,12 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str
const key = await importJWK(publicKey); const key = await importJWK(publicKey);
const {payload} = await jwtVerify(idToken, key, { const {payload} = await jwtVerify(idToken, key, {
issuer: [issuerUrl, issuerUrl.replace(/\/application\/o\/[^\/]+$/, '')], issuer: [
issuerUrl,
normalizedIssuerUrl,
issuerUrl.replace(/\/application\/o\/[^\/]+$/, ''),
normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '')
],
audience: clientId, audience: clientId,
}); });

View File

@@ -128,12 +128,10 @@ function buildSshConfig(host: HostRecord): ConnectConfig {
(base as any).passphrase = host.keyPassword; (base as any).passphrase = host.keyPassword;
} }
logger.info(`SSH key authentication configured for host ${host.ip}`);
} catch (keyError) { } catch (keyError) {
logger.error(`SSH key format error for host ${host.ip}: ${keyError.message}`); logger.error(`SSH key format error for host ${host.ip}: ${keyError.message}`);
if (host.password) { if (host.password) {
(base as any).password = host.password; (base as any).password = host.password;
logger.info(`Falling back to password authentication for host ${host.ip}`);
} else { } else {
throw new Error(`Invalid SSH key format for host ${host.ip}`); throw new Error(`Invalid SSH key format for host ${host.ip}`);
} }
@@ -297,15 +295,27 @@ async function collectMetrics(host: HostRecord): Promise<{
let usedHuman: string | null = null; let usedHuman: string | null = null;
let totalHuman: string | null = null; let totalHuman: string | null = null;
try { try {
const diskOut = await execCommand(client, 'df -h -P / | tail -n +2'); // Get both human-readable and bytes format for accurate calculation
const line = diskOut.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || ''; const diskOutHuman = await execCommand(client, 'df -h -P / | tail -n +2');
const parts = line.split(/\s+/); const diskOutBytes = await execCommand(client, 'df -B1 -P / | tail -n +2');
if (parts.length >= 6) {
totalHuman = parts[1] || null; const humanLine = diskOutHuman.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
usedHuman = parts[2] || null; const bytesLine = diskOutBytes.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
const pctStr = (parts[4] || '').replace('%', '');
const pctNum = Number(pctStr); const humanParts = humanLine.split(/\s+/);
diskPercent = Number.isFinite(pctNum) ? pctNum : null; const bytesParts = bytesLine.split(/\s+/);
if (humanParts.length >= 6 && bytesParts.length >= 6) {
totalHuman = humanParts[1] || null;
usedHuman = humanParts[2] || null;
// Calculate our own percentage using bytes for accuracy
const totalBytes = Number(bytesParts[1]);
const usedBytes = Number(bytesParts[2]);
if (Number.isFinite(totalBytes) && Number.isFinite(usedBytes) && totalBytes > 0) {
diskPercent = Math.max(0, Math.min(100, (usedBytes / totalBytes) * 100));
}
} }
} catch (e) { } catch (e) {
diskPercent = null; diskPercent = null;

View File

@@ -310,8 +310,6 @@ wss.on('connection', (ws: WebSocket) => {
if (keyType && keyType !== 'auto') { if (keyType && keyType !== 'auto') {
connectConfig.privateKeyType = keyType; connectConfig.privateKeyType = keyType;
} }
logger.info('SSH key authentication configured successfully');
} catch (keyError) { } catch (keyError) {
logger.error('SSH key format error: ' + keyError.message); logger.error('SSH key format error: ' + keyError.message);
ws.send(JSON.stringify({type: 'error', message: 'SSH key format error: Invalid private key format'})); ws.send(JSON.stringify({type: 'error', message: 'SSH key format error: Invalid private key format'}));

View File

@@ -197,7 +197,8 @@ function classifyError(errorMessage: string): ErrorType {
if (message.includes("connect etimedout") || if (message.includes("connect etimedout") ||
message.includes("timeout") || message.includes("timeout") ||
message.includes("timed out")) { message.includes("timed out") ||
message.includes("keepalive timeout")) {
return ERROR_TYPES.TIMEOUT; return ERROR_TYPES.TIMEOUT;
} }
@@ -267,7 +268,8 @@ function cleanupTunnelResources(tunnelName: string): void {
tunnelName, tunnelName,
`${tunnelName}_confirm`, `${tunnelName}_confirm`,
`${tunnelName}_retry`, `${tunnelName}_retry`,
`${tunnelName}_verify_retry` `${tunnelName}_verify_retry`,
`${tunnelName}_ping`
]; ];
timerKeys.forEach(key => { timerKeys.forEach(key => {
@@ -302,7 +304,7 @@ function resetRetryState(tunnelName: string): void {
countdownIntervals.delete(tunnelName); countdownIntervals.delete(tunnelName);
} }
['', '_confirm', '_retry', '_verify_retry'].forEach(suffix => { ['', '_confirm', '_retry', '_verify_retry', '_ping'].forEach(suffix => {
const timerKey = `${tunnelName}${suffix}`; const timerKey = `${tunnelName}${suffix}`;
if (verificationTimers.has(timerKey)) { if (verificationTimers.has(timerKey)) {
clearTimeout(verificationTimers.get(timerKey)!); clearTimeout(verificationTimers.get(timerKey)!);
@@ -353,7 +355,8 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
const maxRetries = tunnelConfig.maxRetries || 3; const maxRetries = tunnelConfig.maxRetries || 3;
const retryInterval = tunnelConfig.retryInterval || 5000; const retryInterval = tunnelConfig.retryInterval || 5000;
let retryCount = (retryCounters.get(tunnelName) || 0) + 1; let retryCount = retryCounters.get(tunnelName) || 0;
retryCount = retryCount + 1;
if (retryCount > maxRetries) { if (retryCount > maxRetries) {
logger.error(`All ${maxRetries} retries failed for ${tunnelName}`); logger.error(`All ${maxRetries} retries failed for ${tunnelName}`);
@@ -420,7 +423,6 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
if (!manualDisconnects.has(tunnelName)) { if (!manualDisconnects.has(tunnelName)) {
activeTunnels.delete(tunnelName); activeTunnels.delete(tunnelName);
connectSSHTunnel(tunnelConfig, retryCount); connectSSHTunnel(tunnelConfig, retryCount);
} }
}, retryInterval); }, retryInterval);
@@ -438,13 +440,43 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
} }
function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void { function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void {
broadcastTunnelStatus(tunnelName, { if (isPeriodic) {
connected: true, if (!activeTunnels.has(tunnelName)) {
status: CONNECTION_STATES.CONNECTED broadcastTunnelStatus(tunnelName, {
}); connected: false,
status: CONNECTION_STATES.DISCONNECTED,
reason: 'Tunnel connection lost'
});
}
}
} }
function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void { function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void {
const pingKey = `${tunnelName}_ping`;
if (verificationTimers.has(pingKey)) {
clearInterval(verificationTimers.get(pingKey)!);
verificationTimers.delete(pingKey);
}
const pingInterval = setInterval(() => {
const currentStatus = connectionStatus.get(tunnelName);
if (currentStatus?.status === CONNECTION_STATES.CONNECTED) {
if (!activeTunnels.has(tunnelName)) {
broadcastTunnelStatus(tunnelName, {
connected: false,
status: CONNECTION_STATES.DISCONNECTED,
reason: 'Tunnel connection lost'
});
clearInterval(pingInterval);
verificationTimers.delete(pingKey);
}
} else {
clearInterval(pingInterval);
verificationTimers.delete(pingKey);
}
}, 120000);
verificationTimers.set(pingKey, pingInterval);
} }
function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void { function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
@@ -528,6 +560,8 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
errorType === ERROR_TYPES.PERMISSION || errorType === ERROR_TYPES.PERMISSION ||
manualDisconnects.has(tunnelName); manualDisconnects.has(tunnelName);
handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry);
}); });
@@ -590,7 +624,11 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
setTimeout(() => { setTimeout(() => {
if (!manualDisconnects.has(tunnelName) && activeTunnels.has(tunnelName)) { if (!manualDisconnects.has(tunnelName) && activeTunnels.has(tunnelName)) {
verifyTunnelConnection(tunnelName, tunnelConfig, false); broadcastTunnelStatus(tunnelName, {
connected: true,
status: CONNECTION_STATES.CONNECTED
});
setupPingInterval(tunnelName, tunnelConfig);
} }
}, 2000); }, 2000);
@@ -650,7 +688,6 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
stream.stderr.on("data", (data) => { stream.stderr.on("data", (data) => {
const errorMsg = data.toString().trim(); const errorMsg = data.toString().trim();
logger.debug(`Tunnel stderr for '${tunnelName}': ${errorMsg}`);
}); });
}); });
}); });
@@ -659,11 +696,11 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
host: tunnelConfig.sourceIP, host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort, port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername, username: tunnelConfig.sourceUsername,
keepaliveInterval: 60000, keepaliveInterval: 30000,
keepaliveCountMax: 0, keepaliveCountMax: 3,
readyTimeout: 60000, readyTimeout: 60000,
tcpKeepAlive: true, tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000, tcpKeepAliveInitialDelay: 15000,
algorithms: { algorithms: {
kex: [ kex: [
'diffie-hellman-group14-sha256', 'diffie-hellman-group14-sha256',
@@ -750,11 +787,11 @@ function killRemoteTunnelByMarker(tunnelConfig: TunnelConfig, tunnelName: string
host: tunnelConfig.sourceIP, host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort, port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername, username: tunnelConfig.sourceUsername,
keepaliveInterval: 60000, keepaliveInterval: 30000,
keepaliveCountMax: 0, keepaliveCountMax: 3,
readyTimeout: 60000, readyTimeout: 60000,
tcpKeepAlive: true, tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000, tcpKeepAliveInitialDelay: 15000,
algorithms: { algorithms: {
kex: [ kex: [
'diffie-hellman-group14-sha256', 'diffie-hellman-group14-sha256',

View File

@@ -243,7 +243,7 @@ export function Server({
<Separator className="p-0.5 self-stretch" orientation="vertical"/> <Separator className="p-0.5 self-stretch" orientation="vertical"/>
{/* HDD */} {/* Root Storage */}
<div className="flex-1 min-w-0 px-2 py-2"> <div className="flex-1 min-w-0 px-2 py-2">
<h1 className="font-bold xt-lg flex flex-row gap-2 mb-2"> <h1 className="font-bold xt-lg flex flex-row gap-2 mb-2">
<HardDrive/> <HardDrive/>
@@ -254,7 +254,7 @@ export function Server({
const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A'; const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
const usedText = used ?? 'N/A'; const usedText = used ?? 'N/A';
const totalText = total ?? 'N/A'; const totalText = total ?? 'N/A';
return `HDD Space - ${pctText} (${usedText} of ${totalText})`; return `Root Storage Space - ${pctText} (${usedText} of ${totalText})`;
})()} })()}
</h1> </h1>