Improve parsePortForwarding

feature/forwarding
Kelvin Schoofs 3 years ago
parent b7a68da3ec
commit f03e1754ba

@ -212,7 +212,7 @@ export class ConnectionManager {
});
// Setup initial port forwardings
setImmediate(async () => {
const forwards = (actualConfig.forwardings || []).map(parsePortForwarding);
const forwards = (actualConfig.forwardings || []).map(f => parsePortForwarding(f, 'report'));
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;

@ -7,7 +7,7 @@ import type { Connection } from "./connection";
import type { FileSystemConfig } from './fileSystemConfig';
import { Logging, LOGGING_NO_STACKTRACE } from './logging';
import type { Manager } from './manager';
import { promptQuickPick } from './ui-utils';
import { FormattedItem, promptQuickPick } from './ui-utils';
import { toPromise } from './utils';
/** Represents a dynamic port forwarding (DynamicForward) */
@ -32,10 +32,15 @@ export interface PortForwardingLocalRemote {
export type PortForwarding = PortForwardingDynamic | PortForwardingLocalRemote;
function tryParseInt(input?: string): number | undefined {
const parsed = input ? parseInt(input) : undefined;
return Number.isNaN(parsed) ? undefined : parsed;
}
// https://regexr.com/61quq
const PORT_FORWARD_REGEX = /^^(?<type>[a-z]+)\s*?(?:\s+(?:(?<localAddress>[^\s:]+):))?(?<localPort>\d+)(?:\s+(?:(?<remoteAddress>[^\s:]+):)?(?<remotePort>\d+))?$/i;
const PORT_FORWARD_REGEX = /^(?<type>\w+)\s*?(?:(?:\s+(?:(?<localAddress>[^\s:]+|[\da-zA-Z:]+|\[[\da-zA-Z:]+\]):))?(?<localPort>\d+)|(?<localPath>[/\\][/\\.\w?\-]+))(?:\s+(?:(?<remoteAddress>[^\s:]+|[\da-zA-Z:]+|\[[\da-zA-Z:]+\]):)?(?<remotePort>\d+)|\s+(?<remotePath>[/\\][/\\.\w?\-]+))?$/i;
const PORT_FORWARD_TYPES = ['remote', 'local', 'dynamic'];
export function parsePortForwarding(input: string): PortForwarding | undefined {
export function parsePortForwarding(input: string, mode: 'report' | 'throw' | 'ignore'): PortForwarding | undefined {
try {
const match = input.match(PORT_FORWARD_REGEX);
if (!match) throw new Error(`Could not infer PortForwarding from '${input}'`);
@ -45,46 +50,70 @@ export function parsePortForwarding(input: string): PortForwarding | undefined {
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<Record<string, string>>;
const {
localPath, localAddress = localPath, localPort,
remotePath, remoteAddress = remotePath, remotePort,
} = match.groups as Partial<Record<string, string>>;
let pf: PortForwarding;
if (type === 'remote' && !remoteAddress && !remotePort) {
pf = { type, remoteAddress: localAddress, remotePort: parseInt(localPort!) };
pf = { type, remoteAddress: localAddress, remotePort: tryParseInt(localPort) };
} else if (type === 'local' || type === 'remote') {
pf = {
type,
localAddress, localPort: parseInt(localPort!),
remoteAddress, remotePort: remotePort ? parseInt(remotePort) : undefined,
localAddress, localPort: tryParseInt(localPort),
remoteAddress, remotePort: tryParseInt(remotePort),
};
} else {
pf = { type: 'dynamic', address: localAddress, port: parseInt(localPort!) };
pf = { type: 'dynamic', address: localAddress, port: tryParseInt(localPort)! };
}
validatePortForwarding(pf);
return pf;
} catch (e) {
if (mode === 'ignore') return undefined;
if (mode === 'throw') throw e;
Logging.error(`Parsing port forwarding '${input}' failed:\n${e.message || e}`, LOGGING_NO_STACKTRACE);
return undefined;
}
}
const SINGLE_WORD_PATH_REGEX = /^[/\\.\w?]+$/;
const formatAddrPortPath = (addr?: string, port?: number): string => {
if (port === undefined) return addr || 'N/A';
if (port === undefined) {
if (!addr) return 'N/A';
if (SINGLE_WORD_PATH_REGEX.test(addr)) return addr;
return `'${addr}'`;
}
if (addr) try {
const ip = getIP(addr);
if (ip?.type === 'IPv6') return `[${addr.toString()}]:${port}`;
} catch (e) { }
return `${addr || '*'}:${port}`;
};
export function formatPortForwarding(forwarding: PortForwarding): string {
if (forwarding.type === 'local' || forwarding.type === 'remote') {
const local = (forwarding.localPort !== undefined || forwarding.localAddress)
? formatAddrPortPath(forwarding.localAddress, forwarding.localPort) : 'SOCKSv5';
return `${local}${formatAddrPortPath(forwarding.remoteAddress, forwarding.remotePort)}`;
} else if (forwarding.type === 'dynamic') {
return `${formatAddrPortPath(forwarding.address, forwarding.port)} → SOCKSv5`;
}
// Shouldn't happen but might as well catch it this way
return JSON.stringify(forwarding);
}
export function formatPortForwardingConfig(forwarding: PortForwarding): string {
if (forwarding.type === 'local') {
const { localAddress, localPort, remoteAddress, remotePort } = forwarding;
return `LocalForward(${formatAddrPortPath(localAddress, localPort)}${formatAddrPortPath(remoteAddress, remotePort)})`;
return `LocalForward ${formatAddrPortPath(localAddress, localPort)} ${formatAddrPortPath(remoteAddress, remotePort)}`;
} else if (forwarding.type === 'remote') {
const { localAddress, localPort, remoteAddress, remotePort } = forwarding;
if (!localAddress && localPort === undefined) {
return `RemoteForward(${formatAddrPortPath(remoteAddress, remotePort)})`;
return `RemoteForward ${formatAddrPortPath(remoteAddress, remotePort)}`;
}
return `RemoteForward(${formatAddrPortPath(localAddress, localPort)}${formatAddrPortPath(remoteAddress, remotePort)})`;
return `RemoteForward ${formatAddrPortPath(localAddress, localPort)} ${formatAddrPortPath(remoteAddress, remotePort)}`;
} else if (forwarding.type === 'dynamic') {
return `DynamicForward(${formatAddrPortPath(forwarding.address, forwarding.port)})`;
return `DynamicForward ${formatAddrPortPath(forwarding.address, forwarding.port)}`;
}
// Shouldn't happen but might as well catch it this way
return JSON.stringify(forwarding);
throw new Error(`Unrecognized forwarding type '${forwarding.type}'`);
}
type Disconnect = () => void;
@ -394,42 +423,95 @@ async function promptAddressOrPath(location: 'local' | 'remote', allowWildcard:
}
}
async function promptBindAddress(): Promise<[port: number, address?: string] | undefined> {
const port = await vscode.window.showInputBox({ prompt: 'Port to bind to', validateInput: validatePort, placeHolder: '0-65535' });
if (!port) return undefined; // String so '0' is still truthy
const addr = await vscode.window.showInputBox({ prompt: 'Address to bind to', validateInput: validateHost, value: 'localhost' });
return [parseInt(port), addr];
}
export async function promptPortForwarding(config: FileSystemConfig): Promise<PortForwarding | undefined> {
// TODO: Make use of config with future GatewayPorts fields and such to suggest default values
const types = {
local: 'Local forward',
remote: 'Remote forward',
remoteProxy: 'Remote proxy (client SOCKSv5 ← server)',
dynamic: 'Dynamic forward (client → server SOCKSv5)'
const picker = vscode.window.createQuickPick<FormattedItem>();
picker.title = `Port forwarding to ${config.label || config.name}`;
picker.ignoreFocusOut = true;
const ITEMS: FormattedItem[] = [
{ item: 'local', label: '→ Local forward' },
{ item: 'remote', label: '← Remote forward' },
{ item: 'remoteProxy', label: '$(globe) Remote proxy (client SOCKSv5 ← server)' },
{ item: 'dynamic', label: '$(globe) Dynamic forward (client → server SOCKSv5)' },
{ item: 'examples', label: '$(list-unordered) Show examples' },
];
const formatPF = (forward: PortForwarding, description?: string): FormattedItem => ({
item: forward, alwaysShow: true, description,
label: formatPortForwarding(forward),
detail: formatPortForwardingConfig(forward),
});
const updateItems = () => {
let items: FormattedItem[] = [];
let suggested: FormattedItem[] = [];
if (picker.value === 'examples') {
suggested = [{ item: 'return', label: '$(quick-input-back) Return', alwaysShow: true }];
items = [
formatPF({ type: 'local', localPort: 0, remoteAddress: 'localhost', remotePort: 8080 }, 'Port 0 will pick a free'),
formatPF({ type: 'local', localPort: 8080, remoteAddress: 'localhost', remotePort: 8080 }, 'No address or "*" binds to all interfaces'),
formatPF({ type: 'local', localAddress: '\\\\?\\pipe\\windows\\named\\pipe', remoteAddress: '/tmp/unix/socket' }, 'Supports Unix sockets'),
formatPF({ type: 'remote', remotePort: 8080 }, 'No address or "*" binds to all interfaces'),
formatPF({ type: 'remote', localAddress: 'example.com', localPort: 80, remoteAddress: '0::1', remotePort: 8080 }, 'Supports hostnames and IPv6'),
formatPF({ type: 'remote', remoteAddress: 'localhost', remotePort: 1234 }, 'Bind remotely to proxy through client'),
formatPF({ type: 'dynamic', address: 'localhost', port: 1234 }, 'Bind locally to proxy through server'),
];
} else if (picker.value) {
const type = picker.value.toLowerCase().trimLeft().match(/^[a-zA-Z]*/)![0].replace(/Forward$/, '');
let detail: string;
if (type === 'l' || type === 'local') {
detail = 'Local [localAddress]:localPort remoteAddress:remotePort';
} else if (type === 'r' || type === 'remote') {
detail = 'Remote [localAddress:localPort] [remoteAddress]:remotePort';
} else if (type === 'd' || type === 'dynamic') {
detail = 'Dynamic localAddress:localPort';
} else {
detail = 'Select or type a port forwarding type';
items = [...ITEMS];
}
try {
const forward = parsePortForwarding(picker.value, 'throw')!;
suggested.push(formatPF(forward));
detail = `Current syntax: ${detail}`;
} catch (e) {
const label = (e.message as string).replace(/from '.*'$/, '');
items.unshift({ item: undefined, label, detail, alwaysShow: true });
}
items.push({ item: 'return', label: '$(quick-input-back) Pick type', detail, alwaysShow: true });
} else {
items = ITEMS;
}
// If you set items first, onDidAccept will be triggered (even though it shouldn't)
picker.selectedItems = picker.activeItems = suggested;
picker.items = items.length ? [...items, ...suggested] : suggested;
};
type Type = keyof typeof types;
const type = await promptQuickPick<Type>('Select type of port forwarding', Object.keys(types) as Type[], key => types[key]);
if (!type) return undefined;
if (type === 'local' || type === 'remote') {
const local = await promptAddressOrPath('local', type === 'local');
if (!local) return undefined;
const remote = await promptAddressOrPath('remote', type === 'remote');
if (!remote) return undefined;
const [localPort, localAddress] = local;
const [remotePort, remoteAddress] = remote;
return { type, localAddress, localPort, remoteAddress, remotePort };
} else if (type === 'remoteProxy') {
const remote = await promptAddressOrPath('remote', true);
if (!remote) return undefined;
const [remotePort, remoteAddress] = remote;
return { type: 'remote', remoteAddress, remotePort };
} else if (type === 'dynamic') {
const bind = await promptBindAddress();
if (!bind) return undefined;
const [port, address] = bind;
return { type: 'dynamic', port, address };
updateItems();
picker.onDidChangeValue(updateItems);
return new Promise((resolve) => {
picker.onDidAccept(() => {
if (!picker.selectedItems.length) return;
const [{ item }] = picker.selectedItems;
if (!item) return;
if (item === 'examples') {
picker.value = 'examples';
} else if (item === 'return') {
picker.value = '';
} else if (item === 'local') {
picker.value = 'Local ';
} else if (item === 'remote' || item === 'remoteProxy') {
picker.value = 'Remote ';
} else if (item === 'dynamic') {
picker.value = 'Dynamic ';
} else {
if (picker.value === 'examples') {
// Looking at examples, don't actually accept but copy the value
picker.value = formatPortForwardingConfig(item);
} else {
resolve(item);
picker.hide();
return;
}
return undefined;
}
updateItems();
});
picker.onDidHide(() => resolve(undefined));
picker.show();
});
}

Loading…
Cancel
Save