Compare commits

..

No commits in common. "c10a310c20f0db1cc453b64183aa65dc517b6b3c" and "7375a550eb936814a17f1db154a315579b28afad" have entirely different histories.

4 changed files with 138 additions and 158 deletions

View File

@ -2,11 +2,11 @@
Secure-by-default redirects from HTTP to HTTPS. Secure-by-default redirects from HTTP to HTTPS.
- Browsers get a 301 + Location redirect * Browsers get a 301 + Location redirect
- Only developers, bots, and APIs see security warning (advising to use HTTPS) * Only developers, bots, and APIs see security warning (advising to use HTTPS)
- Always uses meta redirect as a fallback, for everyone * Always uses meta redirect as a fallback, for everyone
- '/' always gets a 301 (for `curl | bash` installers) * '/' always gets a 301 (for `curl | bash` installers)
- minimally configurable, don't get fancy * minimally configurable, don't get fancy
See <https://coolaj86.com/articles/secure-your-redirects/> See <https://coolaj86.com/articles/secure-your-redirects/>
@ -17,16 +17,14 @@ npm install --save redirect-https
``` ```
```js ```js
"use strict"; 'use strict';
var express = require("express"); var express = require('express');
var app = express(); var app = express();
var redirector = require("redirect-https")({ app.use('/', require('redirect-https')({
body: "<!-- Hello Developer! Please use HTTPS instead: {{ URL }} -->" body: '<!-- Hello Mr Developer! Please use HTTPS instead -->'
}); }));
app.use("/", redirector);
module.exports = app; module.exports = app;
``` ```
@ -42,37 +40,10 @@ module.exports = app;
} }
``` ```
- This module will call `next()` if the connection is already tls / https. * This module will call `next()` if the connection is already tls / https.
- If `trustProxy` is true, and `X-Forward-Proto` is https, `next()` will be called. * If `trustProxy` is true, and `X-Forward-Proto` is https, `next()` will be called.
- `{{ URL }}` in the body text will be replaced with a URI encoded and HTML escaped url (it'll look just like it is) * If you use `{{URL}}` in the body text it will be replaced with a URI encoded and HTML escaped url (it'll look just like it is)
- `{{ HTML_URL }}` in the body text will be replaced with a URI decoded and HTML escaped url (it'll look just like it would in Chrome's URL bar) * If you use `{{HTML_URL}}` in the body text it will be replaced with a URI decoded and HTML escaped url (it'll look just like it would in Chrome's URL bar)
- `{{ UNSAFE_URL }}` is the raw, original url
## Demo
```javascript
"use strict";
var http = require("http");
var server = http.createServer();
var securePort = process.argv[2] || 8443;
var insecurePort = process.argv[3] || 8080;
var redirector = require("redirect-https")({
port: securePort,
body: "<!-- Hello! Please use HTTPS instead: {{ URL }} -->",
trustProxy: true // default is false
});
server.on("request", redirector);
server.listen(insecurePort, function () {
console.log(
"Listening on http://localhost.rootprojects.org:" +
server.address().port
);
});
```
## Advanced Options ## Advanced Options
@ -80,16 +51,40 @@ For the sake of `curl | bash` installers and the like there is also the option t
to get a certain redirect for an exact path match: to get a certain redirect for an exact path match:
```js ```js
{ { paths: [
paths: [ { match: '/'
{ match: "/", redirect: 301 }, , redirect: 301
{ match: /^\/$/, redirect: 301 } }
]; , { match: /^\/$/
, redirect: 301
}
]
} }
``` ```
If you're using this, you're probably getting too fancy (but hey, I get too fancy sometimes too). If you're using this, you're probably getting too fancy (but hey, I get too fancy sometimes too).
## Demo
```javascript
'use strict';
var http = require('http');
var server = http.createServer();
var securePort = process.argv[2] || 8443;
var insecurePort = process.argv[3] || 8080;
server.on('request', require('redirect-https')({
port: securePort
, body: '<!-- Hello! Please use HTTPS instead -->'
, trustProxy: true // default is false
}));
server.listen(insecurePort, function () {
console.log('Listening on http://localhost.pplwink.com:' + server.address().port);
});
```
# Meta redirect by default, but why? # Meta redirect by default, but why?
When something is broken (i.e. insecure), you don't want it to kinda work, you want developers to notice. When something is broken (i.e. insecure), you don't want it to kinda work, you want developers to notice.
@ -113,6 +108,6 @@ If your application is properly separated between static assets and api, then it
The incoming URL is already URI encoded by the browser but, just in case, I run an html escape on it The incoming URL is already URI encoded by the browser but, just in case, I run an html escape on it
so that no malicious links of this sort will yield unexpected behavior: so that no malicious links of this sort will yield unexpected behavior:
- `http://localhost.rootprojects.org:8080/"><script>alert('hi')</script>` * `http://localhost.pplwink.com:8080/"><script>alert('hi')</script>`
- `http://localhost.rootprojects.org:8080/';URL=http://example.com` * `http://localhost.pplwink.com:8080/';URL=http://example.com`
- `http://localhost.rootprojects.org:8080/;URL=http://example.com` * `http://localhost.pplwink.com:8080/;URL=http://example.com`

179
index.js
View File

@ -1,98 +1,97 @@
"use strict"; 'use strict';
module.exports = function (opts) { module.exports = function (opts) {
var escapeHtml = require("escape-html"); var escapeHtml = require('escape-html');
if (!opts) { if (!opts) {
opts = {}; opts = {};
} }
if (!isFinite(opts.port)) { if (!isFinite(opts.port)) {
opts.port = 443; opts.port = 443;
} }
if (!opts.browsers) { if (!opts.browsers) {
opts.browsers = 301; opts.browsers = 301;
} }
if (!opts.apis) { if (!opts.apis) {
opts.apis = "meta"; opts.apis = 'meta';
} }
if (!Array.isArray(opts.paths)) { if (!Array.isArray(opts.paths)) {
opts.paths = [{ match: "/" }]; opts.paths = [ { match: '/' } ];
} }
if (!("body" in opts)) { if (!('body' in opts)) {
opts.body = opts.body = "<!-- Hello Developer Person! We don't serve insecure resources around here."
"<!-- Hello Developer Person! We don't serve insecure resources around here." + + "\n Please use HTTPS instead. -->";
"\n Please use HTTPS instead. -->"; }
} opts.body = opts.body.replace(/{{\s+PORT\s+}}/ig, opts.port);
opts.body = opts.body.replace(/{{\s+PORT\s+}}/gi, opts.port);
return function (req, res, next) { return function (req, res, next) {
if ( if (req.connection.encrypted
req.connection.encrypted || || 'https' === req.protocol
"https" === req.protocol || || (opts.trustProxy && 'https' === req.headers['x-forwarded-proto'])
(opts.trustProxy && "https" === req.headers["x-forwarded-proto"]) ) {
) { next();
next(); return;
return; }
}
var url = req.originalUrl || req.url; var url = (req.originalUrl || req.url);
// We don't want chrome showing the "Not Secure" badge during the redirect. // We don't want chrome showing the "Not Secure" badge during the redirect.
var probablyBrowser = var probablyBrowser = (0 === (req.headers['user-agent']||'').indexOf('Mozilla/'));
0 === (req.headers["user-agent"] || "").indexOf("Mozilla/"); // But we don't want devs, APIs, or Bots to accidentally browse insecure.
// But we don't want devs, APIs, or Bots to accidentally browse insecure. var redirect = probablyBrowser ? opts.browsers : opts.apis;
var redirect = probablyBrowser ? opts.browsers : opts.apis; var host = req.headers.host || '';
var host = req.headers.host || ""; if (!/:\d+/.test(host) && 443 !== opts.port) {
if (!/:\d+/.test(host) && 443 !== opts.port) { // we are using standard port 80, but we aren't using standard port 443
// we are using standard port 80, but we aren't using standard port 443 host += ':80';
host += ":80"; }
} var newLocation = 'https://'
var newLocation = + host.replace(/:\d+/, ':' + opts.port) + url
"https://" + host.replace(/:\d+/, ":" + opts.port) + url; ;
//var encodedLocation = encodeURI(newLocation);
var escapedLocation = escapeHtml(newLocation);
var decodedLocation;
try {
decodedLocation = decodeURIComponent(newLocation);
} catch (e) {
decodedLocation = newLocation; // "#/error/?error_message=" + e.toString();
}
var body = opts.body //var encodedLocation = encodeURI(newLocation);
.replace(/{{\s*HTML_URL\s*}}/gi, escapeHtml(decodedLocation)) var escapedLocation = escapeHtml(newLocation);
.replace(/{{\s*URL\s*}}/gi, escapedLocation) var decodedLocation;
.replace(/{{\s*UNSAFE_URL\s*}}/gi, newLocation); try {
var metaRedirect = decodedLocation = decodeURIComponent(newLocation);
"" + } catch(e) {
"<html>" + decodedLocation = newLocation; // "#/error/?error_message=" + e.toString();
'\n<head><META http-equiv="refresh" content="0;URL=\'' + }
escapedLocation +
"'\"></head>" +
"\n<body>" +
body +
"</body>" +
"\n</html>\n";
var pathMatch;
opts.paths.some(function (p) { var body = opts.body
if (!p.match) { .replace(/{{\s*HTML_URL\s*}}/ig, escapeHtml(decodedLocation))
// ignore .replace(/{{\s*URL\s*}}/ig, escapedLocation)
} else if ("string" === typeof p.match) { .replace(/{{\s*UNSAFE_URL\s*}}/ig, newLocation)
pathMatch = url === p.match && (p.redirect || 301); ;
} else {
pathMatch = var metaRedirect = ''
p.match.test && p.match.test(url) && (p.redirect || 301); + '<html>\n'
} + '<head>\n'
if (pathMatch) { //+ ' <style>* { background-color: white; color: white; text-decoration: none; }</style>\n'
redirect = pathMatch; + ' <META http-equiv="refresh" content="0;URL=\'' + escapedLocation + '\'">\n'
} + '</head>\n'
return pathMatch; + '<body>\n' + body + '\n</body>\n'
}); + '</html>\n'
// If it's not a non-0 number (because null is 0) then 'meta' is assumed. ;
if (redirect && isFinite(redirect)) { var pathMatch;
res.statusCode = redirect;
res.setHeader("Location", newLocation); opts.paths.some(function (p) {
} if (!p.match) {
res.setHeader("Content-Type", "text/html; charset=utf-8"); // ignore
res.end(metaRedirect); } else if ('string' === typeof p.match) {
}; pathMatch = (url === p.match) && (p.redirect || 301);
} else {
pathMatch = p.match.test && p.match.test(url) && (p.redirect || 301);
}
if (pathMatch) {
redirect = pathMatch;
}
return pathMatch;
});
// If it's not a non-0 number (because null is 0) then 'meta' is assumed.
if (redirect && isFinite(redirect)) {
res.statusCode = redirect;
res.setHeader('Location', newLocation);
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(metaRedirect);
};
}; };

13
package-lock.json generated
View File

@ -1,13 +0,0 @@
{
"name": "redirect-https",
"version": "1.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "redirect-https", "name": "redirect-https",
"version": "1.3.1", "version": "1.3.0",
"description": "Redirect from HTTP to HTTPS using meta redirects", "description": "Redirect from HTTP to HTTPS using meta redirects",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -8,7 +8,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.coolaj86.com/coolaj86/redirect-https.js.git" "url": "git+https://git.coolaj86.com/coolaj86/redirect-https.js.git"
}, },
"keywords": [ "keywords": [
"https", "https",
@ -27,6 +27,5 @@
"homepage": "https://git.coolaj86.com/coolaj86/redirect-https.js#readme", "homepage": "https://git.coolaj86.com/coolaj86/redirect-https.js#readme",
"dependencies": { "dependencies": {
"escape-html": "^1.0.3" "escape-html": "^1.0.3"
}, }
"devDependencies": {}
} }