|
|
@ -12,7 +12,7 @@ export enum SSHConfigType {
|
|
|
|
COMPUTED = 'COMPUTED',
|
|
|
|
COMPUTED = 'COMPUTED',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const PAIR_REGEX = /^(\w+)\s*(=|\s)\s*(.+)$/;
|
|
|
|
const PAIR_REGEX = /^(\w+)\s*(?:=|\s)\s*(.+)$/;
|
|
|
|
const QUOTE_REGEX = /^"(.*)"$/;
|
|
|
|
const QUOTE_REGEX = /^"(.*)"$/;
|
|
|
|
|
|
|
|
|
|
|
|
const unquote = (str: string): string => str.replace(QUOTE_REGEX, (_, v) => v);
|
|
|
|
const unquote = (str: string): string => str.replace(QUOTE_REGEX, (_, v) => v);
|
|
|
@ -22,13 +22,13 @@ function checkHostname(hostname: string, target: string): boolean | undefined {
|
|
|
|
if (target === '*') return true;
|
|
|
|
if (target === '*') return true;
|
|
|
|
const negate = target.startsWith('!');
|
|
|
|
const negate = target.startsWith('!');
|
|
|
|
if (negate) target = target.substring(1);
|
|
|
|
if (negate) target = target.substring(1);
|
|
|
|
const regex = new RegExp(`^${target.trim().replace(/[$?]/g, replacePatternChar)}$`);
|
|
|
|
const regex = new RegExp(`^${target.trim().replace(/[*?]/g, replacePatternChar)}$`);
|
|
|
|
return regex.test(hostname) ? !negate : undefined;
|
|
|
|
return regex.test(hostname) ? !negate : undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function checkPatternList(input: string, pattern: string): boolean {
|
|
|
|
function checkPatternList(input: string, pattern: string): boolean {
|
|
|
|
for (const pat of pattern.split(',')) {
|
|
|
|
for (const pat of pattern.split(',')) {
|
|
|
|
const result = checkHostname(input, pattern);
|
|
|
|
const result = checkHostname(input, pat);
|
|
|
|
if (result !== undefined) return result;
|
|
|
|
if (result !== undefined) return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
@ -118,6 +118,14 @@ export class SSHConfig implements Iterable<[string, string[]]> {
|
|
|
|
if (this.type === SSHConfigType.GLOBAL) return [true, []];
|
|
|
|
if (this.type === SSHConfigType.GLOBAL) return [true, []];
|
|
|
|
if (this.type === SSHConfigType.HOST) return [this.checkHost(context.hostname), []];
|
|
|
|
if (this.type === SSHConfigType.HOST) return [this.checkHost(context.hostname), []];
|
|
|
|
if (this.type === SSHConfigType.MATCH) return this.checkMatch(context);
|
|
|
|
if (this.type === SSHConfigType.MATCH) return this.checkMatch(context);
|
|
|
|
|
|
|
|
if (this.type === SSHConfigType.COMPUTED) return [false, [[this.source, 0, 'Cannot match a computed config', Severity.ERROR]]];
|
|
|
|
|
|
|
|
throw new Error(`Unrecognized config type '${this.type}'`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public toString(): string {
|
|
|
|
|
|
|
|
if (this.type === SSHConfigType.GLOBAL) return `SSHConfig(GLOBAL,${this.source}:${this.line})`;
|
|
|
|
|
|
|
|
if (this.type === SSHConfigType.HOST) return `SSHConfig(HOST,${this.source}:${this.line},"${this.get('Host')}")`;
|
|
|
|
|
|
|
|
if (this.type === SSHConfigType.MATCH) return `SSHConfig(MATCH,${this.source}:${this.line},"${this.get('Match')}")`;
|
|
|
|
|
|
|
|
if (this.type === SSHConfigType.COMPUTED) return `SSHConfig(COMPUTED,${this.source}:${this.line})`;
|
|
|
|
throw new Error(`Unrecognized config type '${this.type}'`);
|
|
|
|
throw new Error(`Unrecognized config type '${this.type}'`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -143,7 +151,7 @@ function mergeConfigIntoContext(config: SSHConfig, context: MatchContext): void
|
|
|
|
|
|
|
|
|
|
|
|
export class SSHConfigHolder {
|
|
|
|
export class SSHConfigHolder {
|
|
|
|
public readonly errors: readonly LineError[] = [];
|
|
|
|
public readonly errors: readonly LineError[] = [];
|
|
|
|
public readonly configs: SSHConfig[] = [new SSHConfig(SSHConfigType.GLOBAL, this.source, 0)];
|
|
|
|
public readonly configs: SSHConfig[] = [];
|
|
|
|
constructor(public readonly source: string) { }
|
|
|
|
constructor(public readonly source: string) { }
|
|
|
|
public reportError(line: number, message: string, severity = Severity.ERROR): void {
|
|
|
|
public reportError(line: number, message: string, severity = Severity.ERROR): void {
|
|
|
|
(this.errors as LineError[]).push([this.source, line, message, severity]);
|
|
|
|
(this.errors as LineError[]).push([this.source, line, message, severity]);
|
|
|
@ -158,9 +166,14 @@ export class SSHConfigHolder {
|
|
|
|
context = { ...context };
|
|
|
|
context = { ...context };
|
|
|
|
const result = new SSHConfig(SSHConfigType.COMPUTED, this.source, 0);
|
|
|
|
const result = new SSHConfig(SSHConfigType.COMPUTED, this.source, 0);
|
|
|
|
for (const config of this.configs) {
|
|
|
|
for (const config of this.configs) {
|
|
|
|
if (!config.matches(context)) continue;
|
|
|
|
if (!config.matches(context)) {
|
|
|
|
|
|
|
|
Logging.debug(`Config ${config} does not match context ${JSON.stringify(context)}, ignoring`);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Logging.debug(`Config ${config} matches context ${JSON.stringify(context)}, merging`);
|
|
|
|
result.merge(config);
|
|
|
|
result.merge(config);
|
|
|
|
mergeConfigIntoContext(result, context);
|
|
|
|
mergeConfigIntoContext(result, context);
|
|
|
|
|
|
|
|
Logging.debug(` New context: ${JSON.stringify(context)}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -177,7 +190,8 @@ const ERR_MULTIPLE_IDENTITY_FILE = 'Multiple IdentityFiles given, the extension
|
|
|
|
|
|
|
|
|
|
|
|
export function parseContents(content: string, source: string): SSHConfigHolder {
|
|
|
|
export function parseContents(content: string, source: string): SSHConfigHolder {
|
|
|
|
const holder = new SSHConfigHolder(source);
|
|
|
|
const holder = new SSHConfigHolder(source);
|
|
|
|
let current = holder.configs[0];
|
|
|
|
let current = new SSHConfig(SSHConfigType.GLOBAL, source, 0);
|
|
|
|
|
|
|
|
holder.add(current);
|
|
|
|
content.split('\n').forEach((line, lineNumber) => {
|
|
|
|
content.split('\n').forEach((line, lineNumber) => {
|
|
|
|
line = line.trim();
|
|
|
|
line = line.trim();
|
|
|
|
if (!line || line.startsWith('#')) return;
|
|
|
|
if (!line || line.startsWith('#')) return;
|
|
|
@ -189,7 +203,7 @@ export function parseContents(content: string, source: string): SSHConfigHolder
|
|
|
|
// TODO: "Include ..."
|
|
|
|
// TODO: "Include ..."
|
|
|
|
switch (key) {
|
|
|
|
switch (key) {
|
|
|
|
case 'host':
|
|
|
|
case 'host':
|
|
|
|
holder.add(new SSHConfig(SSHConfigType.HOST, source, lineNumber));
|
|
|
|
holder.add(current = new SSHConfig(SSHConfigType.HOST, source, lineNumber));
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
case 'match':
|
|
|
|
case 'match':
|
|
|
|
holder.add(current = new SSHConfig(SSHConfigType.MATCH, source, lineNumber));
|
|
|
|
holder.add(current = new SSHConfig(SSHConfigType.MATCH, source, lineNumber));
|
|
|
@ -233,7 +247,7 @@ export async function buildHolder(paths: string[]): Promise<SSHConfigHolder> {
|
|
|
|
if (!sev) return holder;
|
|
|
|
if (!sev) return holder;
|
|
|
|
const key = SEVERITY_TO_STRING[sev];
|
|
|
|
const key = SEVERITY_TO_STRING[sev];
|
|
|
|
Logging[key](`Building ssh_config holder produced ${key} messages:`, LOGGING_NO_STACKTRACE);
|
|
|
|
Logging[key](`Building ssh_config holder produced ${key} messages:`, LOGGING_NO_STACKTRACE);
|
|
|
|
for (const error of holder.errors) Logging[key](formatLineError(error));
|
|
|
|
for (const error of holder.errors) Logging[key]('- ' + formatLineError(error));
|
|
|
|
Logging[key]('End of ssh_config holder messages', LOGGING_NO_STACKTRACE);
|
|
|
|
Logging[key]('End of ssh_config holder messages', LOGGING_NO_STACKTRACE);
|
|
|
|
return holder;
|
|
|
|
return holder;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -262,7 +276,7 @@ export async function fillFileSystemConfig(config: FileSystemConfig, holder: SSH
|
|
|
|
privateKeyPath: result.get('IdentityFile'),
|
|
|
|
privateKeyPath: result.get('IdentityFile'),
|
|
|
|
tryKeyboard: toBoolean(result.get('KbdInteractiveAuthentication')),
|
|
|
|
tryKeyboard: toBoolean(result.get('KbdInteractiveAuthentication')),
|
|
|
|
// TODO: LocalCommand, PermitLocalCommand, RemoteCommand
|
|
|
|
// TODO: LocalCommand, PermitLocalCommand, RemoteCommand
|
|
|
|
password: toBoolean(result.get('PasswordAuthentication')) as any,
|
|
|
|
password: (toBoolean(result.get('PasswordAuthentication')) != false) as any,
|
|
|
|
port: parseInt(result.get('Port')),
|
|
|
|
port: parseInt(result.get('Port')),
|
|
|
|
// TODO: PreferredAuthentications (ssh2's non-documented authHandler config property?)
|
|
|
|
// TODO: PreferredAuthentications (ssh2's non-documented authHandler config property?)
|
|
|
|
// TODO: ProxyCommand, ProxyJump, ProxyUseFdpass (can't support the latter I'm afraid)
|
|
|
|
// TODO: ProxyCommand, ProxyJump, ProxyUseFdpass (can't support the latter I'm afraid)
|
|
|
@ -281,7 +295,7 @@ export async function fillFileSystemConfig(config: FileSystemConfig, holder: SSH
|
|
|
|
const val = overrides[key];
|
|
|
|
const val = overrides[key];
|
|
|
|
if (val === '') delete overrides[key];
|
|
|
|
if (val === '') delete overrides[key];
|
|
|
|
if (val === []) delete overrides[key];
|
|
|
|
if (val === []) delete overrides[key];
|
|
|
|
if (isNaN(val)) delete overrides[key];
|
|
|
|
if (typeof val === 'number' && isNaN(val)) delete overrides[key];
|
|
|
|
if (val === undefined) delete overrides[key];
|
|
|
|
if (val === undefined) delete overrides[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Logging.debug(`Config overrides for ${config.name} generated from ssh_config files: ${JSON.stringify(censorConfig(overrides as any), null, 4)}`);
|
|
|
|
Logging.debug(`Config overrides for ${config.name} generated from ssh_config files: ${JSON.stringify(censorConfig(overrides as any), null, 4)}`);
|
|
|
|