Open nick22985 opened 1 year ago
Also with the new update it is possible for the ssConnection.isBroken
to be undefined and causes the package to fall over
if (sshConnection.isBroken) {
^
TypeError: Cannot read properties of undefined (reading 'isBroken')
Hi there, you did some impressive job here :-)
I will do some additional testing on that topic, and I am planning to release a pre-release version , that could make your life much easier.
Thanks for the effort I will come back to you !
Also with the new update it is possible for the
ssConnection.isBroken
to be undefined and causes the package to fall overif (sshConnection.isBroken) { ^ TypeError: Cannot read properties of undefined (reading 'isBroken')
I had this error multiple times
I ended up rewriting this to suit my needs. I did not do all the auto connection close things but this may help with solving the issues that I was facing here.
import { Client, type ConnectConfig } from 'ssh2';
import HandlerClient from './handlers/client.js';
import net, { type ListenOptions, type Server } from 'net';
import { type Debugger } from 'debug';
export interface ITunnelOptions {
autoClose: boolean;
reconnectOnError: boolean;
}
export interface IForwardOptions {
srcAddr: string;
srcPort: number;
dstAddr: string;
dstPort: number;
}
export default class sshTunnel {
discordClient: HandlerClient;
debug: Debugger;
tunnelOptions: ITunnelOptions;
listenOptions: ListenOptions;
connectConfig: ConnectConfig;
forwardOptions: IForwardOptions;
// Client, Server
server: Server | undefined;
client: Client | undefined;
clientReconnectInterval: NodeJS.Timeout | undefined;
constructor(options: { discordClient: HandlerClient; tunnelOptions: ITunnelOptions; listenOptions: ListenOptions; connectConfig: ConnectConfig; forwardOptions: IForwardOptions }) {
this.discordClient = options.discordClient;
this.tunnelOptions = Object.assign({ autoClose: false, reconnectOnError: false }, options.tunnelOptions || {});
this.listenOptions = options.listenOptions;
this.connectConfig = Object.assign({ port: 22, username: 'root' }, options.connectConfig);
this.forwardOptions = Object.assign({ dstAddr: '0.0.0.0' }, options.forwardOptions);
this.debug = this.discordClient.$debug.extend('sshTunnel');
this.debug('Starting SSH Tunnel');
}
createServer() {
const $debug = this.debug.extend('createServer');
$debug('Creating Server');
return Promise.resolve()
.then(() => net.createServer())
.then((server) => {
return new Promise((resolve, reject) => {
let errorHandler = (err) => {
$debug('Error', err);
reject(err);
};
server.on('error', errorHandler);
process.on('uncaughtException', errorHandler);
server.on('close', () => {
$debug('Server Close');
if (this.tunnelOptions.reconnectOnError) {
this.createServer().then(() => this.serverEventListeners());
}
});
server.on('drop', () => {
$debug('Server Drop');
});
server.listen(this.listenOptions);
server.on('listening', () => {
process.removeListener('uncaughtException', errorHandler);
$debug('Server Listening');
this.server = server;
resolve(server);
});
});
});
}
createSSHClient() {
const $debug = this.debug.extend('createSSHClient');
$debug('Creating SSH Client');
// make sure client is closed
if (this.client) {
$debug('Client has old connection and is trying to reconnect killing old client');
this.client.end();
this.client = undefined;
}
return new Promise((resolve, reject) => {
let conn: Client = new Client();
conn.on('ready', () => {
this.client = conn;
resolve(conn);
});
conn.on('error', reject);
conn.on('close', () => {
$debug('Client Close');
if (this.tunnelOptions.reconnectOnError) {
setTimeout(() => {
return this.createSSHClient()
.then(() => this.clientEventListeners())
.then(() => $debug('reconnected to client'))
.catch(() => {
$debug.extend('error')('failed to reconnect to client');
});
}, 10000);
}
});
conn.on('end', () => {
$debug('Client End');
});
Promise.resolve()
.then(() => conn.connect(this.connectConfig))
.catch((err: Error) => {
$debug.extend('error')('failed to connect', err);
throw err;
});
}).catch((err: Error) => {
$debug.extend('error')('failed to createSSHClient', err);
throw err;
});
}
serverEventListeners() {
const $debug = this.debug.extend('serverEventListeners');
$debug('Creating Server Event Listeners');
if (!this.server) throw new Error('No Server found');
// reco logic
if (this.tunnelOptions.reconnectOnError)
this.server.on('error', (err) => {
$debug.extend('error')('Server Error', err);
return Promise.resolve().then(() => this.createServer().then(() => this.serverEventListeners()));
});
this.server.on('connection', (connection) => {
$debug('Server Connection', connection.address());
if (!this.client) {
$debug('No Client Connection');
return connection.end(); // Kick the connection trying to connect to the server
}
connection.on('error', (err) => {
$debug('Connection Closed error', err);
});
// This is the mongo Connection itself
return this.client.forwardOut(this.forwardOptions.srcAddr, this.forwardOptions.srcPort, this.forwardOptions.dstAddr, this.forwardOptions.dstPort, (err, stream) => {
if (err) {
$debug.extend('error')('Server Connection Error', connection.address());
$debug('server con err', err);
setTimeout(() => {
return connection.end(); // end user connection to server
}, 10000);
} else
connection
.pipe(stream)
.pipe(connection)
.on('close', (test) => {
$debug('Server Connection Close', test);
})
.on('error', (err: Error) => {
$debug('Connection closed from server (Usually this is from the client not closing the connection) (usually ignore this)', err);
});
});
});
this.server.on('close', () => {
$debug('Server Close');
if (this.client) this.client.end();
});
}
clientEventListeners() {
const $debug = this.debug.extend('clientEventListeners');
$debug('Creating Client Event Listeners');
if (!this.client) throw new Error('No Client found');
this.client.on('ready', () => {
$debug('Client Ready');
});
this.client.on('close', () => {
$debug('Client Close');
// if (this.server) this.server.close();
});
}
createTunnel() {
this.debug('Creating SSH Tunnel');
return Promise.resolve()
.then(() => this.createSSHClient()) // Create SSH Client
.then(() => this.createServer()) // Create Server
.then(() => this.serverEventListeners()) // server event listners
.then(() => this.debug('SSH Tunnel Created'));
}
}
I experience the same issue in my tests (and possibly in the app I'm working on):
it('should throw an error when the destination url is incorrect', async () => {
const privateKey = await readFile(path.resolve(__dirname, 'ssh-config', 'id_rsa'));
await expect(
buildMongooseInstance({
uri: 'mongodb://username:secret@invalid_host:27017/movies?authSource=admin',
connection: {
ssh: {
host: '127.0.0.1',
port: 2224,
username: 'forest',
privateKey,
},
socketTimeoutMS: 10,
connectTimeoutMS: 10,
serverSelectionTimeoutMS: 10,
},
}),
).rejects.toThrow(
new ConnectionError(
'mongodb://forest:***@invalid_host:27017/movies?authSource=admin',
'Server selection timed out after 10 ms',
),
);
});
it('should pass', async () => {
await new Promise(resolve => {
setTimeout(resolve, 1000);
});
expect(true).toBe(true);
});
The first test checks that the SSH error is correctly handled by our code when the URL is incorrect.
BUT when running this test suite, the second test actually fails!
I think this is caused by this line: https://github.com/agebrock/tunnel-ssh/blob/master/index.js#L123
sshConnection.forwardOut(
forwardOptionsLocal.srcAddr,
forwardOptionsLocal.srcPort,
forwardOptionsLocal.dstAddr,
forwardOptionsLocal.dstPort, (err, stream) => {
if (err) {
if (server) {
server.close()
}
throw err;
} else {
clientConnection.pipe(stream).pipe(clientConnection);
}
});
We should not throw the error in this callback, but instead send it back to sshConnection
Hey, I am trying to handle all errors to stop this from crashing my application if the server goes down. Instead it will gracefully handle them and reconnect.
Below is what I currently have but I am facing a issue with if the server goes down on the server it will crash my application. I have purposly stopped mongo on the server that it is connecting to simulate this.