Skip to content

Commit 48653c0

Browse files
authored
Don't throw when parsing SSH config (#9933)
1 parent 49e5dfd commit 48653c0

File tree

2 files changed

+45
-36
lines changed

2 files changed

+45
-36
lines changed

Extension/src/Debugger/extension.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import { getActiveSshTarget, initializeSshTargets, selectSshTarget, SshTargetsPr
1515
import { addSshTargetCmd, BaseNode, refreshCppSshTargetsViewCmd } from '../SSH/TargetsView/common';
1616
import { setActiveSshTarget, TargetLeafNode } from '../SSH/TargetsView/targetNodes';
1717
import { sshCommandToConfig } from '../SSH/sshCommandToConfig';
18-
import { getSshConfiguration, getSshConfigurationFiles, writeSshConfiguration } from '../SSH/sshHosts';
19-
import { pathAccessible } from '../common';
20-
import * as fs from 'fs';
18+
import { getSshConfiguration, getSshConfigurationFiles, parseFailures, writeSshConfiguration } from '../SSH/sshHosts';
2119
import { Configuration } from 'ssh-config';
2220
import { CppSettings } from '../LanguageServer/settings';
2321
import * as chokidar from 'chokidar';
22+
import { getSshChannel } from '../logger';
23+
import { pathAccessible } from '../common';
2424

2525
// The extension deactivate method is asynchronous, so we handle the disposables ourselves instead of using extensionContext.subscriptions.
2626
const disposables: vscode.Disposable[] = [];
@@ -164,6 +164,18 @@ async function disableSshTargetsView(): Promise<void> {
164164
}
165165

166166
async function addSshTargetImpl(): Promise<string> {
167+
const validConfigFiles: string[] = [];
168+
for (const configFile of getSshConfigurationFiles()) {
169+
if (await pathAccessible(configFile) && parseFailures.get(configFile)) {
170+
getSshChannel().appendLine(localize('cannot.modify.config.file', 'Cannot modify SSH configuration file because of parse failure "{0}".', configFile));
171+
} else {
172+
validConfigFiles.push(configFile);
173+
}
174+
}
175+
if (validConfigFiles.length === 0) {
176+
throw new Error(localize('no.valid.ssh.config.file', 'No valid SSH configuration file found.'));
177+
}
178+
167179
const name: string | undefined = await vscode.window.showInputBox({
168180
title: localize('enter.ssh.target.name', 'Enter SSH Target Name'),
169181
placeHolder: localize('ssh.target.name.place.holder', 'Example: `mySSHTarget`'),
@@ -185,7 +197,7 @@ async function addSshTargetImpl(): Promise<string> {
185197

186198
const newEntry: { [key: string]: string } = sshCommandToConfig(command, name);
187199

188-
const targetFile: string | undefined = await vscode.window.showQuickPick(getSshConfigurationFiles().filter(file => pathAccessible(file, fs.constants.W_OK)), { title: localize('select.ssh.config.file', 'Select an SSH configuration file') });
200+
const targetFile: string | undefined = await vscode.window.showQuickPick(validConfigFiles, { title: localize('select.ssh.config.file', 'Select an SSH configuration file') });
189201
if (!targetFile) {
190202
return '';
191203
}

Extension/src/SSH/sshHosts.ts

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
HostConfigurationDirective
1717
} from 'ssh-config';
1818
import { promisify } from 'util';
19-
import { ISshConfigHostInfo, ISshHostInfo, isWindows, resolveHome } from "../common";
19+
import { ISshConfigHostInfo, isWindows, resolveHome } from "../common";
2020
import { getSshChannel } from '../logger';
2121
import * as glob from 'glob';
2222
import * as vscode from 'vscode';
@@ -32,6 +32,11 @@ const userSshConfigurationFile: string = path.resolve(os.homedir(), '.ssh/config
3232
const ProgramData: string = process.env.ALLUSERSPROFILE || process.env.PROGRAMDATA || 'C:\\ProgramData';
3333
const systemSshConfigurationFile: string = isWindows() ? `${ProgramData}\\ssh\\ssh_config` : '/etc/ssh/ssh_config';
3434

35+
// Stores if the SSH config files are parsed successfully.
36+
// Only store root config files' failure status since included files are not modified by our extension.
37+
// path => successful
38+
export const parseFailures: Map<string, boolean> = new Map<string, boolean>();
39+
3540
export function getSshConfigurationFiles(): string[] {
3641
return [userSshConfigurationFile, systemSshConfigurationFile];
3742
}
@@ -64,42 +69,24 @@ function extractHostNames(parsedConfig: Configuration): { [host: string]: string
6469
return hostNames;
6570
}
6671

67-
export async function getConfigurationForHost(host: ISshHostInfo): Promise<ResolvedConfiguration | null> {
68-
return getConfigurationForHostImpl(host, getSshConfigurationFiles());
69-
}
70-
71-
export async function getConfigurationForHostImpl(
72-
host: ISshHostInfo,
73-
configPaths: string[]
74-
): Promise<ResolvedConfiguration | null> {
75-
for (const configPath of configPaths) {
76-
const configuration: Configuration = await getSshConfiguration(configPath);
77-
const config: ResolvedConfiguration = configuration.compute(host.hostName);
78-
79-
if (!config || !config.HostName) {
80-
// No real matching config was found
81-
continue;
82-
}
83-
84-
if (config.IdentityFile) {
85-
config.IdentityFile = config.IdentityFile.map(resolveHome);
86-
}
87-
88-
return config;
89-
}
90-
91-
return null;
92-
}
93-
9472
/**
9573
* Gets parsed SSH configuration from file. Resolves Include directives as well unless specified otherwise.
9674
* @param configurationPath the location of the config file
9775
* @param resolveIncludes by default this is set to true
9876
* @returns
9977
*/
10078
export async function getSshConfiguration(configurationPath: string, resolveIncludes: boolean = true): Promise<Configuration> {
79+
parseFailures.set(configurationPath, false);
10180
const src: string = await getSshConfigSource(configurationPath);
102-
const config: Configuration = caseNormalizeConfigProps(parse(src));
81+
let parsedSrc: Configuration | undefined;
82+
try {
83+
parsedSrc = parse(src);
84+
} catch (err) {
85+
parseFailures.set(configurationPath, true);
86+
getSshChannel().appendLine(localize("failed.to.parse.SSH.config", "Failed to parse SSH configuration file {0}: {1}", configurationPath, (err as Error).message));
87+
return parse('');
88+
}
89+
const config: Configuration = caseNormalizeConfigProps(parsedSrc);
10390
if (resolveIncludes) {
10491
await resolveConfigIncludes(config, configurationPath);
10592
}
@@ -128,13 +115,22 @@ async function resolveConfigIncludes(config: Configuration, configPath: string):
128115
}
129116

130117
async function getIncludedConfigFile(config: Configuration, includePath: string): Promise<void> {
118+
let includedContents: string;
131119
try {
132-
const includedContents: string = (await fs.readFile(includePath)).toString();
133-
const parsed: Configuration = parse(includedContents);
134-
config.push(...parsed);
120+
includedContents = (await fs.readFile(includePath)).toString();
135121
} catch (e) {
136122
getSshChannel().appendLine(localize("failed.to.read.file", "Failed to read file {0}.", includePath));
123+
return;
124+
}
125+
126+
let parsedIncludedContents: Configuration | undefined;
127+
try {
128+
parsedIncludedContents = parse(includedContents);
129+
} catch (err) {
130+
getSshChannel().appendLine(localize("failed.to.parse.SSH.config", "Failed to parse SSH configuration file {0}: {1}", includePath, (err as Error).message));
131+
return;
137132
}
133+
config.push(...parsedIncludedContents);
138134
}
139135

140136
export async function writeSshConfiguration(configurationPath: string, configuration: Configuration): Promise<void> {
@@ -153,6 +149,7 @@ async function getSshConfigSource(configurationPath: string): Promise<string> {
153149
const buffer: Buffer = await fs.readFile(configurationPath);
154150
return buffer.toString('utf8');
155151
} catch (e) {
152+
parseFailures.set(configurationPath, true);
156153
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
157154
return '';
158155
}

0 commit comments

Comments
 (0)