Improve server stats and tunnel stability
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'}));
|
||||
|
||||
@@ -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 {
|
||||
broadcastTunnelStatus(tunnelName, {
|
||||
connected: true,
|
||||
status: CONNECTION_STATES.CONNECTED
|
||||
});
|
||||
if (isPeriodic) {
|
||||
if (!activeTunnels.has(tunnelName)) {
|
||||
broadcastTunnelStatus(tunnelName, {
|
||||
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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user