Setting response headers in WebSocket server and verify in client using npm WebSocket server - node.js

I am creating a mock web server using ws library of node.js:
https://www.npmjs.com/package/ws#api-docs
I need to set a protocol in Sec-WebSocket-Protocol header and send it to client, then verify the header on client side.
I tried below options:
wss.on('headers', function (headers) {
console.log("on headers");
headers.push(`sec-websocket-protocol: ${protocol}`);
})
Also this:
var msg = {
message: message,
"sec-websocket-protocol": protocol
};
ws.send(JSON.stringify(msg));
Nothing seems to work currently. Also on client side I am not sure on how to verify this header?

There is no need to mess with the headers yourself.
On the client side you list the protocols as the second arguemtn to the Websocket constructor.
const ws = new WebSocket(ws_url, ["protocol-1", "protocol-2", "protocol-3"]);
On the server side, you need to pass a handleProtocols function, to chose one of the available protocols.
var wss = new WebSocketServer({
...
handleProtocols: (protocols, client) => {
var protocol = /* choose one from protocols argument */;
return protocol;
},
...
});
Then on the client side you get the chosen protocol on the protocol property on your WebSocket object.
ws.onopen = function() {
console.log("WS opened; protocol chosen:", this.protocol);
};
ws.onmessage = function(data) {
if (this.protocol in protocol_handlers) {
protocol_handlers[this.protocol](data.data);
}
}

Related

Websockets token authentication using middleware and express in node.js

I use node.js, express and express-ws that is based on ws
Express-ws allows to create express-like endpoints for websockets.
I am looking for a solution to authenticate users in websocket connections, based on a token. Since my ws server is based on an HTTP one
const wsHttpServer = http.createServer();
wsHttpServer.listen(5001);
const expressWs = require('express-ws')(app , wsHttpServer);
and since the ws connection is based on an HTTP one that gets upgraded to a ws, WHY I cannot pass a token in my ws that the express route checks, like any other one? My logic is, send the token, check it, if it is ok, proceed to upgrade to a ws connection. So, I can reuse the token-middleware solution that I have in my HTTP connections.
In node
My ws server
const wsHttpServer = http.createServer();
wsHttpServer.listen(5001);
const expressWs = require('express-ws')(app , wsHttpServer);
//set the route
app.use('/ws', require('./routes/wsroute'));
In that route, I would like to use the token.validate() middleware -that in HTTP connections, checks the Authorization header
router.ws('/user/:name/:id', token.validate(), (ws, req) => {
console.log('ws route data : ',vessel, req.params.name, req.params.id);
});
In my client
const socket = new WebSocket('ws://localhost',{
path: '/user/Nick/25/',
port: 5001, // default is 80
protocol : "echo-protocol", // websocket protocol name (default is none)
protocolVersion: 13, // websocket protocol version, default is 13
keepAlive: 60,
headers:{ some:'header', 'ultimate-question':42 } // websocket headers to be used e.g. for auth (default is none)
});
this errors Failed to construct 'WebSocket': The subprotocol '[object Object]' is invalid
I also tried
const socket = new WebSocket('ws://localhost:5001/user/Nick/25', ["Authorization", localStorage.getItem('quad_token')]);
I dont get any errors, but I dont know how to get the Authorization "header" in node
I could
just send const socket = new WebSocket(currentUrl); with some data and include a valid token in that data. But to check it, I have to allow the connection first. I dont want that, I would like to use a middleware solution that automatically checks a token and allows or not to continue.
Questions
Please help me understand:
1 Is it possible to use a token-based, middleware-based solution in ws?
2 How to set a header with a token in a ws connection?
3 How to get that token in node?
1) In my experience there is no available express.js middleware and the solution i found requires to listen to the upgrade event on your http server and blocking access to your socket connection before it reaches ws routes.
2) Your browser will not allow setting additional headers during websocket connection on the client side. It will send though the cookies so you can make use of express-session to authorize on your server first the user, a cookie will be set on the browser and that cookie will be sent over during the websocket connection.
3) You can do like in this answer Intercept (and potentially deny) web socket upgrade request Copying the code here from there for your own perusal.
**wsHttpServer**.on('upgrade', function (req, socket, head) {
var validationResult = validateCookie(req.headers.cookie);
if (validationResult) {
//...
} else {
socket.write('HTTP/1.1 401 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');
socket.close();
socket.destroy();
return;
}
//...
});
As outlined here, it seems that it is not possible for a standard browser websocket client to handle a http error response to an upgrade request. Thus what I ended up using was something like this:
HTTPserver.on('upgrade' (req, sock, head) => {
if (req.url === wsRoute) {
webSocketServer.handleUpgrade(req, sock, head, ws => {
const authenticated = validateToken(req.headers.cookie) // your authentication method
if (!authenticated) {
ws.close(1008, 'Unauthorized') // 1008: policy violation
return
}
webSocketServer.emit('connection', ws, req)
})
} else {
sock.destroy()
}
}
This way we accept the connection first before closing it with an appropriate code and reason, and the websocket client is able to process this close event as required.
On your client side, you should pass an array of strings instead of object, but you must set a header for your HTTP response with a key and value:
key : headeSec-WebSocket-Protocol
value : corresponding protocol used in front.

set cookie in WebSocket only if missing

We can set the cookie in WebSocket handshake: Set cookie inside websocket connection, however I can't decide whether the cookie was already set:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8088 });
wss.on("headers", onHeaders);
function onHeaders(headers) {
console.log("onHeaders cookie: " + headers.cookie); // undefined
headers.push('Set-Cookie: ' + cookie.serialize('client', 1));
}
How can I see whether the "client" value is already available, before setting the cookie?
Install a handler function for the connection event on the WebSocket server. This fires when a WebSocket request is received, and it is passed a request object (an instance of http.IncomingMesssage) as an argument. You can examine the headers of the request object to see whether your cookie is present in the request. Something like:
wss.on('connection', onConnection);
function onConnection(websock, request) {
console.log(request.headers);
}
although of course you'll want to do something more complicated than just printing the headers.

Spring websocket over stomp

Im new to websocket and have been exploring spring websocket solution, I've implemented the hello world application from the following url: Spring websocket.
Instead of using the index.html page, I would like to call the server from nodejs. Here is my implementation with SockJS and Stompjs.
var url = 'http://localhost:8080'
var SockJS = require('sockjs-client'),
Stomp = require('stompjs'),
socket = new SockJS(url + '/hello'),
client = Stomp.over(socket)
function connect(){
client.connect({}, function(frame){
console.log(frame)
client.subscribe(url + '/topic/greetings', function(greeting){
console.log(greeting)
})
})
}
function sendName(){
var name = 'Gideon'
client.send(url + '/app/hello', {}, JSON.stringify({ 'name': name }))
}
function disconnect(){
if(client)
client.disconnect()
}
function start(){
connect()
sendName()
}
start();
I run the script with node --harmony index.js
This are the errors i'm getting when trying different url:
url :var socket = new SockJS('http://localhost:8080/hello')
Error: InvalidStateError: The connection has not been established yet
url: var socket = new SockJS('/hello')
Error: The URL '/hello' is invalid
url: var socket = new SockJS('ws://localhost:8080/hello')
Error: The URL's scheme must be either 'http:' or 'https:'. '" + parsedUrl.protocol + "' is not allowed.
My dependencies
"dependencies": {
"sockjs-client": "^1.0.3",
"stompjs": "^2.3.3"
}
Project can be found here: https://bitbucket.org/gideon_o/spring-websocket-test
The expected endpoint URL for SockJS is an HTTP endpoint. SockJS will check if the WebSocket protocol is available before using it or falling back to other options like long polling. Your first option is the correct one:
var socket = new SockJS('http://localhost:8080/hello')
The STOMP client connect method is non-blocking, that's why you provide a callback that will be executed when the connection is stablished. You are trying to send a message over that connection right after calling the connect method. The connection hasn't been stablished yet (too fast), and you get the error message:
Error: InvalidStateError: The connection has not been established yet
You'll have to move the sending of the message to the callback provided to the connect method to make sure it is already stablished. The same applies to subscriptions (which you already do in your example).
One more thing to notice is that a STOMP destination is not a URL. There's no need to prefix the destination with http://localhost:8080, the destination should be simply /topic/greetings

Node JS TCP Proxy: Reuse socket in callback function

I'm trying to implement a TCP proxy in Node JS. I only have some experience with Javascript so I met a lot of problems along the way. I've done a lot of searching for this one but had no luck.
The problem occurs when browser sends a CONNECT request for HTTPS. My proxy will parse the host name and port, and then create a new socket that connects to the server. If all these steps went well, I will start forwarding message.
Part of my code looks like this:
var net = require('net');
var server = net.createServer(function(clientSock) {
clientSock.on('data', function(clientData) {
var host = // get from data
var port = // get from data
if (data is a CONNECT request) {
// Create a new socket to server
var serverSock = new net.Socket();
serverSock.connect(port, host, function() {
serverSock.write(clientData);
clientSock.write('HTTP/1.1 200 OK\r\n');
}
serverSock.on('data', function(serverData) {
clientSock.write(serverData);
}
}
}
}
Since the CONNECT request needs both client socket and server socket open until one side closes the connection, the code above doesn't have this behavior. Every time I receive some data from client, I will create a new socket to server and the old one is closed.
Is there a way to store the server socket as a global variable so that the data event handler can reuse it? Or is there any other way to solve this?
Thanks a lot!!!!
You can just move the variable up to a higher scope so it survives across multiple events and then you can test to see if its value is already there:
var net = require('net');
var server = net.createServer(function(clientSock) {
var serverSock;
clientSock.on('data', function(clientData) {
var host = // get from data
var port = // get from data
if (data is a CONNECT request) {
// Create a new socket to server
if (!serverSock) {
serverSock = new net.Socket();
serverSock.connect(port, host, function() {
serverSock.write(clientData);
clientSock.write('HTTP/1.1 200 OK\r\n');
}
serverSock.on('data', function(serverData) {
clientSock.write(serverData);
}
} else {
serverSock.write(clientData);
}
}
}
}

webSocketServer node.js how to differentiate clients

I am trying to use sockets with node.js, I succeded but I don't know how to differentiate clients in my code.
The part concerning sockets is this:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({port: 8080});
wss.on('connection', function(ws) {
ws.on('message', function(message) {
console.log('received: %s', message);
ws.send(message);
});
ws.send('something');
});
This code works fine with my client js.
But I would like to send a message to a particular user or all users having sockets open on my server.
In my case I send a message as a client and I receive a response but the others user show nothing.
I would like for example user1 sends a message to the server via webSocket and I send a notification to user2 who has his socket open.
In nodejs you can directly modify the ws client and add custom attributes for each client separately. Also you have a global variable wss.clients that can be used anywhere. Please try the following code with at least two clients connected:
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({
server: httpsServer
});
wss.getUniqueID = function () {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4();
};
wss.on('connection', function connection(ws, req) {
ws.id = wss.getUniqueID();
wss.clients.forEach(function each(client) {
console.log('Client.ID: ' + client.id);
});
});
You can also pass parameters directly in the client connection URL:
https://myhost:8080?myCustomParam=1111&myCustomID=2222
In the connection function you can get these parameters and assign them directly to your ws client:
wss.on('connection', function connection(ws, req) {
const parameters = url.parse(req.url, true);
ws.uid = wss.getUniqueID();
ws.chatRoom = {uid: parameters.query.myCustomID};
ws.hereMyCustomParameter = parameters.query.myCustomParam;
}
You can simply assign users ID to an array CLIENTS[], this will contain all users. You can directly send message to all users as given below:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({port: 8080}),
CLIENTS=[];
wss.on('connection', function(ws) {
CLIENTS.push(ws);
ws.on('message', function(message) {
console.log('received: %s', message);
sendAll(message);
});
ws.send("NEW USER JOINED");
});
function sendAll (message) {
for (var i=0; i<CLIENTS.length; i++) {
CLIENTS[i].send("Message: " + message);
}
}
you can use request header 'sec-websocket-key'
wss.on('connection', (ws, req) => {
ws.id = req.headers['sec-websocket-key'];
//statements...
});
This code snippet in Worlize server really helped me a lot. Even though you're using ws, the code should be easily adaptable. I've selected the important parts here:
// initialization
var connections = {};
var connectionIDCounter = 0;
// when handling a new connection
connection.id = connectionIDCounter ++;
connections[connection.id] = connection;
// in your case you would rewrite these 2 lines as
ws.id = connectionIDCounter ++;
connections[ws.id] = ws;
// when a connection is closed
delete connections[connection.id];
// in your case you would rewrite this line as
delete connections[ws.id];
Now you can easily create a broadcast() and sendToConnectionId() function as shown in the linked code.
Hope that helps.
It depends which websocket you are using. For example, the fastest one, found here: https://github.com/websockets/ws is able to do a broadcast via this method:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({host:'xxxx',port:xxxx}),
users = [];
wss.broadcast = function broadcast(data) {
wss.clients.forEach(function each(client) {
client.send(data);
});
};
Then later in your code you can use wss.broadcast(message) to send to all. For sending a PM to an individual user I do the following:
(1) In my message that I send to the server I include a username
(2) Then, in onMessage I save the websocket in the array with that username, then retrieve it by username later:
wss.on('connection', function(ws) {
ws.on('message', function(message) {
users[message.userName] = ws;
(3) To send to a particular user you can then do users[userName].send(message);
I'm using fd from the ws object. It should be unique per client.
var clientID = ws._socket._handle.fd;
I get a different number when I open a new browser tab.
The first ws had 11, the next had 12.
You can check the connection object. It has built-in identification for every connected client; you can find it here:
let id=ws._ultron.id;
console.log(id);
One possible solution here could be appending the deviceId in front of the user id, so we get to separate multiple users with same user id but on different devices.
ws://xxxxxxx:9000/userID/<<deviceId>>
By clients if you mean the open connections, then you can use ws.upgradeReq.headers['sec-websocket-key'] as the identifier. And keep all socket objects in an array.
But if you want to identify your user then you'll need to add user specific data to socket object.
If someone here is maybe using koa-websocket library, server instance of WebSocket is attached to ctx along side the request. That makes it really easy to manipulate the wss.clients Set (set of sessions in ws). For example pass parameters through URL and add it to Websocket instance something like this:
const wss = ctx.app.ws.server
const { userId } = ctx.request.query
try{
ctx.websocket.uid = userId
}catch(err){
console.log(err)
}
Use a global counter variable and assign its value for every new connection:
const wss = new WebSocket.Server({server});
let count_clients = 0;
wss.on('connection', function connection(ws){
ws.id=count_clients++;
console.log(`new connection, ws.id=${ws.id}, ${ws._socket.remoteAddress}:${ws._socket.remotePort} #clients=${wss.clients.size}`);
ws.on('close', req => {console.log(`disconnected, ws.id=${ws.id}, ${ws._socket.remoteAddress}:${ws._socket.remotePort} #clients=${wss.clients.size}`);});
...
Here is what I did:
* on connect, server generate an unique id (e.g uuid) for the connection,
* save it in memory, (e.g as key of map),
* send back to client in response,
*
*
* client save the id, on each request will also send the id as part of request data,
* then server identify the client by id, on receive further request,
*
* server maintain client, e.g cleanup on close/error,
*
I've impl the idea, it works well to identify the client.
And, I also achieved group/topic broadcast based on the idea, which need the server to maintain extra info.
There are a lot of interesting answers that do the job, however they mostly seem unclean, that is if you don't mind mutating the ws object. I did it this way because I'm using TypeScript and you can't arbitrarily add properties to objects.
import WebSocket from 'ws'
declare module 'ws' {
interface WebSocket {
id: any
key: string
}
}
The id doesn't have to be type any can be number or string depending on how you ID your connections. I haven't flushed out the system yet but for now when a connection is made, I just assign a random number.
const socketConnection = (socket: WebSocket.WebSocket): void => {
socket.id = Math.random()
console.log(socket.id)
const msg = JSON.stringify({ res: `[open] Welcome to the WebSocket server!` })
socket.send(msg)
}
This can be modified at any point so once I authenticate the connection I plan on assigning a relative ID here and might even add in a key property if I want to do some more fancy stuff.
How this works is explained in the Module Augmentation section of the documentation.
TypeScript: Module Augmentation
You can check that it's still assigned by looking over multiple messages in the onmessage event.
const socketMessage = (socket: WebSocket.WebSocket): void => {
socket.on('message', async (message: WebSocket.RawData) => {
console.log(socket.id)
console.log(socket.key)
})
}
Oh and a note, I made this module declaration in the document where I setup my socket. But the modification does populate across documents. For example in the AuthController I started prototyping I use it this way.
export default class AuthController {
public static connections: DLinkedList = new DLinkedList()
static async validate(request: { id: string, socket: WebSocket.WebSocket }): Promise<void> {
console.log('test', request.socket.id)
this.connections.add(request.socket, request.id)
request.socket.send(JSON.stringify({ res: true }))
console.log(this.connections.size())
}
static getSocket(id: string): WebSocket.WebSocket {
return this.connections.getAtKey(id).data
}
static removeSocket(socket: WebSocket.WebSocket) {
}
}
You can also do this in pure JS just by directly modifying the WebSocket object prototype. Some of the answers here talk about it. I haven't done it myself but the principle is similar.
Add a method to an existing class in typescript?
Hope this is useful.

Resources