diff --git a/Extension/src/SSH/sshHosts.ts b/Extension/src/SSH/sshHosts.ts index 97d3b625c..98c4d9308 100644 --- a/Extension/src/SSH/sshHosts.ts +++ b/Extension/src/SSH/sshHosts.ts @@ -99,28 +99,64 @@ export async function getSshConfiguration(configurationPath: string, resolveIncl return config; } -async function resolveConfigIncludes(config: Configuration, configPath: string): Promise { - for (const entry of config) { - if (isDirective(entry) && entry.param === 'Include') { - let includePath: string = resolveHome(entry.value); - if (isWindows && !!includePath.match(/^\/[a-z]:/i)) { - includePath = includePath.substr(1); - } - - if (!path.isAbsolute(includePath)) { - includePath = path.resolve(path.dirname(configPath), includePath); - } - - const pathsToGetFilesFrom: string[] = await globAsync(includePath); +function getProcessedPathKey(filePath: string): string { + const absolutePath: string = path.resolve(filePath); + const normalizedPath: string = path.normalize(absolutePath); + return isWindows ? normalizedPath.toLowerCase() : normalizedPath; +} - for (const filePath of pathsToGetFilesFrom) { - await getIncludedConfigFile(config, filePath); +async function resolveConfigIncludes( + config: Configuration, + configPath: string, + processedIncludePaths?: Set, + processedIncludeEntries?: WeakSet +): Promise { + processedIncludePaths = processedIncludePaths ?? new Set(); + processedIncludeEntries = processedIncludeEntries ?? new WeakSet(); + const configKey: string = getProcessedPathKey(configPath); + if (processedIncludePaths.has(configKey)) { + return; + } + processedIncludePaths.add(configKey); + try { + for (const entry of config) { + if (isDirective(entry) && entry.param === 'Include') { + // Prevent duplicate expansion of the same Include directive within a single resolution pass. + if (processedIncludeEntries.has(entry)) { + continue; + } + processedIncludeEntries.add(entry); + let includePath: string = resolveHome(entry.value); + if (isWindows && !!includePath.match(/^\/[a-z]:/i)) { + includePath = includePath.slice(1); + } + + if (!path.isAbsolute(includePath)) { + includePath = path.resolve(path.dirname(configPath), includePath); + } + + const pathsToGetFilesFrom: string[] = await globAsync(includePath); + + for (const filePath of pathsToGetFilesFrom) { + const includeKey: string = getProcessedPathKey(filePath); + if (processedIncludePaths.has(includeKey)) { + continue; + } + await getIncludedConfigFile(config, filePath, processedIncludePaths, processedIncludeEntries); + } } } + } finally { + processedIncludePaths.delete(configKey); } } -async function getIncludedConfigFile(config: Configuration, includePath: string): Promise { +async function getIncludedConfigFile( + config: Configuration, + includePath: string, + processedIncludePaths: Set, + processedIncludeEntries: WeakSet +): Promise { let includedContents: string; try { includedContents = (await fs.readFile(includePath)).toString(); @@ -136,6 +172,7 @@ async function getIncludedConfigFile(config: Configuration, includePath: string) getSshChannel().appendLine(localize("failed.to.parse.SSH.config", "Failed to parse SSH configuration file {0}: {1}", includePath, (err as Error).message)); return; } + await resolveConfigIncludes(parsedIncludedContents, includePath, processedIncludePaths, processedIncludeEntries); config.push(...parsedIncludedContents); }