diff --git a/src/connection.ts b/src/connection.ts index 22901f5..513dd71 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import { configMatches, getFlagBoolean, loadConfigs } from './config'; import type { EnvironmentVariable, FileSystemConfig } from './fileSystemConfig'; import { Logging, LOGGING_NO_STACKTRACE } from './logging'; -import { ActivePortForwarding } from './portForwarding'; +import type { Manager } from './manager'; +import { ActivePortForwarding, addForwarding, formatPortForwarding, parsePortForwarding } from './portForwarding'; import type { SSHPseudoTerminal } from './pseudoTerminal'; import type { SSHFileSystem } from './sshFileSystem'; import { mergeEnvironment, toPromise } from './utils'; @@ -66,6 +67,7 @@ export class ConnectionManager { public readonly onConnectionUpdated = this.onConnectionUpdatedEmitter.event; /** Fired when a pending connection gets added/removed */ public readonly onPendingChanged = this.onPendingChangedEmitter.event; + public constructor(public readonly manager: Manager) { } public getActiveConnection(name: string, config?: FileSystemConfig): Connection | undefined { const con = config && this.connections.find(con => configMatches(con.config, config)); return con || (config ? undefined : this.connections.find(con => con.config.name === name)); @@ -206,6 +208,27 @@ export class ConnectionManager { vscode.window.showErrorMessage(`Reconnect failed: ${e.message || e}`); }); }); + // Setup initial port forwardings + setImmediate(async () => { + const forwards = (actualConfig.forwardings || []).map(parsePortForwarding); + const badForwards = forwards.reduce((tot, f) => f ? tot : tot + 1, 0); + if (badForwards) vscode.window.showWarningMessage(`Could not parse ${badForwards} of ${forwards.length} port forwarding from the config, ignoring them`); + let failed = 0; + console.log(forwards); + for (const forward of forwards) { + logging.debug(`forward: ${forward}`); + if (!forward) continue; + logging.info(`Adding forwarding ${formatPortForwarding(forward)}`); + try { + await addForwarding(this.manager, con, forward); + } catch (e) { + logging.error(`Error during forwarding ${formatPortForwarding(forward)}:`, LOGGING_NO_STACKTRACE); + logging.error(e); + failed++; + } + } + if (failed) vscode.window.showWarningMessage(`Failed ${failed} of ${forwards.length - badForwards} port forwardings from the config`); + }); this.connections.push(con); this.onConnectionAddedEmitter.fire(con); return con; diff --git a/src/fileSystemConfig.ts b/src/fileSystemConfig.ts index f35f7dc..a99f075 100644 --- a/src/fileSystemConfig.ts +++ b/src/fileSystemConfig.ts @@ -10,8 +10,8 @@ export type ConfigLocation = number | string; /** Might support conditional stuff later, although ssh2/OpenSSH might not support that natively */ export interface EnvironmentVariable { - key: string; - value: string; + key: string; + value: string; } export function formatConfigLocation(location?: ConfigLocation): string { @@ -114,6 +114,8 @@ export interface FileSystemConfig extends ConnectConfig { instantConnection?: boolean; /** List of special flags to enable/disable certain fixes/features. Flags are usually used for issues or beta testing. Flags can disappear/change anytime! */ flags?: string[]; + /** List of port forwardings to (attempt to) establish when the connection gets created */ + forwardings?: string[]; /** Internal property saying where this config comes from. Undefined if this config is merged or something */ _location?: ConfigLocation; /** Internal property keeping track of where this config comes from (including merges) */ diff --git a/src/manager.ts b/src/manager.ts index fa901bf..a3cc2c9 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -30,7 +30,7 @@ interface TerminalLinkUri extends vscode.TerminalLink { export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider { protected fileSystems: SSHFileSystem[] = []; protected creatingFileSystems: { [name: string]: Promise } = {}; - public readonly connectionManager = new ConnectionManager(); + public readonly connectionManager = new ConnectionManager(this); constructor(public readonly context: vscode.ExtensionContext) { // In a multi-workspace environment, when the non-main folder gets removed, // it might be one of ours, which we should then disconnect if it's diff --git a/src/portForwarding.ts b/src/portForwarding.ts index e7a9545..a4b4400 100644 --- a/src/portForwarding.ts +++ b/src/portForwarding.ts @@ -32,6 +32,40 @@ export interface PortForwardingLocalRemote { export type PortForwarding = PortForwardingDynamic | PortForwardingLocalRemote; +// https://regexr.com/61quq +const PORT_FORWARD_REGEX = /^^(?[a-z]+)\s*?(?:\s+(?:(?[^\s:]+):))?(?\d+)(?:\s+(?:(?[^\s:]+):)?(?\d+))?$/i; +const PORT_FORWARD_TYPES = ['remote', 'local', 'dynamic']; +export function parsePortForwarding(input: string): PortForwarding | undefined { + try { + const match = input.match(PORT_FORWARD_REGEX); + if (!match) throw new Error(`Could not infer PortForwarding from '${input}'`); + let type = match.groups?.type.toLowerCase(); + if (!type) throw new Error(`Could not infer PortForwarding from '${input}'`); + if (type.endsWith('forward')) type = type.substring(0, type.length - 7); + if (type.length === 1) type = PORT_FORWARD_TYPES.find(t => t[0] === type); + if (!type || !PORT_FORWARD_TYPES.includes(type)) + throw new Error(`Could not recognize PortForwarding type '${match.groups!.type}'`); + const { localAddress, localPort, remoteAddress, remotePort } = match.groups as Partial>; + let pf: PortForwarding; + if (type === 'remote' && !remoteAddress && !remotePort) { + pf = { type, remoteAddress: localAddress, remotePort: parseInt(localPort!) }; + } else if (type === 'local' || type === 'remote') { + pf = { + type, + localAddress, localPort: parseInt(localPort!), + remoteAddress, remotePort: remotePort ? parseInt(remotePort) : undefined, + }; + } else { + pf = { type: 'dynamic', address: localAddress, port: parseInt(localPort!) }; + } + validatePortForwarding(pf); + return pf; + } catch (e) { + Logging.error(`Parsing port forwarding '${input}' failed:\n${e.message || e}`, LOGGING_NO_STACKTRACE); + return undefined; + } +} + const formatAddrPortPath = (addr?: string, port?: number): string => { if (port === undefined) return addr || 'N/A'; return `${addr || '*'}:${port}`;