Prevent external users to connect websocket (wss) - node.js

I'm building a react-nodejs website at https://example.com and it utilizes websocket (using socket.io library), but right now people can just npm install socket.io-client and connect to my websocket using this:
const {io} = require("socket.io-client");
const socket = io("wss://example.com");
Now they can emit/listen to my backend. At the moment, I use ReCAPTCHA to prevent bots so you need to pass a token to connect to the socket.io server, I was wondering whether there is a better solution than this.

You are correct, you can't prevent people from trying to connect to your socket.io server manually with a custom script using socket.io-client. Normally that's a reason for you to design the server in a way, that connected client sockets can't do any harm. Client sockets are usually on the receiving side.
But of course there is a best-practice on how to authenticate the connecting clients, and, if the authentication is failing, to prevent the connection. Checkout out the socket.io Documentation about client options. There is an auth field available :
Client
import { io } from "socket.io-client";
const socket = io({
auth: {
token: "abcd"
}
});
Server
io.on("connection", (socket) => {
console.log(socket.handshake.auth); // prints { token: "abcd" }
});
So once the user is logged in, use the JWT (Token) (assuming you having a JWT Authentication in place) and pass it during establishing the connection. Then validate the JWT on the server side.
client-certificate authentication
There is also a more advanced option for client authentication which uses certificates:
Client
const fs = require("fs");
const socket = require("socket.io-client")("https://example.com", {
ca: fs.readFileSync("./server-cert.pem"),
cert: fs.readFileSync("./client-cert.pem"),
key: fs.readFileSync("./client-key.pem"),
});
Server
const fs = require("fs");
const server = require("https").createServer({
cert: fs.readFileSync("./server-cert.pem"),
key: fs.readFileSync("./server-key.pem"),
requestCert: true,
ca: [
fs.readFileSync('client-cert.pem')
]
});
const io = require("socket.io")(server);

Related

Create server available on HTTP/HTTPs that can be used with PM2

I'm trying to create a Socket.IO server that has the following goals:
Accessible on the local network of virtual machines using HTTP (http://<server-local-ip>)
That can be accessed via browser by users through the HTTPs protocol, and that can also make the socket.io.js bundle available via HTTPs (https://socket-server.example.com)
That uses all available CPUs in the virtual machine (the server will run in just one virtual machine) - (Possible with PM2)
Have the ability to be automatically restarted in case of failure (Possible with PM2)
For that I created a script based on the Socket.IO help article teaching how to use PM2 and this question that teaches to use HTTP and HTTPs.
/**
* pm2 start basic.js -i 0
*/
const http = require("http");
const https = require("https");
const { Server } = require("socket.io");
const { createAdapter } = require("#socket.io/cluster-adapter");
const { setupWorker } = require("#socket.io/sticky");
const { readFileSync } = require("fs");
const httpServer = http.createServer();
const httpsServer = https.createServer({
key: readFileSync("./localhost-key.pem"),
cert: readFileSync("./localhost.pem")
});
const io = new Server(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
io.adapter(createAdapter());
setupWorker(io);
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
});
httpsServer.on("request", (req, res) => {
io.engine.handleRequest(req, res);
});
httpsServer.on("upgrade", (req, socket, head) => {
io.engine.handleUpgrade(req, socket, head);
});
httpServer.listen(8080);
httpsServer.listen(4430);
Using HTTP and HTTPs always throws an error.
Via HTTPs I can't load the socket.io.js bundle. But as this service will be available via browser, it will be necessary to make it available via HTTPs to users.
Direct access via HTTPs displays:
{
code: 0,
message: "Transport unknown"
}
This is just using the first part of the script, without trying to run with PM2 yet.
When placing the PM2 part next to the script, other errors appear:
I have to remove the code httpServer.listen(3000); for HTTP to work
When I connect to HTTPs the code never finds the session, so it keeps trying to reconnect endlessly.
socket.io.js via HTTPs remains unreachable
Even using HTTP socket.io.js and connecting with <script src="http://localhost:8080/socket.io/socket.io.js"></script> <script> const socket = io('https://localhost:3001');</script> nothing works
However, if I run all this over HTTP only, without requiring HTTPs, it works perfectly.
What am I doing wrong for HTTP/HTTPs not to work together?
Will I have to make the server available only in HTTP and create a proxy via NGINX to support HTTPs and call the HTTP server?

Preventing additional connections in the socket io

How to only allow access to connections from the app in nodejs and socketio?
You will want to pass a JWT with the connection like
socket = io.connect(server, {
"transports": ['websocket'],
query: {
token: JWTTOKEN,
},
"forceNew":true
});
On your socket server you decode the token, if not then don't let them connect. The client request the token each time then its passed to the server for connection.

WebSocket is closed before the connection is established. Socket.io and React

I want to make an Chat application with Socket.io and I've followed this tutorial: https://youtu.be/ZwFA3YMfkoc. I am using React and Node.js
Everything works fine while using it locally and even on different devices on my Network. However if I am hosting my Backend on Heroku it doesn't work.
The error Message is:
WebSocket connection to 'URL' failed: WebSocket is closed before the connection is established.
(URL is the URL of my Backend with the Port). I am using SSL.
I've already tried to enable session affinity but it already was enabled.
My backend code is: (atleast the code that I think is important to the Problem)
const app = express();
const server = http.createServer(app);
const io = socketio(server);
app.use(cors());
server.listen(PORT, () => console.log("Server started on " + PORT));
My frontend code is written in React and is:
var connectionOptions = {
"force new connection": true,
reconnectionAttempts: "Infinity",
timeout: 10000,
transports: ["websocket"],
};
const ENDPOINT = "URL";
socket = io(ENDPOINT, connectionOptions);
So I've fixed my Problem.
The URL on the client side had to be without the Port.
So for example:
const ENDPOINT = "https://web.site.com";
and not:
const ENDPOINT = "https://web.site.com:1337";
Call socket.connect() or just add autoconnect: true in options you are providing.

Using greenlock-express with web sockets

Looking for an example of how to setup HTTPS with greenlock-express in combination with a secure websocket server.
Here's how I wound up setting this up. The key was to use the tslOptions generated by greenlock-express to setup an HTTPS server manually, and then attach a Websocket Server to it in the normal way. With this approach, the redirect from HTTP to HTTPS has to be done manually.
I initially couldn't get things to work because I hadn't opened up port 443 on my server. Make sure you do that otherwise HTTPS won't work!
const express = require('express');
const http = require('http');
const https = require('https');
const WebSocket = require('ws');
//EXPRESS TO BUNDLE APP
let my_app = express();
let dir = __dirname + '/../app';
io_app.use(express.static(dir));
//Just serving static files from a sibling directory called /app
//// SETUP HTTP GREENLOCK
let greenlock = require('greenlock-express').create({
// Let's Encrypt v2 is ACME draft 11
version: 'draft-11'
,
server: 'https://acme-v02.api.letsencrypt.org/directory'
// Note: If at first you don't succeed, switch to staging to debug
// https://acme-staging-v02.api.letsencrypt.org/directory
// You MUST change this to a valid email address
,
email: 'test#example.com'
// You MUST NOT build clients that accept the ToS without asking the user
,
agreeTos: true
// You MUST change these to valid domains
// NOTE: all domains will validated and listed on the certificate
,
approveDomains: ['example.com', 'www.example.com']
// You MUST have access to write to directory where certs are saved
// ex: /home/foouser/acme/etc
,
configDir: require('path').join(require('os').homedir(), 'acme', 'etc')
// Join the community to get notified of important updates and help me make greenlock better
,
communityMember: true
// Contribute telemetry data to the project
,
telemetry: true
,
debug: true
});
//// REDIRECT HTTP TO HTTPS
let redirectHttps = require('redirect-https')();
let acmeChallengeHandler = greenlock.middleware(redirectHttps);
http.createServer(acmeChallengeHandler).listen(80, function() {
console.log("Listening for ACME http-01 challenges on", this.address());
});
//// HTTPS SERVER + WEBSOCKETS
let server = https.createServer(greenlock.tlsOptions, my_app);
let ws = new WebSocket.Server({
server
});
ws.on('connection', function(ws, req) {
//websocket on connection...
});
server.listen(443);

How to make 2 nodejs servers connect with SSL websocket?

I am trying to make 2 servers communicate via socket.io library and SSL.
This used to work until an upgrade of socket.io package (can't tell you which).
I have managed to fix secure connection with a browser. I have also made it work between unsecure (http) servers. But the secure (https) servers refuse to connect between themselves. You may argue that socket.io is not made for server to server communications, but it would save me lots of work to fix it.
I am now running:
node: 7.5.0
express: 4.16.2
socket.io (and socket.io-client): 2.0.3
I cannot even make simple examples below work (removing all my middleware).
node server
// Use SSL certificate
const cert_path = "..";
const fs = require('fs');
const https_options = {
key: fs.readFileSync(cert_path+'/privkey.pem'),
cert: fs.readFileSync(cert_path+'/cert.pem')
};
const app = require('express')();
const https = require('https');
const server = https.createServer(https_options, app);
const io = require('socket.io')(server);
server.listen(8000);
io.on('connection', function (socket) {
console.log("connected");
});
node client
const io = require('socket.io-client');
const socket = io.connect(
'https://localhost:8000',
{secure: true}
);
socket.on("connect", function () {
console.log("connected");
});
Nothing happens, none of them connect. Any idea why?
EDIT: I'm getting both connect_error and reconnect_error that pop every 5s on client side:
{ Error: xhr poll error
at XHR.Transport.onError (../node_modules/engine.io-client/lib/transport.js:64:13)
at Request.<anonymous> (../node_modules/engine.io-client/lib/transports/polling-xhr.js:128:10)
at Request.Emitter.emit (../node_modules/component-emitter/index.js:133:20)
at Request.onError (../node_modules/engine.io-client/lib/transports/polling-xhr.js:310:8)
at Timeout._onTimeout (../node_modules/engine.io-client/lib/transports/polling-xhr.js:257:18)
at ontimeout (timers.js:365:14)
at tryOnTimeout (timers.js:237:5)
at Timer.listOnTimeout (timers.js:207:5) type: 'TransportError', description: 503 }
Digging further in the errors, I see it may come from the certificate. But while I apply several workarounds of SO, I'm getting consecutively ECONNREFUSED, UNABLE_TO_VERIFY_LEAF_SIGNATURE, and finally DEPTH_ZERO_SELF_SIGNED_CERT...
After trying hard:
re-generate my Let's Encrypt certificate
re-generate my self-signed certificates (openssl) and use them by server+client
tinker with socket.io connect options (secure, rejectUnauthorized, ..)
tinker with nodejs global setup even (process.env['NODE_TLS_REJECT_UNAUTHORIZED'])
I finally stumbled on this page of github. It solved my issue and it's worth sharing it.
node client
const https = require('https');
https.globalAgent.options.rejectUnauthorized = false;
const io = require('socket.io-client');
const sockets = io.connect('https://localhost:8001', {agent: https.globalAgent});
Even if I would have preferred getting my connection authorized in the first place, this will work for me.

Resources