Add support for command-based proxying (ProxyCommand)

feature/ssh-config
Kelvin Schoofs 4 years ago
parent 7b94fdcac8
commit f387681dd0

@ -238,6 +238,8 @@ export async function createSocket(config: FileSystemConfig): Promise<NodeJS.Rea
return await (await import('./proxy')).socks(config);
case 'http':
return await (await import('./proxy')).http(config);
case 'command':
return await (await import('./proxy')).command(config);
default:
throw new Error(`Unknown proxy method`);
}

@ -5,6 +5,10 @@ export interface ProxyConfig {
host: string;
port: number;
}
export interface ProxyCommand {
type: 'command';
command: string;
};
export type ConfigLocation = number | string;
@ -89,7 +93,7 @@ export interface FileSystemConfig extends ConnectConfig {
/** Whether to parse ssh_config files (listed by the VS Code setting `sshfs.paths.ssh`) for extra parameters, e.g. Port */
sshConfig?: boolean;
/** Optional object defining a proxy to use */
proxy?: ProxyConfig;
proxy?: ProxyConfig | ProxyCommand;
/** Optional path to a private keyfile to authenticate with */
privateKeyPath?: string;
/** Names of other config (or connection strings) to use as hops */

@ -1,6 +1,9 @@
import { spawn } from 'child_process';
import * as dns from 'dns';
import { request } from 'http';
import { Readable, Writable } from 'node:stream';
import { Duplex } from 'stream';
import type { FileSystemConfig } from './fileSystemConfig';
import { Logging } from './logging';
import { toPromise } from './toPromise';
@ -14,9 +17,20 @@ async function resolveHostname(hostname: string): Promise<string> {
function validateConfig(config: FileSystemConfig) {
if (!config.proxy) throw new Error(`Missing field 'config.proxy'`);
if (!config.proxy.host) throw new Error(`Missing field 'config.proxy.host'`);
if (!config.proxy.port) throw new Error(`Missing field 'config.proxy.port'`);
if (!config.proxy.type) throw new Error(`Missing field 'config.proxy.type'`);
switch (config.proxy.type) {
case 'http':
case 'socks4':
case 'socks5':
if (!config.proxy.host) throw new Error(`Missing field 'config.proxy.host'`);
if (!config.proxy.port) throw new Error(`Missing field 'config.proxy.port'`);
break;
case 'command':
if (!config.proxy.command) throw new Error(`Missing field 'config.proxy.command'`);
break;
default:
throw new Error(`Unrecognized proxy type '${config.proxy!.type}'`);
}
}
export async function socks(config: FileSystemConfig): Promise<NodeJS.ReadWriteStream> {
@ -73,3 +87,35 @@ export function http(config: FileSystemConfig): Promise<NodeJS.ReadWriteStream>
}
});
}
class ReadWriteWrapper extends Duplex {
constructor(protected _readable: Readable, protected _writable: Writable) {
super();
_readable.once('finish', () => this.end());
_writable.once('end', () => this.push(null));
_readable.once('error', e => this.emit('error', e));
_writable.once('error', e => this.emit('error', e));
this.once('close', () => _writable.end());
}
_destroy() { this._readable.destroy(); this._writable.destroy(); }
_write(chunk: any, encoding: string, callback: any) {
return this._writable._write(chunk, encoding, callback);
}
_read(size: number) {
const chunk = this._readable.read();
if (chunk) this.push(chunk);
else this._readable.once('readable', () => this._read(size));
}
}
export async function command(config: FileSystemConfig): Promise<NodeJS.ReadWriteStream> {
Logging.info(`Creating ProxyCommand connection for ${config.name}`);
validateConfig(config);
const proxy = config.proxy!;
if (proxy.type !== 'command') throw new Error(`Expected config.proxy.type' to be 'command'`);
Logging.debug('\tcommand: ' + proxy.command);
const proc = spawn(proxy.command, { shell: true, stdio: ['pipe', 'pipe', 'inherit'] });
if (proc.killed) throw new Error(`ProxyCommand process died with exit code ${proc.exitCode}`);
if (!proc.pid) throw new Error(`ProxyCommand process did not spawn, possible exit code: ${proc.exitCode}`);
return new ReadWriteWrapper(proc.stdout, proc.stdin);
}

Loading…
Cancel
Save