feat(sshclient): add a promise-first SSH client with secure host verification and improve SSH key/config handling
This commit is contained in:
@@ -2,21 +2,40 @@ import * as plugins from './smartssh.plugins.js';
|
||||
import * as helpers from './smartssh.classes.helpers.js';
|
||||
import { SshKey } from './smartssh.classes.sshkey.js';
|
||||
|
||||
export type TSshStrictHostKeyChecking = boolean | 'accept-new';
|
||||
|
||||
export interface ISshConfigOptions {
|
||||
strictHostKeyChecking?: TSshStrictHostKeyChecking;
|
||||
}
|
||||
|
||||
export interface ISshConfigHostBlock {
|
||||
host: string;
|
||||
values: Record<string, string>;
|
||||
}
|
||||
|
||||
export class SshConfig {
|
||||
private _sshKeyArray: SshKey[];
|
||||
constructor(sshKeyArrayArg: SshKey[]) {
|
||||
private _options: ISshConfigOptions;
|
||||
constructor(sshKeyArrayArg: SshKey[], optionsArg: ISshConfigOptions = {}) {
|
||||
this._sshKeyArray = sshKeyArrayArg;
|
||||
this._options = {
|
||||
...optionsArg,
|
||||
strictHostKeyChecking: optionsArg.strictHostKeyChecking ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* stores a config file
|
||||
*/
|
||||
store(dirPathArg: string) {
|
||||
plugins.fs.ensureDirSync(dirPathArg);
|
||||
let configArray: configObject[] = [];
|
||||
const resolvedDir = helpers.resolveSshDirPath(dirPathArg);
|
||||
helpers.ensureSshDirSync(resolvedDir);
|
||||
const configArray: configObject[] = [];
|
||||
for (const sshKey of this._sshKeyArray) {
|
||||
let configString = '';
|
||||
if (sshKey.host) {
|
||||
helpers.assertSafeHost(sshKey.host);
|
||||
const identityFilePath = plugins.path.join(resolvedDir, sshKey.host);
|
||||
configString =
|
||||
'Host ' +
|
||||
sshKey.host +
|
||||
@@ -24,11 +43,20 @@ export class SshConfig {
|
||||
' HostName ' +
|
||||
sshKey.host +
|
||||
'\n' +
|
||||
' IdentityFile ~/.ssh/' +
|
||||
sshKey.host +
|
||||
'\n' +
|
||||
' StrictHostKeyChecking no' +
|
||||
' IdentityFile ' +
|
||||
helpers.quoteSshConfigValue(identityFilePath) +
|
||||
'\n';
|
||||
if (this._options.strictHostKeyChecking !== undefined) {
|
||||
const strictHostKeyChecking = this._options.strictHostKeyChecking;
|
||||
configString +=
|
||||
' StrictHostKeyChecking ' +
|
||||
(strictHostKeyChecking === 'accept-new'
|
||||
? 'accept-new'
|
||||
: strictHostKeyChecking
|
||||
? 'yes'
|
||||
: 'no') +
|
||||
'\n';
|
||||
}
|
||||
}
|
||||
configArray.push({
|
||||
configString: configString,
|
||||
@@ -40,10 +68,52 @@ export class SshConfig {
|
||||
for (const config of configArray) {
|
||||
configFile = configFile + config.configString + '\n';
|
||||
}
|
||||
plugins.fs.writeFileSync(plugins.path.join(dirPathArg, 'config'), configFile);
|
||||
plugins.fs.writeFileSync(plugins.path.join(resolvedDir, 'config'), configFile);
|
||||
plugins.fs.chmodSync(plugins.path.join(resolvedDir, 'config'), 0o600);
|
||||
}
|
||||
read(dirPathArg: string) {
|
||||
return plugins.fs.readFileSync(plugins.path.join(dirPathArg, 'config'), 'utf8');
|
||||
const configPath = plugins.path.join(helpers.resolveSshDirPath(dirPathArg), 'config');
|
||||
return plugins.fs.readFileSync(configPath, 'utf8');
|
||||
}
|
||||
|
||||
parse(dirPathArg: string) {
|
||||
return SshConfig.parse(this.read(dirPathArg));
|
||||
}
|
||||
|
||||
static parse(configStringArg: string): ISshConfigHostBlock[] {
|
||||
const blocks: ISshConfigHostBlock[] = [];
|
||||
let currentBlock: ISshConfigHostBlock | undefined;
|
||||
|
||||
for (const rawLine of configStringArg.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [keyword, ...valueParts] = line.split(/\s+/);
|
||||
const value = valueParts.join(' ');
|
||||
if (!keyword || !value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.toLowerCase() === 'host') {
|
||||
if (!helpers.isSafeHost(value)) {
|
||||
continue;
|
||||
}
|
||||
currentBlock = {
|
||||
host: value,
|
||||
values: {},
|
||||
};
|
||||
blocks.push(currentBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentBlock) {
|
||||
currentBlock.values[keyword.toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user