Skip to content

Commit

Permalink
Websocket connection overhaul, touch #456.
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulDalek committed Dec 15, 2023
1 parent c7d0df3 commit 3dc1db8
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 70 deletions.
7 changes: 6 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,9 @@ PROXY_SERVER_TIMEOUT =

# WebSocket config
WS_ENABLE = true
WS_URL =
WS_RECONNECT = true
WS_REJECT_UNAUTHORIZED = false
WS_PING_TIMEOUT = 16000
WS_RECONNECT_INTERVAL = 3000
WS_URL =
WS_SECRET =
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ log/
tests/_temp
tmp/
dist/
cert/

.DS_Store
.cache
Expand Down
4 changes: 2 additions & 2 deletions dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.esm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.esm.js.map

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions lib/envConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod';

const envToBoolean = () =>
z.enum(['true', 'false']).transform((v) => v === 'true');

const EnvConfig = z.object({
WS_ENABLE: envToBoolean(),
WS_RECONNECT: envToBoolean(),
WS_REJECT_UNAUTHORIZED: envToBoolean(),
WS_PING_TIMEOUT: z.coerce.number(),
WS_RECONNECT_INTERVAL: z.coerce.number(),
WS_URL: z.string(),
WS_SECRET: z.string()
});

export const envConfig = EnvConfig.parse(process.env);
2 changes: 1 addition & 1 deletion lib/schemas/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export const defaultConfig = {
description: 'The port on which to run the SSL server.'
},
certPath: {
envLink: 'HIGHCHARTS_SSL_CERT_PATH',
envLink: 'HIGHCHARTS_SERVER_SSL_CERT_PATH',
value: '',
type: 'string',
description: 'The path to the SSL certificate/key.'
Expand Down
3 changes: 2 additions & 1 deletion lib/server/routes/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ See LICENSE file in root for details.
*******************************************************************************/

import { v4 as uuid } from 'uuid';
import { WebSocket } from 'ws';

import websocket from '../websocket.js';
import { getAllowCodeExecution, startExport } from '../../chart.js';
Expand Down Expand Up @@ -255,7 +256,7 @@ const exportHandler = (request, response) => {
}

// If the client is found, send data through WebSocket
if (websocketClient) {
if (websocketClient && websocketClient.readyState === WebSocket.OPEN) {
// Already prepared options but before the export process
websocketClient.send(JSON.stringify(options));
}
Expand Down
40 changes: 18 additions & 22 deletions lib/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ import { posix } from 'path';
import bodyParser from 'body-parser';
import cors from 'cors';
import express from 'express';
import multer from 'multer';
import http from 'http';
import https from 'https';
import jwt from 'jsonwebtoken';
import multer from 'multer';

import rateLimit from './rate_limit.js';
import websocket from './websocket.js';
import { envConfig } from '../envConfig.js';
import { log } from '../logger.js';
import { toBoolean, __dirname } from '../utils.js';
import { __dirname } from '../utils.js';

import healthRoute from './routes/health.js';
import exportRoutes from './routes/export.js';
Expand Down Expand Up @@ -84,23 +86,8 @@ export const startServer = async (serverConfig) => {
return false;
}

// // Get the pool
// const pool = getPool();

// // Try to create browser instance before starting the server
// const resource = await pool.acquire();

// // If not found, throw an error
// if (!resource.browser) {
// log(1, `[server] Could not acquire browser instance.`);
// process.exit(1);
// }

// // Release the resource
// pool.release(resource);

// Listen HTTP server
if (!serverConfig.ssl.enable && !serverConfig.ssl.force) {
// Listen HTTP server, if TLS is not forced
if (!serverConfig.ssl.force) {
// Main server instance (HTTP)
const httpServer = http.createServer(app);
// Attach error handlers and listen to the server
Expand Down Expand Up @@ -140,7 +127,7 @@ export const startServer = async (serverConfig) => {

if (key && cert) {
// Main server instance (HTTPS)
const httpsServer = https.createServer(app);
const httpsServer = https.createServer({ key, cert }, app);
// Attach error handlers and listen to the server
attachErrorHandlers(httpsServer);
// Listen
Expand Down Expand Up @@ -172,8 +159,17 @@ export const startServer = async (serverConfig) => {
vswitchRoute(app);

// Set the WebSocket connection if enabled
if (toBoolean(process.env.WS_ENABLE) == true) {
websocket.connect(process.env.WS_URL);
if (envConfig.WS_ENABLE == true) {
websocket.connect(envConfig.WS_URL, {
rejectUnauthorized: envConfig.WS_REJECT_UNAUTHORIZED,
headers: {
// Set an access token that lasts only 5 minutes
auth: jwt.sign({ success: 'success' }, envConfig.WS_SECRET, {
algorithm: 'HS256',
expiresIn: '5m'
})
}
});
}
};

Expand Down
120 changes: 82 additions & 38 deletions lib/server/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,90 @@ See LICENSE file in root for details.
*******************************************************************************/
import WebSocket from 'ws';

import { log } from '../../lib/logger.js';
import { log } from '../logger.js';
import { envConfig } from '../envConfig.js';

// WebSocket client
let webSocket;

// In case of closing or termination of a client connection
let reconnectInterval;

/**
* Connects to WebSocket on a provided url.
*
* @param {string} webSocketUrl - The WebSocket server's URL.
* @param {object} options - Options for WebSocket connection.
*/
function connect(webSocketUrl, options) {
// Try to connect to indicated WebSocket server
webSocket = new WebSocket(webSocketUrl, options);

// Open event
webSocket.on('open', () => {
log(3, `[websocket] Connected to WebSocket server: ${webSocketUrl}`);
clearInterval(reconnectInterval);
});

// Close event where ping timeout is cleared
webSocket.on('close', (code) => {
log(
3,
'[websocket]',
`Disconnected from WebSocket server: ${webSocketUrl} with code: ${code}`
);
clearTimeout(webSocket._pingTimeout);
webSocket = null;
});

// Error event
webSocket.on('error', (error) => {
log(1, `[websocket] WebSocket error occured: ${error.message}`);
});

// Message event
webSocket.on('message', (message) => {
log(3, `[websocket] Data received: ${message}`);
});

// Ping event with the connection health check and termination logic
webSocket.on('ping', () => {
log(3, '[websocket] PING');
clearTimeout(webSocket._pingTimeout);
webSocket._pingTimeout = setTimeout(() => {
// Terminate the client connection
webSocket.terminate();

// Try to reconnect if required
if (envConfig.WS_RECONNECT === true) {
reconnect(webSocketUrl, options);
}
}, envConfig.WS_PING_TIMEOUT);
});
}

/**
* Re-connects to WebSocket on a provided url.
*
* @param {string} webSocketUrl - The WebSocket server's URL.
* @param {object} options - Options for WebSocket connection.
*/
function reconnect(webSocketUrl, options) {
reconnectInterval = setInterval(() => {
if (webSocket === null) {
connect(webSocketUrl, options);
}
}, envConfig.WS_RECONNECT_INTERVAL);
}

/**
* Gets the instance of the WebSocket connection.
*/
function getClient() {
return webSocket;
}

export default {
/**
* Connects to WebSocket on provided url.
*
* @param {string} webSocketUrl - The WebSocket server's url.
*/
connect: (webSocketUrl) => {
// Try to connect to indicated WebSocket
webSocket = new WebSocket(webSocketUrl);

// Open event handler with message
webSocket.on('open', () => {
log(3, `[websocket] Connected to WebSocket server: ${webSocketUrl}`);
});

// Close event handler with message and code
webSocket.on('close', (code) => {
log(
3,
`[websocket] Disconnected from WebSocket server: ${webSocketUrl} with code: ${code}`
);
});

// Error event handler with error message
webSocket.on('error', (error) => {
log(4, `[websocket] WebSocket error occured: ${error.message}`);
});

// Message event handler
webSocket.on('message', (message) => {
log(3, `[websocket] Data received: ${message}`);
});
},

/**
* Gets the instance of the WebSocket connection.
*/
getClient: () => webSocket
connect,
getClient
};
Loading

0 comments on commit 3dc1db8

Please sign in to comment.