I am trying to create a middleware for Express.js to redirect all non-secure (port 80) traffic to the secured SSL port (443). Unfortunately there is no information in an Exp
Since I was working on nginx, I had access to the header's x-forwarded-proto property so I could write a tiny middleware to redirect all traffic as described here: http://elias.kg/post/14971446990/force-ssl-with-express-js-on-heroku-nginx
Edit: Updated the url
First, let me see if I can clarify the problem. You are limited to one (1) node.js process, but that process can listen on two (2) network ports, both 80 and 443, right? (When you say one server it's not clear if you mean one process or only one network port).
Given that constraint, you problem seems to be, for reasons you don't provide, somehow your clients are connecting to the wrong port. This is a bizarre edge case because by default, clients will make HTTP requests to port 80 and HTTPS to port 443. And when I say "by default", I mean if no specific ports are included in the URLs. So unless you are explicitly using criss-crossed URLs like http://example.com:443 and https://example.com:80, you really shouldn't have any criss-crossed traffic hitting your site. But since you asked the question, I guess you must have it, although I bet you are using non-standard ports as opposed to the 80/443 defaults.
So, for background: YES some web servers handle this reasonably well. For example, if you do http://example.com:443 to nginx, it will respond with an HTTP 400 "Bad Request" response indicating "The plain HTTP request was sent to HTTPS port". YES, you can listen on both 80 and 443 from the same node.js process. You just need to create 2 separate instances of express.createServer()
, so that's no problem. Here's a simple program to demonstrate handling both protocols.
var fs = require("fs");
var express = require("express");
var http = express.createServer();
var httpsOptions = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
var https = express.createServer(httpsOptions);
http.all('*', function(req, res) {
console.log("HTTP: " + req.url);
return res.redirect("https://" + req.headers["host"] + req.url);
});
http.error(function(error, req, res, next) {
return console.log("HTTP error " + error + ", " + req.url);
});
https.error(function(error, req, res, next) {
return console.log("HTTPS error " + error + ", " + req.url);
});
https.all('*', function(req, res) {
console.log("HTTPS: " + req.url);
return res.send("Hello, World!");
});
http.listen(80);
And I can test this via cURL like this:
$ curl --include --silent http://localhost/foo
HTTP/1.1 302 Moved Temporarily
X-Powered-By: Express
Content-Type: text/html
Location: https://localhost/foo
Connection: keep-alive
Transfer-Encoding: chunked
<p>Moved Temporarily. Redirecting to <a href="https://localhost/foo">https://localhost/foo</a></p>
$ curl --include --silent --insecure https://localhost:443/foo
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 13
Connection: keep-alive
Hello, World!%
And showing the redirect from HTTP to HTTPS...
curl --include --silent --location --insecure 'http://localhost/foo?bar=bux'
HTTP/1.1 302 Moved Temporarily
X-Powered-By: Express
Content-Type: text/html
Location: https://localhost/foo?bar=bux
Connection: keep-alive
Transfer-Encoding: chunked
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 13
Connection: keep-alive
Hello, World!%
So that will work to serve both protocols for the regular case and redirect properly. However, criss-crosses don't work at all. I believe a criss-crossed request hitting an express server isn't going to get routed through the middleware stack because it will be treated as an error from the very beginning and won't even get the request URI parsed properly, which is necessary to send it through the route middleware chain. The express stack doesn't even get them I think because they are not valid requests, so they get ignored somewhere in the node TCP stack. It's probably possible to write a server to do this, and there may already be a module out there, but you'd have to write it at the TCP layer directly. And you'd have to detect a regular HTTP request in the first chunk of client data that hits the TCP port and wire that connection to an HTTP server instead of your normal TLS handshake.
When I do either of these, my express error handlers do NOT get called.
curl --insecure https://localhost:80/foo
curl: (35) Unknown SSL protocol error in connection to localhost:80
curl http://localhost:443/foo
curl: (52) Empty reply from server