HTTPS over WebSockets tunnel - node.js

I'm trying to build a system in which the backend can send request to the client that will behave as a proxy. I'm constrained in using WebSockets due to the current state of the remote server. I've build a basic version of my idea that works with HTTP request sent from a browser, but as soon as I activate HTTPS it apparently fails on the handshake. I'm adding a request ID to organise and imitate the request/response model. I want to achieve an E2E encryption with this proxy chain.
This is my code for the proxy that catches the requests and sends them over the WebSocket:
const net = require('net');
const WebSocket = require('ws');
const crypto = require("crypto");
const wss = new WebSocket('ws://localhost:8001');
const server = net.createServer();
server.on('connection', (socket) => {
// settings per connection opened
let requests = [];
// Receives data
socket.on('data', data => {
const requestID = crypto.randomBytes(16).toString("hex");
const additionalInfo = JSON.stringify({
"Request": requestID,
});
const header = Buffer.from(`${additionalInfo.length} ${additionalInfo}\n\n`);
wss.send(Buffer.concat([header, data]));
requests.push(requestID);
});
socket.on('error', error => {
console.log(error);
});
socket.on('end', () => {
requests = [];
});
// server response to the request
wss.on('message', (data) => {
// extract basic data
const jsonDataSize = data.toString().split(' ')[0];
const jsonDataOffset = jsonDataSize.length + 1;
const jsonDataText = data.slice(jsonDataOffset, Number(jsonDataSize) + jsonDataOffset).toString();
const jsonData = JSON.parse(jsonDataText);
const message = data.slice(Number(jsonDataSize) + jsonDataOffset + 2);
// check if received response matches the request
if (requests.includes(jsonData.Request)) {
// console.log(data.toString());
socket.write(message);
}
});
wss.on('error', error => {
console.log(error);
});
});
server.on('close', () => {
console.log("Server closed");
})
server.listen({host: "localhost", port: 8000}, () => {
console.log("Server listening on localhost:8000");
});
This is my code for the WebSocket receiver:
const net = require('net');
const WebSocket = require('ws');
const wss = new WebSocket.Server({port: 8001}, () => {
console.log('WebSocket server listening on localhost:8001');
});
wss.on('connection', socket => {
let server;
// trigger for a new message from the same connection
socket.on('message', data => {
// console.log("---------->");
// console.log(data.toString());
const jsonDataSize = data.toString().split(' ')[0];
const jsonDataOffset = jsonDataSize.length + 1;
const jsonDataText = data.slice(jsonDataOffset, Number(jsonDataSize) + jsonDataOffset).toString();
// check if it is a new tls connection request
let isTLSConnection = data.toString().indexOf("CONNECT") !== -1;
// set the new connection's settings
if (isTLSConnection && data.toString().includes('Host: ')) {
const serverPort = 443;
const serverAddress = data
.toString()
.split("CONNECT")[1]
.split(" ")[1]
.split(":")[0];
server = net.createConnection({host: serverAddress, port: serverPort});
// Send back 200 OK to the browser
const header = Buffer.from(`${jsonDataSize} ${jsonDataText}\n\n`);
socket.send(Buffer.concat([header, Buffer.from("HTTP/1.1 200 OK\r\n\r\n")]));
return;
} else if (data.toString().includes('Host: ')) {
const serverPort = 80;
const serverAddress = data.toString().split('Host: ')[1].split("\r\n")[0];
server = net.createConnection({host: serverAddress, port: serverPort});
}
// extract payload
const message = data.slice(Number(jsonDataSize) + jsonDataOffset + 2);
// send payload to server
server.write(message);
server.on('data', (data) => {
// console.log("<----------");
// console.log(data.toString());
const header = Buffer.from(`${jsonDataSize} ${jsonDataText}\n\n`);
socket.send(Buffer.concat([header, data]));
});
server.on('error', error => {
console.log(error);
});
});
socket.on('error', (error) => {
console.log(error);
});
});
I took inspiration from Build your own proxy server from scratch
I tried implementing a request/response model on top of WebSockets since I believe the error relies there, but it didn't work

You should use wss://localhost:8001 when using https

Related

New connection cause the current connections to stop working

I am making the chat application using socket (which I'm new at) with multiple tenants structure and using namespaces. Here's my code:
Socket server:
index.js
class Server {
constructor() {
this.port = process.env.PORT || 3000;
this.host = process.env.HOST || `localhost`;
this.app = express();
this.http = http.Server(this.app);
this.rootSocket = socketio(this.http);
}
run() {
new socketEvents(this.rootSocket).socketConfig();
this.app.use(express.static(__dirname + '/uploads'));
this.http.listen(this.port, this.host, () => {
console.log(`Listening on ${this.host}:${this.port}`);
});
}
}
const app = new Server();
app.run();
socket.js
var redis = require('redis');
var redisConnection = {
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASS
};
var sub = redis.createClient(redisConnection);
var pub = redis.createClient(redisConnection);
class Socket {
constructor(rootSocket) {
this.rootIo = rootSocket;
}
socketEvents() {
/**
* Subscribe redis channel
*/
sub.subscribe('visitorBehaviorApiResponse');
//TODO: subscribe channel..
// Listen to redis channel that published from api
sub.on('message', (channel, data) => {
data = JSON.parse(data);
console.log(data);
const io = this.rootIo.of(data.namespace);
if (channel === 'visitorBehaviorApiResponse') {
io.to(data.thread_id).emit('receiveBehavior', data);
io.to('staff_room').emit('incomingBehavior', data);
}
})
sub.on('error', function (error) {
console.log('ERROR ' + error)
})
var clients = 0;
this.rootIo.on('connection', (rootSocket) => {
clients++;
console.log('root:' + rootSocket.id + ' connected (total ' + clients + ' clients connected)');
const ns = rootSocket.handshake['query'].namespace;
// Dynamic namespace for multiple tenants
if (typeof (ns) === 'string') {
const splitedUrl = ns.split("/");
const namespace = splitedUrl[splitedUrl.length - 1];
const nsio = this.rootIo.of(namespace);
this.io = nsio;
this.io.once('connection', (socket) => {
var visitors = [];
console.log('new ' + socket.id + ' connected');
// once a client has connected, we expect to get a ping from them saying what room they want to join
socket.on('createChatRoom', function (data) {
socket.join(data.thread_id);
if (typeof data.is_staff !== 'undefined' && data.is_staff == 1) {
socket.join('staff_room');
} else {
if (visitors.some(e => e.visitor_id === data.visitor_id)) {
visitors.forEach(function (visitor) {
if (visitor.visitor_id === data.visitor_id) {
visitor.socket_ids.push(socket.id);
}
})
} else {
data.socket_ids = [];
data.socket_ids.push(socket.id);
visitors.push(data);
}
socket.join('visitor_room');
}
//TODO: push to redis to check conversation type
});
socket.on('sendMessage', function (data) {
console.log(data);
pub.publish('chatMessage', JSON.stringify(data));
this.io.in(data.thread_id).emit('receiveMessage', data);
this.io.in('staff_room').emit('incomingMessage', data);
// Notify new message in room
data.notify_type = 'default';
socket.to(data.thread_id).emit('receiveNotify', data);
}.bind(this))
socket.on('disconnect', (reason) => {
sub.quit();
console.log('client ' + socket.id + ' left, ' + reason);
});
socket.on('error', (error) => {
console.log(error);
});
});
}
// Root disconnect
rootSocket.on('disconnect', function () {
clients--;
console.log('root:' + rootSocket.id + ' disconnected (total ' + clients + ' clients connected)');
});
});
}
socketConfig() {
this.socketEvents();
}
}
module.exports = Socket;
Client:
const server = 'https://socket-server'
const connect = function (namespace) {
return io.connect(namespace, {
query: 'namespace=' + namespace,
resource: 'socket.io',
transports: ['websocket'],
upgrade: false
})
}
const url_string = window.location.href
const url = new URL(url_string)
const parameters = Object.fromEntries(url.searchParams)
const socket = connect(`${server}/${parameters.shopify_domain}`)
var handleErrors = (err) => {
console.error(err);
}
socket.on('connect_error', err => handleErrors(err))
socket.on('connect_failed', err => handleErrors(err))
socket.on('disconnect', err => handleErrors(err))
The problem that I met is when socket server got a new connection, the existing connections will be stopped working util they make a page refreshing to reconnect a new socket.id.
And when a namespace's client emit data, it sends to other namespaces, seem my code is not work correctly in a namespace.
Could you take a look at my code and point me where I'm wrong?
Thanks
1) Get UserId or accessToken while handshaking(in case of accessToken decrypt it).
and store userID: socketId(in Redis or in local hashmap) depends upon the requirement .
2) When u are going to emit to particular user fetch the socketid to that userid from redis or local hashmap
and emit to it.
**io.to(socketId).emit('hey', 'I just met you');**
3) If you are using multiple servers use sticky sessions
4) Hope this will help you

Node js Socket.iO accessing socket outside for multiple client

How can we access socket object outside for multiple socket connection. I created a object globally and tried to do this. But it always works for last connected socket.
'use strict';
const path = require('path')
const express = require('express');
const http = require('http');
const chalk = require('chalk');
const socketio = require('socket.io');
var connectionString = '';
const eventHubConsumerGroup = ""
const app = express()
const server = http.createServer(app ,() => {
console.log(chalk.green('Server created'))
})
const io = socketio(server)
const port = process.env.port || 3000
const publicDirectoryPath = path.join(__dirname , '../public')
var server_token = "1234567890";
app.use(express.static(publicDirectoryPath))
var localSocket;
io.on('connection',function(socket){
localSocket = socket;
console.log(socket.handshake.query.deviceID)
console.log('on user connected '+socket.id);
//report = new Report(socket);
socket.auth = false;
socket.on('authenticate',function(token){
console.log('token recieved is '+token);
if(server_token == token){
socket.auth = true;
console.log('connection is authenticated '+socket.id);
socket.emit("authenticate",true);
} else {
console.log("Connection not established")
socket.emit("authenticate",false);
}
})
socket.on('sendSocketEvent' , message => {
console.log(chalk.yellowBright(`Message recieved from ${socket.id} + ${message}`));
io.to(socket.id).emit('recieveSocketEvent', `Hello test`);
})
socket.on('disconnect',function(){
console.log('one user disconnected '+socket.id);
})
setTimeout(function(){
if(!socket.auth){
console.log('disconnecting the socket '+socket.id);
socket.emit("timeOut");
socket.disconnect();
}
},1000);
})
server.listen(port,() => {
console.log(chalk.redBright(`Server is up on port ${port}`))
})
var printMessage = function (message) {
console.log(JSON.stringify(message));
console.log(message.DeviceId);
if (localSocket != null){
if (message.DeviceId == localSocket.handshake.query.deviceID) {
localSocket.emit('recieveSocketEvent', message);
}
}
};
class EventHubReader {
constructor(connectionString, consumerGroup) {
this.connectionString = connectionString;
this.consumerGroup = consumerGroup;
this.eventHubClient = undefined;
this.receiveHandlers = undefined;
}
async startReadMessage(startReadMessageCallback) {
try {
console.log(this.connectionString)
const client = await EventHubClient.createFromIotHubConnectionString(this.connectionString);
console.log('Successfully created the EventHub Client from IoT Hub connection string.');
this.eventHubClient = client;
const partitionIds = await this.eventHubClient.getPartitionIds();
console.log('The partition ids are: ', partitionIds);
const onError = (err) => {
console.error(err.message || err);
};
const onMessage = (message) => {
const deviceId = message.annotations['iothub-connection-device-id'];
return startReadMessageCallback(message.body, message.enqueuedTimeUtc, deviceId);
};
this.receiveHandlers = partitionIds.map(id => this.eventHubClient.receive(id, onMessage, onError, {
eventPosition: EventPosition.fromEnqueuedTime(Date.now()),
consumerGroup: this.consumerGroup,
}));
} catch (ex) {
console.error(ex.message || ex);
}
}
// Close connection to Event Hub.
async stopReadMessage() {
const disposeHandlers = [];
this.receiveHandlers.forEach((receiveHandler) => {
disposeHandlers.push(receiveHandler.stop());
});
await Promise.all(disposeHandlers);
this.eventHubClient.close();
}
}
var { EventHubClient, EventPosition } = require('#azure/event-hubs');
const eventHubReader = new EventHubReader(connectionString, eventHubConsumerGroup);
(async () => {
console.log("Step1")
await eventHubReader.startReadMessage((message, date, deviceId) => {
console.log("Here getting called");
try {
const payload = {
IotData: message,
MessageDate: date || Date.now().toISOString(),
DeviceId: deviceId,
};
printMessage(payload);
} catch (err) {
console.error('Error broadcasting: [%s] from [%s].', err, message);
}
});
})().catch();
the problem is in condition "printMessage" . here I am trying to restrict the emit based on socket deviceID, but it's only working for last connected socket.
Can You please help me in this.
var localSocket;
io.on('connection',function(socket){
localSocket = socket;
})
You're overwriting the same variable, on each new connection, which means it will always point to the last socket connected.
What exactly do you want to do? To send this message to all connected sockets?

Dynamically Create Kafka Consumers That Send Data Through a Websocket

I am using kafka-node to read stream data and pass it to my web app with Web Sockets using NodeJS. This works fine if I able to define the kafka producer server and the topic I am interested in, however for my use case the end users will input the kafka producer server and the topic and my NodeJS backend will be responsible to receive that request and to create the appropriate kafka/websocket connections.
My idea was the following:
Create a rest API to which the web app could send requests to in order to create a new kafka consumer/web socket connection (/registerTopic)
Save the new kafka consumers in a global array when I create a new kafka consumer so that I can later pause or resume the stream with another rest API call (/pauseTopic and /resumeTopic)
I ran into problems trying to move the WebSocket code into /registerTopic...Whenever I do this everything acts very strangely and I suddenly get 1000x messages at once and then 40-50x messages every second even though the kafka producer is only sending 1 message per second. Any ideas on how I can get this working?
const express = require("express");
const app = express();
const cors = require('cors');
const bodyParser = require('body-parser');
const fs = require('fs');
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
var WebSocketServer = require('websocket').server;
var http = require('http');
var https = require('https');
var kafka = require('kafka-node');
var topics = [];
var privateKey = fs.readFileSync('PATH', 'utf8');
var certificate = fs.readFileSync('PATH', 'utf8');
var credentials = { key: privateKey, cert: certificate };
var consumers = new Set();
// This was working without any issues before I tried to make this dynamic!
/* var Consumer = kafka.Consumer,
client = new kafka.KafkaClient('localhost:9092'),
consumer = new Consumer(
client, [{ topic: 'numtest', partition: 0 }], { autoCommit: false }); */
var server = http.createServer(function (request, response) {
console.log(' Request recieved : ' + request.url);
response.writeHead(404);
response.end();
});
server.listen(8080, function () {
console.log('Listening on port : 8080');
});
webSocketServer = new WebSocketServer({
httpServer: server,
autoAcceptConnections: false
});
function iSOriginAllowed(origin) {
return true;
}
// This was working without any issues before I tried to make this dynamic!
/* webSocketServer.on('request', function (request) {
if (!iSOriginAllowed(request.origin)) {
request.reject();
console.log(' Connection from : ' + request.origin + ' rejected.');
return;
}
let connection = request.accept('echo-protocol', request.origin);
console.log(' Connection accepted : ' + request.origin);
connection.on('message', function (message) {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
}
});
consumer.on('message', function (message) {
console.log('msg');
connection.sendUTF(message.value);
});
connection.on('close', function (reasonCode, description) {
console.log('Connection ' + connection.remoteAddress + ' disconnected.');
});
}); */
var httpsServer = http.createServer(credentials, app);
httpsServer.listen(3000, () => {
console.log("Server running on port 3000");
});
app.get("/getTopics", (req, res, next) => {
res.json(topics);
});
app.post("/registerTopic", (req, res) => {
try {
var client = new kafka.KafkaClient(req.body.host);
var Consumer = kafka.Consumer;
consumer = new Consumer(
client, [{ topic: req.body.topic, partition: 0 }], { autoCommit: false });
let consumerExists = false;
for (let c = 0; c < [...consumers].length; c++) {
if ([...consumers][c].topic == req.body.topic && [...consumers][c].sessionId == req.body.sessionId) {
consumerExists = true;
}
}
if (!consumerExists) {
consumers.add({ 'topic': req.body.topic, 'sessionId': req.body.sessionId, 'consumer': consumer });
}
client.loadMetadataForTopics([], function (error, results) {
Object.keys(results[1].metadata).forEach(function (key) {
var value = results[1].metadata[key];
if (!value['0'].topic.includes('__') && !value['0'].topic.includes('offset')) {
topics.push({ 'producer': req.body.host, 'topic': value['0'].topic });
}
});
});
webSocketServer.on('request', function (request) {
if (!iSOriginAllowed(request.origin)) {
request.reject();
console.log(' Connection from : ' + request.origin + ' rejected.');
return;
}
let connection = request.accept('echo-protocol', request.origin);
console.log(' Connection accepted : ' + request.origin);
connection.on('message', function (message) {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
}
});
consumer.on('message', function (message) {
console.log('msg');
connection.sendUTF(message.value);
});
connection.on('close', function (reasonCode, description) {
console.log('Connection ' + connection.remoteAddress + ' disconnected.');
});
});
res.json("Working");
} catch (error) {
console.error(error);
res.status(400).send('Unable to register new topic')
}
});
app.post("/pauseTopic", (req, res) => {
try {
console.log(req.body);
let filteredConsumer = [...consumers].filter(function (item) {
console.log(req.body.topic, item.sessionId);
if (item.topic == req.body.topic && item.sessionId == req.body.sessionId) {
return c;
}
});
console.log(filteredConsumer);
//filteredConsumer[0].consumer.pause();
res.json("Working");
} catch (error) {
console.error(error);
res.status(400).send('Unable to register new topic')
}
});
app.post("/resumeTopic", (req, res) => {
try {
let filteredConsumer = [...consumers].filter(function (item) {
if (item.topic == req.body.topic && item.sessionId == req.body.sessionId) {
return item;
}
});
filteredConsumer[0].consumer.resume();
res.json("Working");
} catch (error) {
console.error(error);
res.status(400).send('Unable to register new topic')
}
});

Send message synchronously with Nodejs Websocket

My app has three modules:
WebSocket Module: resides on the remote server (internet) and it acts as a Websocket
server that entertain the connection between Client Module and
Webhook module.
Webhook Module: resides on the remote server (internet) and it acts as
a webhook to answer the HTTP post request from the user. It is connected to Websocket Module via websocket as well.
Client Module: resides on my local machine and it is connected to
the
Webhook model via websocket. This client responsible to get query
from my local backend.
When a user call Webhook Module through HTTP Post request, Webhook module initiate a connection to WebSocket module via websocket. Then, the WebSocket module initiate the connection to Client module and response back with the necessary information. Actually I have to do this to eliminate the HTTP tunnel that is blocked in my company.
The problem is, when I open two browser windows to perform the HTTP Post request at the same time with different parameter, for example, param "A", I would expect to get return "A", with param "B", I expect to get "B" not "A". But There is something wrong with my codes/design. If I executed at the same time, I throw "A" then I got "B" which is wrong. How do I overcome this.
This is a simple diagram to illustrate it.
WebSocket Module:
'use strict'
//This is WebSocket Server
const clients = {};
const SocketIO = require('socket.io');
const express = require('express');
const http = require('http');
const app = express();
const server = http.createServer(app);
const ws = SocketIO(server);
const port = 3000;
var clientid;
ws.on('connection', (client) => {
clients[client.id] = client;
console.log('new connection', client.id);
clientid = client.id;
client.emit('message', { message: 'welc' })
client.on('disconnect', () => {
delete clients[client.id];
console.log('Client ' + client.id + ' disconnected. Deleted');
});
client.on('WH', function (from, msg) {
console.log('Message from Webhook', from, ' : ', msg);
client.broadcast.emit('message', { message: msg });
//console.log('send to: ' + clientid);
//ws.to(clientid).emit('hey', { message: msg });
//client.emit('message', { message: msg })
});
client.on('CL', function (from, msg) {
console.log('Message from Client', from, ' : ', msg);
client.broadcast.emit('message', 'me', msg);
//ws.to(client.id).emit('message', 'me', msg);
//client.emit('message', 'me', msg);
});
});
server.listen(process.env.PORT || port);
console.log('WebSocket Server is running on port ' + port);
Webhook Module
'use strict'
//This is WebHook Server
const express = require('express');
const bodyParser = require('body-parser');
const request = require('request');
const http = require('http');
const io = require('socket.io-client');
const app = express()
app.use(bodyParser.json())
const clients = {};
const SocketIO = require('socket.io');
const server = http.createServer(app);
const ws = SocketIO(server);
const port = 5000;
let Res;
let httpreq = false;
let nctid;
let ts;
const socket = io.connect('http://localhost:3000', {reconnect: true});
// Add a connect listener
socket.on('connect', function() {
console.log('Connected to WebSocket server!');
});
socket.on('message', function(from, msg) {
//console.log('Message from ', from, ' : ', msg);
console.log('nctid: ' + nctid + ', ts: ' + ts);
//Get the message from Client
if (httpreq) {
Res.send({
replies: [{
type: 'text',
content: msg,
}],
conversation: {
memory: {
key: msg
}
}
})
httpreq = false;
}
});
app.listen(process.env.PORT || port, () => {
console.log('Webhook server is running on port ' + port);
})
app.post('/', (req, res) => {
//console.log(req.body)
let query = req.body.nlp.entities.query[0].value;
nctid = req.body.nlp.entities.nctid[0].value;
ts = Math.floor(Date.now() / 1000);
console.log("query: " + query + '|' + nctid + '|' + ts);
//Send message to WebSocket server with parameter query and NCTID
socket.emit('WH', 'me', query + '|' + nctid);
Res = res;
httpreq = true;
})
app.post('/errors', (req, res) => {
console.log(req.body);
res.send();
})
Client Module
'use strict'
//This is client app running on client premise
const request = require('request');
const parser = require('xml2json');
const io = require('socket.io-client');
const socket = io.connect('http://localhost:3000', {reconnect: true});
// Add a connect listener
socket.on('connect', function(socket) {
console.log('Connected to WebSocket server!');
});
socket.on('message', function(from, msg) {
//console.log('MSG', from, ' : ', msg);
console.log(from);
let param = from.message.split('|');
let query = param[0];
let nctid = param[1];
if (typeof nctid != 'undefined') {
getNCTID(nctid, function(returnValue) {
//console.log(returnValue);
try {
let json = parser.toJson(returnValue);
json = JSON.parse(json);
if (query == 'title')
socket.emit('CL', 'me', 'Title is ' + json.clinical_study.brief_title);
else
socket.emit('CL', 'me', 'Status is ' + json.clinical_study.overall_status);
} catch (e) {
console.log(e);
socket.emit('CL', 'me', 'No NCTID ' + nctid);
}
});
}
});
function getNCTID(nctid, callback) {
let url = "https://clinicaltrials.gov/ct2/show/" + nctid + "?displayxml=true";
let options = {
url: url,
method: 'GET'
}
//console.log(options);
let requestWithEncoding = function(options, callback) {
let req = request.get(options);
req.on('response', function(res) {
let chunks = [];
res.on('data', function(chunk) {
chunks.push(chunk);
});
res.on('end', function() {
let buffer = Buffer.concat(chunks);
let encoding = res.headers['content-encoding'];
if (encoding == 'gzip') {
zlib.gunzip(buffer, function(err, decoded) {
callback(err, decoded && decoded.toString());
});
} else if (encoding == 'deflate') {
zlib.inflate(buffer, function(err, decoded) {
callback(err, decoded && decoded.toString());
})
} else {
callback(null, buffer.toString());
}
});
});
req.on('error', function(err) {
callback(err);
});
}
requestWithEncoding(options, function(err, data) {
if (err) {
console.log('err:' + err);
callback('error');
} else
//console.log(data);
callback(data);
})
}

NodeJS + WS access currently running WS server instance

I have implemented a simple REST API using NodeJS, ExpressJS and routing-controllers. I have also implemented a basic WebSocket server running alongside the REST API and using WS.
const app = express();
app.use(bodyParser.json({limit: "50mb"}));
app.use(bodyParser.urlencoded({limit: "50mb", extended: true}));
useExpressServer(app, {
controllers: [
UserController
]
});
const server = app.listen(21443, (err: Error) => {
console.log("listening on port 21443");
});
const wss = new WebSocket.Server({server});
wss.on("connection", (ws: WebSocket) => {
ws.on("message", (message: string) => {
console.log("received: %s", message);
ws.send(`Hello, you sent -> ${message}`);
});
ws.send("Hi there, I am a WebSocket server");
});
My question is how to I get access to the currently running WS instance so that I am able to send or broadcast from my controller methods. I have a number of POST methods that run long processes and so return a HTTP 200 to the client, I then would like to either send or broadcast to all connected WS clients.
What is the correct way to access the WebSocket.Server instance from within my controller classes?
You can create the websocket earlier and pass the instance around:
const notifier = new NotifierService();
notifier.connect(http.createServer(app));
app.get("/somethingHappened", () => {
notifier.broadcast("new notification!!");
});
app.use(routes(notifier))
Full code:
app.js
Pass the websocket to the other routes:
const express = require("express");
const http = require("http");
const NotifierService = require("../server/NotifierService.js");
const routes = require("./routes");
const app = express();
const server = http.createServer(app);
const notifier = new NotifierService();
notifier.connect(server);
app.get("/somethingHappened", () => {
notifier.broadcast("new notification!!");
});
// to demonstrate how the notifier instance can be
// passed around to different routes
app.use(routes(notifier));
server
.listen(4000)
.on("listening", () =>
console.log("info", `HTTP server listening on port 4000`)
);
NotifierService.js class that handles the websocket
const url = require("url");
const { Server } = require("ws");
class NotifierService {
constructor() {
this.connections = new Map();
}
connect(server) {
this.server = new Server({ noServer: true });
this.interval = setInterval(this.checkAll.bind(this), 10000);
this.server.on("close", this.close.bind(this));
this.server.on("connection", this.add.bind(this));
server.on("upgrade", (request, socket, head) => {
console.log("ws upgrade");
const id = url.parse(request.url, true).query.storeId;
if (id) {
this.server.handleUpgrade(request, socket, head, (ws) =>
this.server.emit("connection", id, ws)
);
} else {
socket.destroy();
}
});
}
add(id, socket) {
console.log("ws add");
socket.isAlive = true;
socket.on("pong", () => (socket.isAlive = true));
socket.on("close", this.remove.bind(this, id));
this.connections.set(id, socket);
}
send(id, message) {
console.log("ws sending message");
const connection = this.connections.get(id);
connection.send(JSON.stringify(message));
}
broadcast(message) {
console.log("ws broadcast");
this.connections.forEach((connection) =>
connection.send(JSON.stringify(message))
);
}
isAlive(id) {
return !!this.connections.get(id);
}
checkAll() {
this.connections.forEach((connection) => {
if (!connection.isAlive) {
return connection.terminate();
}
connection.isAlive = false;
connection.ping("");
});
}
remove(id) {
this.connections.delete(id);
}
close() {
clearInterval(this.interval);
}
}
module.exports = NotifierService;
routes.js
const express = require("express");
const router = express.Router();
module.exports = (webSocketNotifier) => {
router.post("/newPurchase/:id", (req, res, next) => {
webSocketNotifier.send(req.params.id, "purchase made");
res.status(200).send();
});
return router;
};
List of connected clients are stored inside wss object. You can receive and loop through them like this:
wss.clients.forEach((client) => {
if (client.userId === current_user_id && client.readyState === WebSocket.OPEN) {
// this is the socket of your current user
}
})
Now you need to somehow identify your client. You can do it by assigning some id to this client on connection:
wss.on('connection', async (ws, req) => {
// req.url is the url that user connected with
// use a query parameter on connection, or an authorization token by which you can identify the user
// so your connection url will look like
// http://example.com/socket?token=your_token
ws.userId = your_user_identifier
....
})
To broadcast use:
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
If your controller and socket will be in different files (and I am sure they will), you will have to export the wss object in your socket file and import it in controller.

Resources