Skip to content
This repository was archived by the owner on Dec 13, 2023. It is now read-only.

Commit 1af0ae6

Browse files
authored
Better logging and reconnects (#3)
* improved logging and disconnect handling (-vvv and -q) * stop checking last ping after a disconnect * version bump * forgot to commit logger * gitignore node_modules * better env variable processing for proxies
1 parent de30bd6 commit 1af0ae6

File tree

7 files changed

+311
-133
lines changed

7 files changed

+311
-133
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

cbt-enterprise-connection-manager.js

Lines changed: 158 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ var tls = require('tls');
66
var url = require('url');
77
var Tunnel = require('./tunnel');
88
var utils = require('./utils');
9+
var util = require('util');
910
var ProxyAgent = require('https-proxy-agent');
1011

12+
var Logger = require('./log');
13+
1114
var RECONNECTING = false;
1215

1316
// simple arg parser
@@ -29,21 +32,56 @@ var argv = require('yargs')
2932
description: false,
3033
hide: true,
3134
default: false
32-
}
35+
},
36+
'verbose': {
37+
alias: 'v',
38+
count: true,
39+
conflicts: 'quiet'
3340

41+
},
42+
'quiet': {
43+
alias: 'q',
44+
boolean: true,
45+
conflicts: 'verbose'
46+
}
3447
})
3548
.argv;
3649

50+
// parse logging args and create logger
51+
let initialLogLevel;
52+
if (argv.quiet){
53+
// no logs if quiet option specified
54+
initialLogLevel = 'OFF';
55+
} else {
56+
// otherwise, log level based on count of verbose flags
57+
switch (argv.verbose){
58+
case 0:
59+
initialLogLevel = 'WARN';
60+
break;
61+
case 1:
62+
initialLogLevel = 'INFO';
63+
break;
64+
case 2:
65+
initialLogLevel = 'DEBUG';
66+
break;
67+
case 3:
68+
default:
69+
initialLogLevel = 'TRACE';
70+
break;
71+
}
72+
}
73+
const log = new Logger(initialLogLevel);
74+
// bind to global
75+
global.log = log
3776

38-
39-
var httpProxy = process.env.http_proxy || process.env.HTTP_PROXY || null
40-
var httpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY || null
41-
77+
// apply acceptAllCerts arg by setting env variable for NodeJS
4278
if (argv.acceptAllCerts){
43-
console.log('setting reject_unauthorized = 0')
4479
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
4580
}
4681

82+
// parse env variables for proxy options
83+
let proxyUrl = utils.getProxyFromEnv();
84+
4785
var tunnels = [];
4886

4987
function getApiUrl(env){
@@ -61,102 +99,126 @@ function getApiUrl(env){
6199
}
62100
}
63101

102+
function createProxyAgent(proxyUrl){
103+
proxyUrl = new url.URL(proxyUrl)
104+
let proxyAgentOpts ={
105+
host:proxyUrl.hostname,
106+
port: proxyUrl.port,
107+
auth: proxyUrl.username ? `${proxyUrl.username}:${proxyUrl.password}` : ``,
108+
procotol: proxyUrl.protocol
109+
}
110+
log.debug('ProxyAgent options: ' + util.inspect(proxyAgentOpts))
111+
let proxyAgent = new ProxyAgent(proxyAgentOpts)
112+
}
113+
64114
var socket
65115

66116
function cbtConnect() {
67117

68-
// var socket = socketIo("http://localhost:3000/socket", { path: "/socket"} );
69-
// var socket = socketIo.connect("http://localhost:3000/socket/socket");
70-
let proxy = httpProxy || httpsProxy
118+
let socketUrl = getApiUrl(argv.env)
71119

72-
if(proxy){
73-
console.log('going to setup proxy agent')
74-
if (proxy.indexOf('http') !== 0){
75-
proxy = 'http://' + proxy
76-
}
77-
let proxyURL = new url.URL(proxy)
78-
let proxyAgentOpts ={
79-
host:proxyURL.hostname,
80-
port: proxyURL.port,
81-
auth: proxyURL.username ? `${proxyURL.username}:${proxyURL.password}` : ``,
82-
secureProxy:true
83-
}
84-
let proxyAgent = new ProxyAgent(proxyAgentOpts)
85-
socket = new WebSocket(getApiUrl(argv.env), {agent: proxyAgent});
120+
// create websocket connection (with proxy if specified)
121+
if(proxyUrl){
122+
let proxyAgent = createProxyAgent(proxyUrl)
123+
socket = new WebSocket(socketUrl, {agent: proxyAgent});
86124
} else {
87-
socket = new WebSocket(getApiUrl(argv.env));
125+
socket = new WebSocket(socketUrl);
88126
}
89127

90-
91-
92128
socket.on('error', function(err){
93129
if (RECONNECTING){
94-
// console.log('reconnecting...')
130+
log.debug('Socket error while reconnecting');
131+
log.trace('Socket error: ' + (err || err.stack));
95132
} else {
96-
console.log("Socket error!!");
97-
console.dir(err);
133+
log.error('Socket error: ' + (err || err.stack));
98134
let apiUrl = new url.URL(getApiUrl(argv.env));
99135
var tlsSocket = tls.connect({host: apiUrl.hostname, port:443, rejectUnauthorized: false}, () => {
100-
console.log('TLS connect successful, getting certificates from peer')
101-
console.log(tlsSocket.getPeerCertificate(true))
136+
log.debug('TLS connect successful, getting certificates from peer')
137+
log.debug(tlsSocket.getPeerCertificate(true))
102138
})
103139
}
104140
})
105141

106142
socket.on('close', () => {
107-
console.log('Signalling socket disconnected')
143+
log.error('Signalling socket disconnected. Attempting to reconnect.')
108144
RECONNECTING = true
109145
let reconnectInterval = setInterval( () => {
110146
if (socket.readyState === 0) {
111147
// socket is still connecting... just wait
112148
} else if (socket.readyState === 1){
113149
// socket is reconnected!
150+
log.info('Socket reconnect successful!')
114151
RECONNECTING = false
115152
clearInterval(reconnectInterval)
116153
} else {
117-
console.log('Signalling socket reconnecting...')
154+
log.debug('Signalling socket reconnecting...')
118155
cbtConnect()
119156
}
120157
}, 500)
121158
})
122159

123160
socket.once('open', () => {
124-
console.log("connection established! Initiating auth!");
125-
socket.send(JSON.stringify({action: 'authenticate', username: argv.username, authkey: argv.authkey}));
126161

127-
socket.on('message', msg => {
128-
// handle incoming messages through the signaling socket
129-
try {
130-
msg = JSON.parse(msg)
131-
} catch (parseErr) {
132-
console.error('could not parse inbound message: ' + msg)
133-
return
162+
// setup handler for if server stops pinging
163+
socket.lastPing = new Date();
164+
let serverKeepalive = setInterval(() => {
165+
let now = new Date();
166+
let timeSincePing = (now - socket.lastPing) / 1000;
167+
if (timeSincePing > 15){
168+
log.error(`No ping from CBT in ${timeSincePing} seconds. Closing socket to trigger reconnect`)
169+
clearInterval(serverKeepalive)
170+
socket.terminate()
134171
}
172+
}, 500)
135173

136-
// after parsing successful, each message should come in with an "action"
137-
switch(msg.action){
138-
case 'authenticated':
139-
console.log("Authentication successful! Waiting for a request to open a tunnel...");
140-
break
141-
case 'start':
142-
if (argv.env == 'test' || argv.env == 'local'){
143-
msg.options.test = 'test';
144-
}
145-
var t = new Tunnel(msg.user.username, msg.user.authkey, msg.options)
146-
tunnels.push(t)
147-
t.start()
148-
break
149-
case 'keepalive_check':
150-
socket.send(JSON.stringify({action:'keepalive_ack'}))
151-
break
152-
case 'unauthorized':
153-
console.error("authentication failed! " + msg.message)
154-
process.exit(1)
155-
break
156-
case 'bye':
157-
console.log("got bye");
158-
socket.close();
159-
}
174+
log.info("Socket connection established! Authenticating...");
175+
let payload = {action: 'authenticate', username: argv.username, authkey: argv.authkey};
176+
177+
// clone payload to log it without logging creds
178+
let loggablePayload = JSON.parse(JSON.stringify(payload));
179+
loggablePayload.authkey = '********';
180+
log.trace("ECM => CBT: " + JSON.stringify(loggablePayload));
181+
182+
socket.send(JSON.stringify(payload));
183+
184+
socket.on('message', msg => {
185+
// handle incoming messages through the signaling socket
186+
try {
187+
msg = JSON.parse(msg);
188+
} catch (parseErr) {
189+
log.warn('Could not parse message from CBT. Offending message:' + msg);
190+
log.debug('Parse error: ' + (parseErr.stack || parseErr));
191+
return;
192+
}
193+
194+
// after parsing successful, each message should come in with an "action"
195+
switch(msg.action){
196+
case 'authenticated':
197+
log.trace("ECM <= CBT: " + util.inspect(msg));
198+
log.info("Authentication successful! Waiting for a request to open a tunnel.");
199+
break;
200+
case 'start':
201+
log.trace("ECM <= CBT: " + util.inspect(msg));
202+
if (argv.env == 'test' || argv.env == 'local'){
203+
msg.options.test = 'test';
204+
}
205+
var t = new Tunnel(msg.user.username, msg.user.authkey, msg.options);
206+
tunnels.push(t);
207+
t.start();
208+
break;
209+
case 'keepalive_check':
210+
socket.lastPing = new Date();
211+
socket.send(JSON.stringify({action:'keepalive_ack'}));
212+
break;
213+
case 'unauthorized':
214+
log.trace("ECM <= CBT: " + util.inspect(msg));
215+
log.error("Authentication failed! " + msg.message);
216+
process.exit(1);
217+
break
218+
case 'bye':
219+
log.info("CBT requested this socket close. Closing socket...");
220+
socket.close();
221+
}
160222
})
161223
})
162224
}
@@ -166,16 +228,41 @@ function cbtConnect() {
166228
// ( or if we can't reach cbt to check... )
167229
utils.checkVersion( argv.env, () => {
168230
cbtConnect()
231+
232+
233+
// attempt graceful shutdown on sigint
169234
process.on('SIGINT', () => {
170-
console.log('\nECM is shutting down');
235+
log.info('ECM is shutting down');
236+
171237
if (tunnels.length === 0){
238+
log.trace('No tunnels running, quitting now')
172239
process.exit(0)
173-
}
174-
tunnels.map( (tunnel, tunnelIndex) => {
175-
tunnel.stop( () => {
240+
} else {
241+
242+
log.trace(`Currently ${tunnels.length} tunnels running. Going to shut them down before quitting.`)
243+
tunnels.map( (tunnel, tunnelIndex) => {
244+
log.trace(`Stopping tunnel ${tunnelIndex}`)
245+
tunnel.stop( () => { })
176246
})
177-
})
178247

248+
// quit when all tunnels are stopped
249+
setInterval(() => {
250+
251+
// get status of all tunnels
252+
let tunnelStatus = tunnels.map( tunnel => {
253+
return tunnel.status()
254+
})
255+
256+
// quit if all tunnels are stopped (status() returns false)
257+
if (tunnelStatus.every( tunnel => !tunnel )){
258+
log.info('All tunnels stopped')
259+
process.exit(0)
260+
}
261+
262+
}, 250)
263+
}
264+
265+
// exit after 7 seconds if there are stubborn tunnels
179266
setTimeout(() => {process.exit(0)}, 7000);
180267
})
181268
})

log.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
function timestamp(){
2+
return new Date().toISOString()
3+
}
4+
5+
class Logger{
6+
7+
constructor(currentLevel){
8+
9+
this.levels = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF']
10+
11+
this.changeLevel(currentLevel)
12+
13+
14+
}
15+
16+
changeLevel(newLevel){
17+
18+
if (typeof newLevel === 'undefined'){
19+
// default to INFO level
20+
newLevel = "INFO"
21+
}
22+
23+
if (typeof newLevel === 'string'){
24+
newLevel = this.levels.indexOf(newLevel.toUpperCase())
25+
}
26+
27+
// make sure our log level is valid
28+
if (newLevel < 0) {
29+
// level not found -- use most verbose
30+
newLevel = 0
31+
} else if (newLevel > this.levels.length - 1) {
32+
// level beyond ERROR specified -- use least verbose
33+
newLevel = this.levels.length - 1
34+
}
35+
36+
this.currentLevel = newLevel
37+
}
38+
39+
_log(level, logLinesIterable){
40+
// the pad is to make the log entries line up for all log levels
41+
let logstr = `${timestamp()} [${level.padStart(5)}] - ${logLinesIterable.join(' ')}`
42+
if (this.levels.indexOf(level) >= this.currentLevel){
43+
console.log(logstr)
44+
}
45+
}
46+
47+
trace(...logLinesIterable){
48+
this._log('TRACE', logLinesIterable)
49+
}
50+
51+
debug(...logLinesIterable){
52+
this._log('DEBUG', logLinesIterable)
53+
}
54+
55+
info(...logLinesIterable){
56+
this._log('INFO', logLinesIterable)
57+
}
58+
59+
warn(...logLinesIterable){
60+
this._log('WARN', logLinesIterable)
61+
}
62+
63+
error(...logLinesIterable){
64+
this._log('ERROR', logLinesIterable)
65+
}
66+
67+
}
68+
69+
module.exports = Logger

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cbt-enterprise-connection-manager",
3-
"version": "2.2.4",
3+
"version": "2.3.0",
44
"description": "A small NodeJS utility to manage local connections",
55
"main": "cbt-enterprise-connection-manager.js",
66
"scripts": {

0 commit comments

Comments
 (0)