parent
03241aa420
commit
c707d56895
@ -0,0 +1,149 @@
|
||||
import * as net from 'net';
|
||||
import type { ClientChannel, TcpConnectionDetails, UnixConnectionDetails } from 'ssh2';
|
||||
import type { Duplex } from 'stream';
|
||||
import type { Connection } from "./connection";
|
||||
import type { Manager } from './manager';
|
||||
import { toPromise } from './toPromise';
|
||||
|
||||
/** Represents a dynamic port forwarding (DynamicForward) */
|
||||
export interface PortForwardingDynamic {
|
||||
type: 'dynamic';
|
||||
port: number;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
/** Represents a local (LocalForward) or remote (RemoteForward) port forwarding */
|
||||
export interface PortForwardingLocalRemote {
|
||||
type: 'local' | 'remote';
|
||||
/** Represents the local port to use, or undefined for a Unix socket */
|
||||
localPort?: number;
|
||||
/** Represents the (optional) local bind address, or the Unix socket path if `localPort` is undefined */
|
||||
localAddress?: string;
|
||||
/** Represents the remote port to use, or undefined for a Unix socket */
|
||||
remotePort?: number;
|
||||
/** Represents the (optional) remote bind address, or the Unix socket path if `remotePort` is undefined */
|
||||
remoteAddress?: string;
|
||||
}
|
||||
|
||||
export type PortForwarding = PortForwardingDynamic | PortForwardingLocalRemote;
|
||||
|
||||
type Disconnect = () => void;
|
||||
export type ActivePortForwarding = [data: PortForwarding, connection: Connection, disconnect: Disconnect];
|
||||
export function isActivePortForwarding(apf: any): apf is ActivePortForwarding {
|
||||
return Array.isArray(apf) && apf.length === 3 && 'type' in apf[0];
|
||||
}
|
||||
|
||||
function validateLocalRemoteForwarding(forwarding: PortForwardingLocalRemote) {
|
||||
if (forwarding.localPort === undefined && !forwarding.localAddress) {
|
||||
throw new Error(`Missing both 'localPort' and 'localAddress' fields for a ${forwarding.type} port forwarding`);
|
||||
}
|
||||
if (forwarding.remotePort === undefined && !forwarding.remoteAddress) {
|
||||
throw new Error(`Missing both 'remotePort' and 'remoteAddress' fields for a ${forwarding.type} port forwarding`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createLocalForwarding(connection: Connection, forwarding: PortForwardingLocalRemote): Promise<ActivePortForwarding> {
|
||||
validateLocalRemoteForwarding(forwarding);
|
||||
if (forwarding.localAddress === '*') forwarding = { ...forwarding, localAddress: '::' };
|
||||
const { client } = connection;
|
||||
const sockets = new Set<net.Socket>();
|
||||
const server = net.createServer(socket => {
|
||||
sockets.add(socket);
|
||||
socket.on('close', () => sockets.delete(socket));
|
||||
if (forwarding.remotePort === undefined) {
|
||||
client.openssh_forwardOutStreamLocal(forwarding.remoteAddress!, (err, channel) => {
|
||||
if (err) return socket.destroy(err);
|
||||
socket.pipe(channel).pipe(socket);
|
||||
});
|
||||
} else {
|
||||
client.forwardOut(socket.localAddress, socket.localPort, forwarding.remoteAddress!, forwarding.remotePort!, (err, channel) => {
|
||||
if (err) return socket.destroy(err);
|
||||
socket.pipe(channel).pipe(socket);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (forwarding.localPort === undefined) {
|
||||
await toPromise(cb => server.listen(forwarding.localAddress!, cb));
|
||||
} else {
|
||||
await toPromise(cb => server.listen(forwarding.localPort, forwarding.localAddress, cb));
|
||||
if (forwarding.localPort === 0) {
|
||||
forwarding = { ...forwarding, localPort: (server.address() as net.AddressInfo).port };
|
||||
}
|
||||
}
|
||||
return [forwarding, connection, () => server.close(() => sockets.forEach(s => s.destroy()))];
|
||||
}
|
||||
|
||||
async function createRemoteForwarding(connection: Connection, forwarding: PortForwardingLocalRemote): Promise<ActivePortForwarding> {
|
||||
validateLocalRemoteForwarding(forwarding);
|
||||
const channels = new Set<Duplex>();
|
||||
const onSocket = (channel: Duplex) => {
|
||||
channels.add(channel);
|
||||
channel.on('close', () => channels.delete(socket));
|
||||
let socket: net.Socket;
|
||||
if (forwarding.localPort === undefined) {
|
||||
socket = net.createConnection(forwarding.localAddress!);
|
||||
} else {
|
||||
socket = net.createConnection(forwarding.localPort!, forwarding.localAddress!);
|
||||
}
|
||||
socket.on('connect', () => socket.pipe(channel).pipe(socket));
|
||||
};
|
||||
let unlisten: () => void;
|
||||
if (forwarding.remotePort === undefined) {
|
||||
await toPromise(cb => connection.client.openssh_forwardInStreamLocal(forwarding.remoteAddress!, cb));
|
||||
const listener = (details: UnixConnectionDetails, accept: () => ClientChannel) => {
|
||||
if (details.socketPath !== forwarding.remoteAddress) return;
|
||||
onSocket(accept());
|
||||
};
|
||||
connection.client.on('unix connection', listener);
|
||||
unlisten = () => connection.client.off('unix connection', listener);
|
||||
} else {
|
||||
const remotePort = await toPromise<number>(cb => connection.client.forwardIn(forwarding.remoteAddress!, forwarding.remotePort!, cb));
|
||||
forwarding = { ...forwarding, remotePort };
|
||||
const listener = (details: TcpConnectionDetails, accept: () => ClientChannel) => {
|
||||
if (details.destPort !== forwarding.remotePort) return;
|
||||
if (details.destIP !== forwarding.remoteAddress) return;
|
||||
onSocket(accept());
|
||||
};
|
||||
connection.client.on('tcp connection', listener);
|
||||
unlisten = () => connection.client.off('tcp connection', listener);
|
||||
}
|
||||
return [forwarding, connection, () => {
|
||||
unlisten();
|
||||
if (forwarding.remotePort === undefined) {
|
||||
connection.client.openssh_unforwardInStreamLocal(forwarding.remoteAddress!);
|
||||
} else {
|
||||
connection.client.unforwardIn(forwarding.remoteAddress!, forwarding.remotePort!);
|
||||
}
|
||||
channels.forEach(s => s.destroy());
|
||||
}];
|
||||
}
|
||||
|
||||
function createDynamicForwarding(connection: Connection, forwarding: PortForwardingDynamic): Promise<ActivePortForwarding> {
|
||||
// TODO
|
||||
throw new Error('Dynamic port forwarding is not supported yet');
|
||||
}
|
||||
|
||||
function getFactory(type: PortForwarding['type']): (conn: Connection, pf: PortForwarding) => Promise<ActivePortForwarding> {
|
||||
switch (type) {
|
||||
case 'local': return createLocalForwarding;
|
||||
case 'remote': return createRemoteForwarding;
|
||||
case 'dynamic': return createDynamicForwarding;
|
||||
default:
|
||||
throw new Error(`Forwarding type '${type}' is not recognized`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addForwarding(manager: Manager, connection: Connection, forwarding: PortForwarding): Promise<void> {
|
||||
const factory = getFactory(forwarding.type);
|
||||
manager.connectionManager.update(connection, c => c.pendingUserCount++);
|
||||
try {
|
||||
const active = await factory(connection, forwarding);
|
||||
manager.connectionManager.update(connection, c => {
|
||||
c.forwardings.push(active);
|
||||
c.pendingUserCount--;
|
||||
});
|
||||
} catch (e) {
|
||||
manager.connectionManager.update(connection, c => c.pendingUserCount--);
|
||||
throw e;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
|
||||
import 'ssh2';
|
||||
declare module 'ssh2' {
|
||||
export interface UnixConnectionDetails {
|
||||
socketPath: string;
|
||||
}
|
||||
export interface Client {
|
||||
// Missing from @types/ssh2 (but with it being so behind and ssh2 being rewritten, useless to add a PR to fix this)
|
||||
/** Emitted when an incoming forwarded unix connection is being requested. */
|
||||
on(event: "unix connection", listener: (details: UnixConnectionDetails, accept: () => ClientChannel, reject: () => void) => void): this;
|
||||
}
|
||||
}
|
Loading…
Reference in new issue