108 lines
2.8 KiB
TypeScript
108 lines
2.8 KiB
TypeScript
import * as plugins from './smartssh.plugins.js';
|
|
import { SshKey } from './smartssh.classes.sshkey.js';
|
|
|
|
const unsafeHostMessage =
|
|
'SSH host aliases must be single, filename-safe values without whitespace or path separators.';
|
|
|
|
export const assertSafeHost = (hostArg: string) => {
|
|
if (!hostArg || hostArg === '.' || hostArg === '..') {
|
|
throw new Error(unsafeHostMessage);
|
|
}
|
|
|
|
if (!/^[A-Za-z0-9._@:%+-]+$/.test(hostArg)) {
|
|
throw new Error(unsafeHostMessage);
|
|
}
|
|
};
|
|
|
|
export const isSafeHost = (hostArg: string) => {
|
|
try {
|
|
assertSafeHost(hostArg);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const resolveSshDirPath = (dirPathArg?: string) => {
|
|
return plugins.path.resolve(
|
|
dirPathArg ?? plugins.path.join(plugins.smartpath.get.home(), '.ssh')
|
|
);
|
|
};
|
|
|
|
export const ensureSshDirSync = (dirPathArg: string) => {
|
|
plugins.fs.ensureDirSync(dirPathArg);
|
|
plugins.fs.chmodSync(dirPathArg, 0o700);
|
|
};
|
|
|
|
export const quoteSshConfigValue = (valueArg: string) => {
|
|
if (/^[A-Za-z0-9._~/:@%+-]+$/.test(valueArg)) {
|
|
return valueArg;
|
|
}
|
|
|
|
return `"${valueArg.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
};
|
|
|
|
export const fingerprintSha256 = (keyArg: Buffer | string) => {
|
|
const hash = plugins.crypto
|
|
.createHash('sha256')
|
|
.update(keyArg)
|
|
.digest('base64')
|
|
.replace(/=+$/, '');
|
|
return `SHA256:${hash}`;
|
|
};
|
|
|
|
export const fingerprintMd5 = (keyArg: Buffer | string) => {
|
|
const hash = plugins.crypto.createHash('md5').update(keyArg).digest('hex');
|
|
return hash.match(/.{2}/g)?.join(':') ?? hash;
|
|
};
|
|
|
|
export const sshKeyArrayFromDir = (dirArg: string): SshKey[] => {
|
|
const resolvedDir = resolveSshDirPath(dirArg);
|
|
if (!plugins.fs.pathExistsSync(resolvedDir)) {
|
|
return [];
|
|
}
|
|
|
|
const entries = plugins.fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
const keyMap = new Map<string, { private?: string; public?: string }>();
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
|
|
const fileName = entry.name;
|
|
if (
|
|
fileName === 'config' ||
|
|
fileName === 'authorized_keys' ||
|
|
fileName === 'known_hosts' ||
|
|
fileName.startsWith('.')
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const isPublicKey = fileName.endsWith('.pub');
|
|
const host = isPublicKey ? fileName.slice(0, -4) : fileName;
|
|
if (!isSafeHost(host)) {
|
|
continue;
|
|
}
|
|
|
|
const keyRecord = keyMap.get(host) ?? {};
|
|
const filePath = plugins.path.join(resolvedDir, fileName);
|
|
const keyContent = plugins.fs.readFileSync(filePath, 'utf8');
|
|
if (isPublicKey) {
|
|
keyRecord.public = keyContent;
|
|
} else {
|
|
keyRecord.private = keyContent;
|
|
}
|
|
keyMap.set(host, keyRecord);
|
|
}
|
|
|
|
return Array.from(keyMap.entries()).map(([host, keyRecord]) => {
|
|
return new SshKey({
|
|
host,
|
|
private: keyRecord.private,
|
|
public: keyRecord.public,
|
|
});
|
|
});
|
|
};
|