fix: handle special characters in SSH password/passphrase (Fixes #312)
- Use sshpass -f with temp file in transferScriptsFolder so password/passphrase
never go through shell; safe for {, $, ", etc.
- Pass password via SSH_PASSWORD env in testWithExpect instead of embedding
in script
- Add ServerForm hint: SSH key recommended; special chars supported
This commit is contained in:
@@ -438,6 +438,11 @@ export function ServerForm({
|
|||||||
{errors.password && (
|
{errors.password && (
|
||||||
<p className="text-destructive mt-1 text-sm">{errors.password}</p>
|
<p className="text-destructive mt-1 text-sm">{errors.password}</p>
|
||||||
)}
|
)}
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
SSH key is recommended when possible. Special characters (e.g.{" "}
|
||||||
|
<code className="rounded bg-muted px-0.5">{"{ } $ \" '"}</code>) are
|
||||||
|
supported.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { spawn as ptySpawn } from 'node-pty';
|
import { spawn as ptySpawn } from 'node-pty';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,9 +197,22 @@ class SSHExecutionService {
|
|||||||
async transferScriptsFolder(server, onData, onError) {
|
async transferScriptsFolder(server, onData, onError) {
|
||||||
const { ip, user, password, auth_type = 'password', ssh_key_passphrase, ssh_key_path, ssh_port = 22 } = server;
|
const { ip, user, password, auth_type = 'password', ssh_key_passphrase, ssh_key_path, ssh_port = 22 } = server;
|
||||||
|
|
||||||
|
const cleanupTempFile = (/** @type {string | null} */ tempPath) => {
|
||||||
|
if (tempPath) {
|
||||||
|
try {
|
||||||
|
unlinkSync(tempPath);
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
/** @type {string | null} */
|
||||||
|
let tempPath = null;
|
||||||
try {
|
try {
|
||||||
// Build rsync command based on authentication type
|
// Build rsync command based on authentication type.
|
||||||
|
// Use sshpass -f with a temp file so password/passphrase never go through the shell (safe for special chars like {, $, ").
|
||||||
let rshCommand;
|
let rshCommand;
|
||||||
if (auth_type === 'key') {
|
if (auth_type === 'key') {
|
||||||
if (!ssh_key_path || !existsSync(ssh_key_path)) {
|
if (!ssh_key_path || !existsSync(ssh_key_path)) {
|
||||||
@@ -205,13 +220,19 @@ class SSHExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ssh_key_passphrase) {
|
if (ssh_key_passphrase) {
|
||||||
rshCommand = `sshpass -P passphrase -p ${ssh_key_passphrase} ssh -i ${ssh_key_path} -p ${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
tempPath = join(tmpdir(), `sshpass-${process.pid}-${Date.now()}.tmp`);
|
||||||
|
writeFileSync(tempPath, ssh_key_passphrase);
|
||||||
|
chmodSync(tempPath, 0o600);
|
||||||
|
rshCommand = `sshpass -P passphrase -f ${tempPath} ssh -i ${ssh_key_path} -p ${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
||||||
} else {
|
} else {
|
||||||
rshCommand = `ssh -i ${ssh_key_path} -p ${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
rshCommand = `ssh -i ${ssh_key_path} -p ${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Password authentication
|
// Password authentication
|
||||||
rshCommand = `sshpass -p ${password} ssh -p ${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
tempPath = join(tmpdir(), `sshpass-${process.pid}-${Date.now()}.tmp`);
|
||||||
|
writeFileSync(tempPath, password ?? '');
|
||||||
|
chmodSync(tempPath, 0o600);
|
||||||
|
rshCommand = `sshpass -f ${tempPath} ssh -p ${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rsyncCommand = spawn('rsync', [
|
const rsyncCommand = spawn('rsync', [
|
||||||
@@ -226,31 +247,31 @@ class SSHExecutionService {
|
|||||||
stdio: ['pipe', 'pipe', 'pipe']
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
|
||||||
rsyncCommand.stdout.on('data', (/** @type {Buffer} */ data) => {
|
rsyncCommand.stdout.on('data', (/** @type {Buffer} */ data) => {
|
||||||
// Ensure proper UTF-8 encoding for ANSI colors
|
const output = data.toString('utf8');
|
||||||
const output = data.toString('utf8');
|
onData(output);
|
||||||
onData(output);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
rsyncCommand.stderr.on('data', (/** @type {Buffer} */ data) => {
|
rsyncCommand.stderr.on('data', (/** @type {Buffer} */ data) => {
|
||||||
// Ensure proper UTF-8 encoding for ANSI colors
|
const output = data.toString('utf8');
|
||||||
const output = data.toString('utf8');
|
onError(output);
|
||||||
onError(output);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
rsyncCommand.on('close', (code) => {
|
rsyncCommand.on('close', (code) => {
|
||||||
if (code === 0) {
|
cleanupTempFile(tempPath);
|
||||||
resolve();
|
if (code === 0) {
|
||||||
} else {
|
resolve();
|
||||||
reject(new Error(`rsync failed with code ${code}`));
|
} else {
|
||||||
}
|
reject(new Error(`rsync failed with code ${code}`));
|
||||||
});
|
}
|
||||||
|
});
|
||||||
rsyncCommand.on('error', (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
rsyncCommand.on('error', (error) => {
|
||||||
|
cleanupTempFile(tempPath);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
cleanupTempFile(tempPath);
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -169,16 +169,17 @@ class SSHService {
|
|||||||
const timeout = 10000;
|
const timeout = 10000;
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|
||||||
|
// Pass password via env so it is not embedded in the script (safe for special chars like {, $, ").
|
||||||
const expectScript = `#!/usr/bin/expect -f
|
const expectScript = `#!/usr/bin/expect -f
|
||||||
set timeout 10
|
set timeout 10
|
||||||
spawn ssh -p ${ssh_port} -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PasswordAuthentication=yes -o PubkeyAuthentication=no ${user}@${ip} "echo SSH_LOGIN_SUCCESS"
|
spawn ssh -p ${ssh_port} -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PasswordAuthentication=yes -o PubkeyAuthentication=no ${user}@${ip} "echo SSH_LOGIN_SUCCESS"
|
||||||
expect {
|
expect {
|
||||||
"password:" {
|
"password:" {
|
||||||
send "${password}\r"
|
send "$env(SSH_PASSWORD)\\r"
|
||||||
exp_continue
|
exp_continue
|
||||||
}
|
}
|
||||||
"Password:" {
|
"Password:" {
|
||||||
send "${password}\r"
|
send "$env(SSH_PASSWORD)\\r"
|
||||||
exp_continue
|
exp_continue
|
||||||
}
|
}
|
||||||
"SSH_LOGIN_SUCCESS" {
|
"SSH_LOGIN_SUCCESS" {
|
||||||
@@ -193,7 +194,8 @@ expect {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
const expectCommand = spawn('expect', ['-c', expectScript], {
|
const expectCommand = spawn('expect', ['-c', expectScript], {
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
env: { ...process.env, SSH_PASSWORD: password ?? '' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user