Search code examples
node.jshttpsocket.iocorsprivate-network-access

Cross Origin issue when remote HTML tries to connect to local hosted server


Let me start by saying that I know that what I'm trying to do, is a bit unorthodox, but I was wondering if any of your brilliant minds could help me workaround this.

We have an old system, where the HTML is hosted on a remote server which is connecting to a local server with [email protected] (I know it's old) using http (because the server needs to be hosted on the local machine and self-signed SSLs are not working very well).

What we used to do is to disable "Block insecure private network requests" in the chrome flags chrome://flags/#block-insecure-private-network-requests, but this seems to have gone away now which left us getting a CORS error Access to XMLHttpRequest at 'http://localhost/socket.io/?EIO=3&transport=polling&t=OLTDEpr' from origin 'http://remote.com' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space `local`.

enter image description here

I have tried stuff like the bits suggested in this post Socket.io + Node.js Cross-Origin Request Blocked but none of them is working.

I stripped down the system to the absolute basics so I can try to find a workaroud, but I haven't manage to do so. At the end of the post I have copied a link to the code of both these two servers.

LOCAL SERVER

const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require("socket.io")(http);
io.origins('*:*');


app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", '*');
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

io.on("connection", function(socket){
    console.log("A new connected, with socketid " + socket.id);
});

http.listen(80, function(){
    console.log("start");
});

REMOTE SERVER

const express = require('express');
const app = express();
const http = require('http').Server(app);

app.use(express.static('public'));
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", '*');
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

app.get('/*', function(req, res){
    res.render('home');
});

http.listen(80, function(){
    console.log("Server started");
});

REMOTE HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./js/site.js"></script>
</head>
<body>
    
    <script>
        Test.Init("http://localhost");
    </script>
</body>
</html>

REMOTE JS


/*!
 * Socket.IO v2.1.0
 * (c) 2014-2018 Guillermo Rauch
 * Released under the MIT License.
 */
/// I HAVE A COPY OF THAT Socket.IO CLIENT CODE IN HERE. ///

const Test = function(){

    const Init = (url) => {
        const socket = io(url);
        socket.on('connect', function(){
            console.log("Successfully connected!");
        });
    }

    return {
        Init
    }
}();

Here's the zipped code too if you want to experiment. https://1drv.ms/u/s!AkpIm5Ify5v9pqh8Ox6-32GoT2Hecw?e=l6lHeC

Many thanks, and happy new year to all :)


Solution

  • If your Chrome browser already enforces the newish "private network access" protocol, it will generate preflight requests, to which your local server must respond with the header

    Access-Control-Allow-Private-Network: true
    

    This could be achieved with the following middleware before the CORS handling middleware you already have:

    app.options(function(req, res, next) {
      if (req.get("Origin") === "https://<your remote server>" &&
          req.get("Access-Control-Request-Private-Network"))
        res.set("Access-Control-Allow-Private-Network", "true");
      next();
    });
    app.use(function(req, res, next) {
      res.header("Access-Control-Allow-Origin", '*');
      res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
      next();
    });
    

    Note private network access is only allowed if the remote server is on HTTPS, and this requires the local server to be on HTTPS as well (to avoid "mixed content"). To enable HTTPS for a local IP, you can generate a server certificate for this IP address with the following steps:

    Step 1. Generate a self-signed CA certificate, which must be uploaded to the browser as "trusted root certificate":

    openssl req -new -subj /CN=YourCAName -x509 -key cakey.pem -out cacert.pem
    

    (You must specify a passphrase to protect the CA private key.)

    Step 2. Generate a server certificate for your local server and have it signed by your CA certificate:

    echo subjectAltName = IP:xx.xx.xx.xx > IP.ext
    openssl req -new -subj /CN=YourSubjectName \
    | openssl x509 -req -CA cacert.pem -CAkey cakey.pem -extfile IP.ext
    

    where xx.xx.xx.xx is your local IP address. (You must specify a passphrase for the server private key and repeat the passphrase for the CA private key.)

    Step 3. Use the server certificate and the server private key (together with its passphrase) in the TLS options of your local Node.js HTTPS server.

    See also How to create .pem files for https web server.