Good day to all!
In this post I will try to consider the problems of an encrypted connection to a Web server (HTTPS) and the same connection to a Comet server using WebSocket (WSS) when using self-signed certificates, as well as options to solve this problem.
At the moment, I am developing a Web UI to a certain company product that will be used within client corporate networks.
')
Initial conditions: access to the Web UI directly by the server’s IP address, it is desirable to use standard 80/443 ports to facilitate deployment, the server's OS is Windows Server 2012.
We need a Comet server to organize communication between a certain service that generates reports on specified parameters, and the Web UI directly, through which the client sends a request, receives the status of the completion and completion of the task, as well as for sending various service messages about events (for example - the user is logged in, the user data has been changed by the administrator, etc.).
For the Comet server and the Web UI backend, we use Node.JS.
In general, I don’t have much experience with using Node.JS, but as it turned out, it’s very convenient to do similar tasks on it - without going into details on the very writing of the first version with a global broadcasting of messages between all participants without “rooms” or “channels”, it took me literally couple of hours. And a lot of time was invested in Node.JS modules on which a WebSocket server (Socket.IO, WS) can be implemented - then it took a while to study the connection of the C # server component and the Comet server. Socket.IO didn’t work for us - in version> 1, there is a lot of overhead implemented there, we decided to use a more native implementation for various reasons and took the
WS module .
I will say right away that the Comet-server has two interfaces: the first is the usual HTTP interface (internal with authorization of requests), through which users and components are authorized, sending commands to the server; the second is directly WebSocket interface (WS), which connects authorized users and components to receive messages about events in the system.
In the HTTP + WS combination, everything naturally works with a bang and no problems, but for the task we need SSL, and accordingly HTTPS + WSS.
Creating a self-signed certificate using OpenSSL is
simple , screwing a
couple of lines to the Web UI, too,
is easy to interface Comet.
And here the most interesting began.
As everyone knows, when using self-signed certificates, any browser issues a warning and asks the user to confirm that he trusts this certificate.
So, we go to the test Web UI with client emulation at
127.0.0.1
127.0.0.1
, we confirm the certificate, we give an attempt to connect to the address
wss://127.0.0.1:4433/
- and we get a connection error. Because for the address 127.0.0.1 and port 4433 the certificate must be confirmed a second time. The browser doesn’t issue any additional messages about the need to confirm the certificate. Just silently resets the request and that's it, it doesn't even reach the server.
Of the four tested browsers - IE, Firefox, Opera, Chrome - the second time they do not ask for confirmation to another port at the same address, Chrome and Opera, IE and Firefox silently chop the request and issue an error to the console.
Since the client will most likely have IE (after all, Windows is a mass OS for corporate clients), this is a big problem.
Solution 1
The simplest solution that comes to mind is to proxy requests. In Linux, I would put Nginx and proxy through it, but we have Windows. I took Apache 2.2 that I already had, set up SSL and gave this directive:
ProxyPass /wss/ ws://127.0.0.1:8095/ ProxyPass / http://127.0.0.1:8080/
That is, requests for / wss / forward to the WS interface, everything else - on the Web UI.
It did not work. Googling and so on gave Apache 2.4 with the mod_proxy_wstunnel module. Put, connect - everything is fine, Web UI issues a request once, WSS connection is established without any problems.
But Apache in this turns out all the same superfluous component, and a little spoils performance which is given by asynchronous application on Node.JS.
I decided to try to write a proxy server on Node.JS, did not work. In short, simultaneous requests to any one port with raising of WS and HTTPS interfaces HTTPS interface at point-blank does not catch requests using the WSS protocol. Totally. Proxy HTTPS separately for internal HTTP and separate WSS for internal WS proxies, but to catch the url when requesting
wss://127.0.0.1/wss/
- in any way.
Well, back to the first option and two ports with confirmation of certificates.
In Node.JS, the WSS server is created on the basis of the HTTPS server and is able to give some content on a direct request.
127.0.0.1:4433
127.0.0.1:4433
, which I decided to use:
var ws = require('ws'); var https = require('https'); var fs = require('fs'); var processRequest = function( req, res ) { res.statusCode = 200; res.end('Hello, world!' + "\n"); }; var options = { key: fs.readFileSync(PathToKey), cert: fs.readFileSync(PathToCert) }; var cometApp = https.createServer(options, processRequest).listen(4433, function() { console.log('Comet server [WSS] on *:4433'); }); var cometServerOptions = { server: cometApp, verifyClient: function(request) {
For fun, I decided to try an iframe. A warning is issued in the iframe ... And there are no buttons for the user to confirm the certificate. And in any case, I can’t track it in any way whether the user confirmed there or not, in order to establish a connection later. Prescribe in technical requirements, so that the user must go to two addresses? Also somehow ugly ... AJAX-request to another port at the same address does not work at all due to internal security - for Javascript these are two different sites.
Solution 2
It turned out somewhat delusional, but very working. So, we need to confirm the certificate twice. But what if we ask the user to first go to the HTTPS interface of the WSS server, and then after confirmation we redirect to the main WebUI?
var ws = require('ws'); var https = require('https'); var fs = require('fs'); var processRequest = function( req, res ) { res.setHeader("Location", redirectUrl); res.statusCode = 302; res.end(); }; var options = { key: fs.readFileSync(PathToKey), cert: fs.readFileSync(PathToCert) }; var cometApp = https.createServer(options, processRequest).listen(4433, function() { console.log('Comet server [WSS] on *:4433'); }); var cometServerOptions = { server: cometApp, verifyClient: function(request) {
Works! Upon request
127.0.0.1:4433/
127.0.0.1:4433/
now asks me to confirm the certificate, then automatically uploads to the Web UI
127.0.0.1
127.0.0.1
, where it also asks to confirm the certificate, after which the HTTPS bundle on one + WSS on the other port works fine and does not ask anything else.
NB! Above, I did not write about domain names. Of course, they can be used to get normal certificates, but I have certain limitations in the form of a corporate network, from which there is not always access to the certificate authority that issued the certificate. Yes, and to require domain names on the internal network for production in my case is an extra headache. Another server for the Web UI on Linux, for example, with Nginx proxy for various reasons, we also do not consider.
findings
To solve this problem, there were two ways:
- Proxying from a direct address on the part of the url (using Apache or Nginx)
- Redirect from the HTTPS interface of the WS server to the main Web UI with certificate confirmation two times
I would welcome any comments, maybe there are some other solutions.
UPDATE
for Node.JS, after all, it turned out to make WS requests proxying (I took
node-http-proxy ) in terms of URL (thanks to
xdenser ) by means of Node.JS itself to the Upgrade event, and it is optimal to add proxying directly to Web UI http-server so that do not produce components like this:
var httpProxy = require('http-proxy'); var proxy = httpProxy.createProxyServer({}); function proxyWebsocketResponse(req, res, head) { try { var hostname = req.headers.host.split(":")[0]; var pathname = url.parse(req.url).pathname; if (pathname === config.comet.websocket.path) { var options = { target: 'ws://' + config.comet.websocket.host + ':' + config.comet.websocket.port + '/', ws: true }; proxy.ws(req, res, head, options); proxy.on('error', function(e) { console.log('WebSocket error: ' + e.message); res.end(); }); } else { res.statusCode = 501; res.end('Not Implemented'); } } catch (e) { console.log('Error: ' + e.message); } } httpServer.addListener('upgrade', proxyWebsocketResponse);