@microsoft/dev-tunnels-ssh-tcp
Version:
SSH TCP extensions library for Dev Tunnels
116 lines • 6.67 kB
JavaScript
"use strict";
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalPortForwarder = void 0;
const dev_tunnels_ssh_1 = require("@microsoft/dev-tunnels-ssh");
const streamForwarder_1 = require("./streamForwarder");
/**
* Listens on a local port and forwards incoming connections as SSH channels.
*/
class LocalPortForwarder extends dev_tunnels_ssh_1.SshService {
/* @internal */
constructor(pfs, session, channelType, localIPAddress, localPort, remoteHost, remotePort) {
super(session);
this.pfs = pfs;
this.channelType = channelType;
this.localIPAddress = localIPAddress;
this.port = localPort;
this.remoteHost = remoteHost;
// The remote port defaults to the same as the local port, if the remote port
// was unspecified and a specific (nonzero) local port was specified. Whether
// or not a specific local port was specified, the local port may be changed
// by the TCP listener factory. In that case the remote port does not change.
this.remotePort = remotePort !== null && remotePort !== void 0 ? remotePort : (localPort !== 0 ? localPort : undefined);
}
/**
* Local port that the forwarder is listening on.
*/
get localPort() {
return this.port;
}
/* @internal */
async startForwarding(cancellation) {
var _a;
let listenAddress = this.localIPAddress;
try {
this.tcpListener = await this.pfs.tcpListenerFactory.createTcpListener(this.remotePort, listenAddress, this.port, true);
const serverAddress = this.tcpListener.address();
if (!(serverAddress.port > 0)) {
this.tcpListener.close();
throw new Error('Could not get server port.');
}
this.port = serverAddress.port;
// The SSH protocol specifies that "localhost" or "" (any) should be dual-mode.
// So 2 TCP listener instances are required in those cases.
if (this.localIPAddress === '127.0.0.1' || this.localIPAddress === '0.0.0.0') {
// Call the factory again to create another listener, but this time with the
// corresponding IPv6 local address, and not allowing a port change.
listenAddress = this.localIPAddress === '0.0.0.0' ? '::' : '::1';
try {
this.tcpListener2 = await this.pfs.tcpListenerFactory.createTcpListener(this.remotePort, listenAddress, this.port, false);
}
catch (e) {
if (!(e instanceof Error) || e.code !== 'EADDRNOTAVAIL') {
throw e;
}
// The OS may not support IPv6 or there may be no IPv6 network interfaces.
this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, dev_tunnels_ssh_1.SshTraceEventIds.portForwardServerListenFailed, 'PortForwardingService failed to listen on ' +
`${listenAddress}:{this.port}: ${e.message}`, e);
// Do not rethrow, just skip IPv6 in this case.
}
}
}
catch (e) {
if (!(e instanceof Error))
throw e;
this.trace(dev_tunnels_ssh_1.TraceLevel.Error, dev_tunnels_ssh_1.SshTraceEventIds.portForwardServerListenFailed, `PortForwardingService failed to listen on ${listenAddress}:${this.port}: ${e.message}`, e);
throw e;
}
this.tcpListener.on('connection', this.acceptConnection.bind(this));
(_a = this.tcpListener2) === null || _a === void 0 ? void 0 : _a.on('connection', this.acceptConnection.bind(this));
this.trace(dev_tunnels_ssh_1.TraceLevel.Info, dev_tunnels_ssh_1.SshTraceEventIds.portForwardServerListening, `PortForwardingService listening on ${this.localIPAddress}:${this.port}.`);
if (this.tcpListener2) {
this.trace(dev_tunnels_ssh_1.TraceLevel.Info, dev_tunnels_ssh_1.SshTraceEventIds.portForwardServerListening, `PortForwardingService also listening on ${listenAddress}:${this.port}.`);
}
}
async acceptConnection(socket) {
var _a, _b, _c, _d, _e;
this.trace(dev_tunnels_ssh_1.TraceLevel.Info, dev_tunnels_ssh_1.SshTraceEventIds.portForwardConnectionAccepted, 'PortForwardingService accepted connection from: ' +
`${socket.remoteAddress} on port ${this.port}`);
// TODO: Set socket options?
let channel;
try {
channel = await this.pfs.openChannel(this.session, this.channelType, (_a = socket.remoteAddress) !== null && _a !== void 0 ? _a : null, (_b = socket.remotePort) !== null && _b !== void 0 ? _b : null, (_c = this.remoteHost) !== null && _c !== void 0 ? _c : this.localIPAddress, (_d = this.remotePort) !== null && _d !== void 0 ? _d : this.localPort);
}
catch (e) {
if (!(e instanceof Error))
throw e;
// TODO: Destroy the socket in a way that causes a connection reset:
// https://github.com/nodejs/node/issues/27428
socket.destroy();
// Don't re-throw. This is an async event handler so the caller isn't awaiting.
// The error details have already been traced.
return;
}
// The event handler may return a transformed stream.
const forwardedStream = await this.pfs.forwardedPortConnecting((_e = this.remotePort) !== null && _e !== void 0 ? _e : this.localPort, false, new dev_tunnels_ssh_1.SshStream(channel));
if (!forwardedStream) {
// The event handler rejected the connection.
return;
}
const forwarder = new streamForwarder_1.StreamForwarder(socket, forwardedStream, channel.session.trace);
this.pfs.streamForwarders.push(forwarder);
}
dispose() {
var _a, _b;
// Note stopping the listener does not disconnect any already-accepted sockets.
(_a = this.tcpListener) === null || _a === void 0 ? void 0 : _a.close();
(_b = this.tcpListener2) === null || _b === void 0 ? void 0 : _b.close();
this.trace(dev_tunnels_ssh_1.TraceLevel.Info, dev_tunnels_ssh_1.SshTraceEventIds.portForwardServerListening, `PortForwardingService stopped listening on ${this.localIPAddress}:${this.port}.`);
super.dispose();
}
}
exports.LocalPortForwarder = LocalPortForwarder;
//# sourceMappingURL=localPortForwarder.js.map