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');
try {
sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run();
logger.info('Removed redirect_uri column from users table');
} catch (e) {
}

View File

@@ -79,7 +79,12 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str
const key = await importJWK(publicKey);
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,
});

View File

@@ -128,12 +128,10 @@ function buildSshConfig(host: HostRecord): ConnectConfig {
(base as any).passphrase = host.keyPassword;
}
logger.info(`SSH key authentication configured for host ${host.ip}`);
} catch (keyError) {
logger.error(`SSH key format error for host ${host.ip}: ${keyError.message}`);
if (host.password) {
(base as any).password = host.password;
logger.info(`Falling back to password authentication for host ${host.ip}`);
} else {
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 totalHuman: string | null = null;
try {
const diskOut = await execCommand(client, 'df -h -P / | tail -n +2');
const line = diskOut.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
const parts = line.split(/\s+/);
if (parts.length >= 6) {
totalHuman = parts[1] || null;
usedHuman = parts[2] || null;
const pctStr = (parts[4] || '').replace('%', '');
const pctNum = Number(pctStr);
diskPercent = Number.isFinite(pctNum) ? pctNum : null;
// Get both human-readable and bytes format for accurate calculation
const diskOutHuman = await execCommand(client, 'df -h -P / | tail -n +2');
const diskOutBytes = await execCommand(client, 'df -B1 -P / | tail -n +2');
const humanLine = diskOutHuman.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
const bytesLine = diskOutBytes.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
const humanParts = humanLine.split(/\s+/);
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) {
diskPercent = null;

View File

@@ -310,8 +310,6 @@ wss.on('connection', (ws: WebSocket) => {
if (keyType && keyType !== 'auto') {
connectConfig.privateKeyType = keyType;
}
logger.info('SSH key authentication configured successfully');
} catch (keyError) {
logger.error('SSH key format error: ' + keyError.message);
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") ||
message.includes("timeout") ||
message.includes("timed out")) {
message.includes("timed out") ||
message.includes("keepalive timeout")) {
return ERROR_TYPES.TIMEOUT;
}
@@ -267,7 +268,8 @@ function cleanupTunnelResources(tunnelName: string): void {
tunnelName,
`${tunnelName}_confirm`,
`${tunnelName}_retry`,
`${tunnelName}_verify_retry`
`${tunnelName}_verify_retry`,
`${tunnelName}_ping`
];
timerKeys.forEach(key => {
@@ -302,7 +304,7 @@ function resetRetryState(tunnelName: string): void {
countdownIntervals.delete(tunnelName);
}
['', '_confirm', '_retry', '_verify_retry'].forEach(suffix => {
['', '_confirm', '_retry', '_verify_retry', '_ping'].forEach(suffix => {
const timerKey = `${tunnelName}${suffix}`;
if (verificationTimers.has(timerKey)) {
clearTimeout(verificationTimers.get(timerKey)!);
@@ -353,7 +355,8 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
const maxRetries = tunnelConfig.maxRetries || 3;
const retryInterval = tunnelConfig.retryInterval || 5000;
let retryCount = (retryCounters.get(tunnelName) || 0) + 1;
let retryCount = retryCounters.get(tunnelName) || 0;
retryCount = retryCount + 1;
if (retryCount > maxRetries) {
logger.error(`All ${maxRetries} retries failed for ${tunnelName}`);
@@ -420,7 +423,6 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
if (!manualDisconnects.has(tunnelName)) {
activeTunnels.delete(tunnelName);
connectSSHTunnel(tunnelConfig, retryCount);
}
}, retryInterval);
@@ -438,13 +440,43 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
}
function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void {
if (isPeriodic) {
if (!activeTunnels.has(tunnelName)) {
broadcastTunnelStatus(tunnelName, {
connected: true,
status: CONNECTION_STATES.CONNECTED
connected: false,
status: CONNECTION_STATES.DISCONNECTED,
reason: 'Tunnel connection lost'
});
}
}
}
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 {
@@ -528,6 +560,8 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
errorType === ERROR_TYPES.PERMISSION ||
manualDisconnects.has(tunnelName);
handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry);
});
@@ -590,7 +624,11 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
setTimeout(() => {
if (!manualDisconnects.has(tunnelName) && activeTunnels.has(tunnelName)) {
verifyTunnelConnection(tunnelName, tunnelConfig, false);
broadcastTunnelStatus(tunnelName, {
connected: true,
status: CONNECTION_STATES.CONNECTED
});
setupPingInterval(tunnelName, tunnelConfig);
}
}, 2000);
@@ -650,7 +688,6 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
stream.stderr.on("data", (data) => {
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,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
keepaliveInterval: 60000,
keepaliveCountMax: 0,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
tcpKeepAliveInitialDelay: 15000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
@@ -750,11 +787,11 @@ function killRemoteTunnelByMarker(tunnelConfig: TunnelConfig, tunnelName: string
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
keepaliveInterval: 60000,
keepaliveCountMax: 0,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
tcpKeepAliveInitialDelay: 15000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',

View File

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