Skip to content

Commit 0555e4c

Browse files
Fix SSH ED25519 key loading and status check loops (#140)
- Fix SSH ED25519 key loading error by ensuring temporary key files end with newline - Fix cleanup logic using rmdirSync instead of unlinkSync for directories - Add timeout protection to prevent hanging SSH connections - Fix endless status checking loops by removing problematic dependencies - Add debouncing and proper cleanup for status checks - Support file upload without extensions for Windows-generated keys - Improve SSH key type detection for OpenSSH format keys Resolves libcrypto errors and prevents resource leaks in SSH operations.
1 parent 08e0c82 commit 0555e4c

File tree

5 files changed

+94
-38
lines changed

5 files changed

+94
-38
lines changed

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function InstalledScriptsTab() {
5959
} | null>(null);
6060
const [controllingScriptId, setControllingScriptId] = useState<number | null>(null);
6161
const scriptsRef = useRef<InstalledScript[]>([]);
62+
const statusCheckTimeoutRef = useRef<NodeJS.Timeout | null>(null);
6263

6364
// Error modal state
6465
const [errorModal, setErrorModal] = useState<{
@@ -312,17 +313,34 @@ export function InstalledScriptsTab() {
312313

313314
// Function to fetch container statuses - simplified to just check all servers
314315
const fetchContainerStatuses = useCallback(() => {
315-
const currentScripts = scriptsRef.current;
316+
console.log('fetchContainerStatuses called, isPending:', containerStatusMutation.isPending);
316317

317-
// Get unique server IDs from scripts
318-
const serverIds = [...new Set(currentScripts
319-
.filter(script => script.server_id)
320-
.map(script => script.server_id!))];
321-
322-
if (serverIds.length > 0) {
323-
containerStatusMutation.mutate({ serverIds });
318+
// Prevent multiple simultaneous status checks
319+
if (containerStatusMutation.isPending) {
320+
console.log('Status check already pending, skipping');
321+
return;
324322
}
325-
}, [containerStatusMutation]);
323+
324+
// Clear any existing timeout
325+
if (statusCheckTimeoutRef.current) {
326+
clearTimeout(statusCheckTimeoutRef.current);
327+
}
328+
329+
// Debounce status checks by 500ms
330+
statusCheckTimeoutRef.current = setTimeout(() => {
331+
const currentScripts = scriptsRef.current;
332+
333+
// Get unique server IDs from scripts
334+
const serverIds = [...new Set(currentScripts
335+
.filter(script => script.server_id)
336+
.map(script => script.server_id!))];
337+
338+
console.log('Executing status check for server IDs:', serverIds);
339+
if (serverIds.length > 0) {
340+
containerStatusMutation.mutate({ serverIds });
341+
}
342+
}, 500);
343+
}, []); // Remove containerStatusMutation from dependencies to prevent loops
326344

327345
// Run cleanup when component mounts and scripts are loaded (only once)
328346
useEffect(() => {
@@ -335,9 +353,19 @@ export function InstalledScriptsTab() {
335353

336354
useEffect(() => {
337355
if (scripts.length > 0) {
356+
console.log('Status check triggered - scripts length:', scripts.length);
338357
fetchContainerStatuses();
339358
}
340-
}, [scripts.length, fetchContainerStatuses]);
359+
}, [scripts.length]); // Remove fetchContainerStatuses from dependencies
360+
361+
// Cleanup timeout on unmount
362+
useEffect(() => {
363+
return () => {
364+
if (statusCheckTimeoutRef.current) {
365+
clearTimeout(statusCheckTimeoutRef.current);
366+
}
367+
};
368+
}, []);
341369

342370
const scriptsWithStatus = scripts.map(script => ({
343371
...script,

src/app/_components/SSHKeyInput.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,25 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
9393
);
9494

9595
if (keyLine) {
96-
const keyType = keyLine.includes('RSA') ? 'RSA' :
97-
keyLine.includes('ED25519') ? 'ED25519' :
98-
keyLine.includes('ECDSA') ? 'ECDSA' : 'Unknown';
96+
let keyType = 'Unknown';
97+
98+
// Check for traditional PEM format keys
99+
if (keyLine.includes('RSA')) {
100+
keyType = 'RSA';
101+
} else if (keyLine.includes('ED25519')) {
102+
keyType = 'ED25519';
103+
} else if (keyLine.includes('ECDSA')) {
104+
keyType = 'ECDSA';
105+
} else if (keyLine.includes('OPENSSH PRIVATE KEY')) {
106+
// For OpenSSH format keys, try to detect type from the key content
107+
// Look for common patterns in the base64 content
108+
const base64Content = keyContent.replace(/-----BEGIN.*?-----/, '').replace(/-----END.*?-----/, '').replace(/\s/g, '');
109+
110+
// This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns
111+
// We'll default to "OpenSSH" for now since we can't reliably detect the type
112+
keyType = 'OpenSSH';
113+
}
114+
99115
return `${keyType} key (${keyContent.length} characters)`;
100116
}
101117

@@ -142,7 +158,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
142158
<input
143159
ref={fileInputRef}
144160
type="file"
145-
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa"
161+
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa,ed25519,id_rsa,id_ed25519,id_ecdsa,*"
146162
onChange={handleFileSelect}
147163
className="hidden"
148164
disabled={disabled}
@@ -153,7 +169,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
153169
Drag and drop your SSH private key here, or click to browse
154170
</p>
155171
<p className="text-xs text-muted-foreground">
156-
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, etc.)
172+
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, ed25519, etc.)
157173
</p>
158174
</div>
159175
</div>

src/server/api/routers/installedScripts.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -551,23 +551,31 @@ export const installedScriptsRouter = createTRPCRouter({
551551
const listCommand = 'pct list';
552552
let listOutput = '';
553553

554-
await new Promise<void>((resolve, reject) => {
555-
void sshExecutionService.executeCommand(
556-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
557-
server as any,
558-
listCommand,
559-
(data: string) => {
560-
listOutput += data;
561-
},
562-
(error: string) => {
563-
console.error(`pct list error on server ${(server as any).name}:`, error);
564-
reject(new Error(error));
565-
},
566-
(_exitCode: number) => {
567-
resolve();
568-
}
569-
);
554+
// Add timeout to prevent hanging connections
555+
const timeoutPromise = new Promise<never>((_, reject) => {
556+
setTimeout(() => reject(new Error('SSH command timeout after 30 seconds')), 30000);
570557
});
558+
559+
await Promise.race([
560+
new Promise<void>((resolve, reject) => {
561+
void sshExecutionService.executeCommand(
562+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
563+
server as any,
564+
listCommand,
565+
(data: string) => {
566+
listOutput += data;
567+
},
568+
(error: string) => {
569+
console.error(`pct list error on server ${(server as any).name}:`, error);
570+
reject(new Error(error));
571+
},
572+
(_exitCode: number) => {
573+
resolve();
574+
}
575+
);
576+
}),
577+
timeoutPromise
578+
]);
571579

572580
// Parse pct list output
573581
const lines = listOutput.split('\n').filter(line => line.trim());

src/server/ssh-execution-service.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ class SSHExecutionService {
3232
const tempDir = mkdtempSync(join(tmpdir(), 'ssh-key-'));
3333
const tempKeyPath = join(tempDir, 'private_key');
3434

35-
writeFileSync(tempKeyPath, ssh_key);
35+
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
36+
const normalizedKey = ssh_key.trimEnd() + '\n';
37+
writeFileSync(tempKeyPath, normalizedKey);
3638
chmodSync(tempKeyPath, 0o600); // Set proper permissions
3739

3840
return tempKeyPath;
@@ -295,7 +297,7 @@ class SSHExecutionService {
295297
try {
296298
unlinkSync(tempKeyPath);
297299
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
298-
unlinkSync(tempDir);
300+
rmdirSync(tempDir);
299301
} catch (cleanupError) {
300302
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
301303
}
@@ -314,7 +316,7 @@ class SSHExecutionService {
314316
try {
315317
unlinkSync(tempKeyPath);
316318
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
317-
unlinkSync(tempDir);
319+
rmdirSync(tempDir);
318320
} catch (cleanupError) {
319321
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
320322
}
@@ -328,7 +330,7 @@ class SSHExecutionService {
328330
try {
329331
unlinkSync(tempKeyPath);
330332
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
331-
unlinkSync(tempDir);
333+
rmdirSync(tempDir);
332334
} catch (cleanupError) {
333335
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
334336
}
@@ -383,7 +385,7 @@ class SSHExecutionService {
383385
try {
384386
unlinkSync(tempKeyPath);
385387
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
386-
unlinkSync(tempDir);
388+
rmdirSync(tempDir);
387389
} catch (cleanupError) {
388390
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
389391
}
@@ -414,7 +416,7 @@ class SSHExecutionService {
414416
try {
415417
unlinkSync(tempKeyPath);
416418
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
417-
unlinkSync(tempDir);
419+
rmdirSync(tempDir);
418420
} catch (cleanupError) {
419421
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
420422
}

src/server/ssh-service.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,9 @@ expect {
557557
tempKeyPath = join(tempDir, 'private_key');
558558

559559
// Write the private key to temporary file
560-
writeFileSync(tempKeyPath, ssh_key);
560+
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
561+
const normalizedKey = ssh_key.trimEnd() + '\n';
562+
writeFileSync(tempKeyPath, normalizedKey);
561563
chmodSync(tempKeyPath, 0o600); // Set proper permissions
562564

563565
// Build SSH command

0 commit comments

Comments
 (0)