I would like to add a backwards compatible feature to node-red-dashboard
that will allow us to add custom middleware to the socket io connection. The implementation is simple and not too different from how we add http middleware. The main difference is that io.use()
can only receive a single function, instead of an array of functions. But, we can call io.use()
multiple times to add as many middleware functions as needed.
example code to add user configured io middleware as a single function, an array of functions, or fallback to the default:
if (typeof uiSettings.ioMiddleware === 'function') {
io.use(uiSettings.ioMiddleware);
} else if (Array.isArray(uiSettings.ioMiddleware)) {
uiSettings.ioMiddleware.forEach(middleware => {
io.use(middleware);
});
} else {
// use the default that guards against cross domain connection attempts
io.use(function (socket, next) {
if (socket.client.conn.request.url.indexOf('transport=websocket') !== -1) {
// Reject direct websocket requests
socket.client.conn.close();
return;
}
if (socket.handshake.xdomain === false) {
return next();
} else {
socket.disconnect(true);
}
}
}
In our custom io middleware, we can look at more things that just xdomain. For example, we can get the cookies from socket.request.cookies and verify user authentication, etc. We can then take full advantage of creating an error object and calling next(error)
. This will send the simple error object to the client side code and trigger the connect_error
event, at which point the error's data can be acted upon. The only error info that survives the trip from server to client is error.message
and error.data
. error.data can be an object containing additional data. It only supports simple data such as boolean, int, string, etc.
example sending error from within socket io middleware:
const err = new Error();
err.message = 'authentication fail';
err.data = {
forceReload: true, // use this to trigger the forceReload() function in main.js
}
next(err);
example client side code in main.js to handle the error.data.forceReload
value:
// inside the `connect_error` handler
if ((error.type === 'TransportError' && error.description === 401) || (error.data && error.data.forceReload === true)) {
forceReload();
}
An alternative to using forceReload could be something like forceRedirect, where the server side could send an error with { data: { forceRedirect: '/loginpage' } }
and that could trigger the connect_error handler to set window.location = error.data.forceRedirect
.
If this seems reasonable, I can make a PR.
And as a reminder, this would be completely backwards compatible and not break anything because it preserves the existing behavior and the new setting is opt-in.