set cookie in WebSocket only if missing - node.js

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.

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.

Setting response headers in WebSocket server and verify in client using npm WebSocket server

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);
}
}

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

How can I get (Express's) sessionID for a SockJS connection

I am using SockJS on Express server. Is there any way to get the associate HTTP session ID of the client?
I see there is a way to do it for raw web socket and Socket.io, but I am struggling to find how to do it for SockJS.
This is how my server looks like. I want a similar handler to fetch session ID:
var sockjs_echo = sockjs.createServer(sockjs_opts);
sockjs_echo.on('connection', function(conn) {
conn.on('data', function(message) {
conn.write(message);
});
});
This is a "hack", but it works for me:
sockjs_echo.on('connection', function(conn) {
var cookieHeader = conn._session.recv.ws._stream._readableState.pipes._driver._request.headers.cookie
var cookies = {}
cookieHeader.split(';').forEach(function( cookie ) {
var parts = cookie.split('=');
cookies[parts.shift().trim()] = decodeURI(parts.join('='));
});
conn.on('data', function(message) {
conn.write(message);
});
});
'cookies' variable (example):
{
"dev_cookie": "1mimec6rbcolp0ujkcbqq9pdq4uoa5v0p8a284v32tmd4q3k0qi9p4vjteoifdn9b0lsm238fghf974o9jfehfuhvm3ltrgq02ad6k0",
"session_cookie": "s%3AjkKYPKFFT8r60rXUsVYISoOF17o49GUl.pbpu6T1%2BcdrIu5uQPRxZUYOrl5GnC179GaI5pWyR7SA",
"other_cookie": "s%3AzRMiC3fjo4gxTXX1p2XSi_C_EydIa358.KAdP1gwtZBVfcbkmwi%2B3pa0L1pbOCzQ3lHnNEyFvvHc"
}
Thanks so much for asking this question, #darwinbaisa, and for the answer, c-toesca. This came after days of searching.
For XHR streaming, the cookies are at: conn._session.recv.request.headers.cookie.
The only other way I could think of doing this was to make the express session cookie httpOnly: false, thus exposing it to javascript and, of course, the possibility of hacking, then pass it back as a prefix to any message from the SockJS javascript client to the node server.
Or to assign the ID to a javascript variable as I dynamically wrote a web page response, so that javascript would have access to the variable, and again could return it to the server. But again, this would have exposed the ID, and even if the ID was hashed or encrypted, it could still be used in a malicious call to the server from javascript.
Things like this are made a lot easier in the node WS library, but I need a fallback from that for websocket-challenged browsers.

How to authenticate socket.io connection without underlying useragent to keep the cookies and persist the session

I'm trying to test an app's socket.io which uses passport.socketio to authenticate the socket connection
var socket = require('socket.io-client')('http://localhost:' + app.PORT);
This does not work because there's no accompanying cookie.
Even if I get the cookie from a persisted superagent session
var cookie;
var agent = request.agent(app);
agent.post('/login').send('credentials').end(function(err, res) {
cookie = res.req._headers.cookie;
});
where/how do I use it ?
I found that there are already quite a few requests for socket.io-client to add cookie support
http://github.com/LearnBoost/socket.io-client/issues/450
http://github.com/LearnBoost/socket.io-client/pull/439
http://github.com/LearnBoost/socket.io-client/issues/344
but I don't see them going anywhere.
Is there any other solution to use persistent cookie session with socket while testing?
Cookie data could be passed using querystring
agent.post('/login').send('credentials').end(function(err, res) {
cookie = res.req._headers.cookie.replace(/=/g, '%3D'); //escape '='
});
socket = require('socket.io-client')('http://localhost' + '/?cookie=' + cookie);
It becomes available in the server socket
io.set('authorization', function(handshakeData, callback){
handshakeData._query.cookie;
});
And so it can be used to perform authorization. Since I was using passport.socketio, it plays nicely with a little change to check this query string instead of headers.

Resources