4

I'm unable to make websockets work on a node backend using an apache proxy through HTTPS to connect to the node instance. Websockets are working properly if no (apache) http(s) proxy is used.

My setup: I have an apache server with multiple virtual hosts. I have a HTTPS webpage for myserver.com and the HTTPS API with node/express/ws in api.myserver.com subdomain through the proxy, that redirects the requests to the node.js instance (multiple instances on PM2) running on port 3333.

This is my apache virtual host for the subdomain:

<VirtualHost *:443>
    ServerName api.myserver.com
    ServerAdmin hello@myserver.com
    DocumentRoot /var/www/html/myserver/api
        Options -Indexes

    SSLEngine on                                                                
    SSLProtocol all -SSLv2                                                      
    SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM                

    SSLCertificateFile /etc/apache2/certs/STAR_myserver_co.crt
    SSLCertificateKeyFile /etc/apache2/certs/myserver_private_key.pem
    SSLCertificateChainFile /etc/apache2/certs/STAR_myserver_co.ca-bundle

    SSLProxyEngine On
    ProxyPreserveHost On
    ProxyRequests Off

    # This is for websocket requests
    ProxyPass /wss/ wss://localhost:3333/
    ProxyPassReverse /wss/ wss://localhost:3333/

    # This is for normal requests    
    ProxyPass / https://localhost:3333/
    ProxyPassReverse / https://localhost:3333/
</VirtualHost>

This works OK for redirecting the connections to the node express backend. I have installed mod_proxy, mod_proxy_http and mod_proxy_wstunnel.

This is the node.js API backend: first, I initialize express, sessions, etc.

// express, session and mongodb session storage
var express = require('express')
var session = require('express-session')
var MongoStore = require('connect-mongo')(session)
var app = express()
// configure sessionStore and sessions, mongodb, etc...

// Certificates and credentials for HTTPS server
var fs = require('fs')
var privateKey  = fs.readFileSync(__dirname + '/certs/myserver_private_key.pem', 'utf8')
var certificate = fs.readFileSync(__dirname + '/certs/myserver_cert.pem', 'utf8')
var ca = fs.readFileSync(__dirname + '/certs/myserver_ca.pem', 'utf8')
var credentials = {key: privateKey, cert: certificate, ca: ca}

app.enable('trust proxy')
app.set("trust proxy", 1)

And then I setup the HTTPS server securely, using the same certificates that in APACHE:

// Setup HTTPS server
var https = require('https')
var server = https.createServer(credentials, app)
server.listen(appPort, 'localhost', function () {
    // Server up and running!
    var host = server.address().address
    var port = server.address().port
    console.log('myserver listening at https://%s:%s', host, port)
})

Last, I setup the websocket connections:

// setup Websockets
wss = new WebSocketServer({ server: server })
wss.on('connection', function connection(ws) {
    var cookies = cookie.parse(ws.upgradeReq.headers.cookie)
    var sid = cookieParser.signedCookie(cookies["connect.sid"], myserver_secret)
    // extract user credentials and data from cookie/sid,

    // get the session object
    sessionStore.get(sid, function (err, ss) {
        ...
    })
})

Then my clients just try to connect to websockets securely (because, being a HTTPS app, I cannot use the ws:// insecure websockets connection):

window.WebSocket = window.WebSocket || window.MozWebSocket
webSocket = new WebSocket('wss://' + location.host + '/wss')

And then I get always the same error 302:

[Error] WebSocket connection to 'wss://api.myserver.com/wss' failed: Unexpected response code: 302

If I test on a local server directly to the node instance https://localhost:3333/ it's working perfectly and websockets work as they should.

Any idea of how to solve this? Is there a problem with ws redirections made by Apache proxy modules?

1 Answers1

1

You've already set up HTTPS on Apache so you've got a secure connection from the client to your physical server. You're proxying the connection from Apache to your node.js app on localhost, so the only possible eavesdroppers are physically on your box already. You've told your app to trust the proxy with app.enable('trust proxy'); app.set("trust proxy", 1);

So why would you also configure HTTPS for node.js? One of the main reasons we front-end our web apps with apache/nginx/haproxy is TLS offloading.

In addition to getting the X-Forwarded-For header with the remote clients IP, enabling proxy trust also gets you X-Forwarded-Proto so your Express app knows if the connection was http or https. Therefore, the https layer between Apache and node is doing little besides introducing overhead for your app.

I can't help you with proxying WebSockets with Apache. I started using Apache back when it was still a series of patches for httpd. I quit before WebSockets rolled around and I can't risk falling off the wagon. Too many painful memories. If you're curious, here's how I use haproxy to pass WebSockets to one of my express apps:

mode      http
option    forwardfor
frontend http-in
bind :::80 v4v6
bind :::443 v4v6 ssl crt /etc/ssl/private
http-request  set-header X-Forwarded-Proto https if { ssl_fc }
http-request  set-header X-Forwarded-Port %[dst_port]
acl is_websocket hdr(Upgrade) -i WebSocket
acl is_websocket hdr_beg(Host) -i ws

redirect scheme https code 301 if !is_websocket !{ ssl_fc }
use_backend websocket_server if  is_websocket

backend websocket_server
timeout queue 5s
timeout server 86400s
timeout connect 86400s
server node_app 127.0.0.1:8080

backend ...others...
Matt Simerson
  • 419
  • 3
  • 9