Compare commits

...

3 Commits

Author SHA1 Message Date
AJ ONeal c10a310c20 doc and formatting updates 2020-04-26 17:40:00 -06:00
AJ ONeal 0e5fff5c4a make Prettier 2020-04-26 17:31:48 -06:00
AJ ONeal 16251a99b9 adjust spacing to be easier to read from curl output 2020-04-26 17:31:25 -06:00
4 changed files with 159 additions and 139 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,14 +17,16 @@ 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();
app.use('/', require('redirect-https')({ var redirector = require("redirect-https")({
body: '<!-- Hello Mr Developer! Please use HTTPS instead -->' body: "<!-- Hello Developer! Please use HTTPS instead: {{ URL }} -->"
})); });
app.use("/", redirector);
module.exports = app; module.exports = app;
``` ```
@ -40,10 +42,37 @@ 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.
* 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) - `{{ 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 `{{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) - `{{ 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)
- `{{ 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
@ -51,40 +80,16 @@ 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: [ {
{ match: '/' paths: [
, redirect: 301 { match: "/", 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.
@ -108,6 +113,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.pplwink.com:8080/"><script>alert('hi')</script>` - `http://localhost.rootprojects.org:8080/"><script>alert('hi')</script>`
* `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` - `http://localhost.rootprojects.org:8080/;URL=http://example.com`

View File

@ -1,7 +1,7 @@
'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 = {};
@ -13,40 +13,41 @@ module.exports = function (opts) {
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 = "<!-- Hello Developer Person! We don't serve insecure resources around here." opts.body =
+ "\n Please use HTTPS instead. -->"; "<!-- Hello Developer Person! We don't serve insecure resources around here." +
"\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 (req.connection.encrypted if (
|| 'https' === req.protocol req.connection.encrypted ||
|| (opts.trustProxy && 'https' === req.headers['x-forwarded-proto']) "https" === req.protocol ||
(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 = (0 === (req.headers['user-agent']||'').indexOf('Mozilla/')); var probablyBrowser =
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 encodedLocation = encodeURI(newLocation);
var escapedLocation = escapeHtml(newLocation); var escapedLocation = escapeHtml(newLocation);
var decodedLocation; var decodedLocation;
@ -57,29 +58,29 @@ module.exports = function (opts) {
} }
var body = opts.body var body = opts.body
.replace(/{{\s*HTML_URL\s*}}/ig, escapeHtml(decodedLocation)) .replace(/{{\s*HTML_URL\s*}}/gi, escapeHtml(decodedLocation))
.replace(/{{\s*URL\s*}}/ig, escapedLocation) .replace(/{{\s*URL\s*}}/gi, escapedLocation)
.replace(/{{\s*UNSAFE_URL\s*}}/ig, newLocation) .replace(/{{\s*UNSAFE_URL\s*}}/gi, newLocation);
; var metaRedirect =
"" +
var metaRedirect = '' "<html>" +
+ '<html>\n' '\n<head><META http-equiv="refresh" content="0;URL=\'' +
+ '<head>\n' escapedLocation +
//+ ' <style>* { background-color: white; color: white; text-decoration: none; }</style>\n' "'\"></head>" +
+ ' <META http-equiv="refresh" content="0;URL=\'' + escapedLocation + '\'">\n' "\n<body>" +
+ '</head>\n' body +
+ '<body>\n' + body + '\n</body>\n' "</body>" +
+ '</html>\n' "\n</html>\n";
;
var pathMatch; var pathMatch;
opts.paths.some(function (p) { opts.paths.some(function (p) {
if (!p.match) { if (!p.match) {
// ignore // ignore
} else if ('string' === typeof p.match) { } else if ("string" === typeof p.match) {
pathMatch = (url === p.match) && (p.redirect || 301); pathMatch = url === p.match && (p.redirect || 301);
} else { } else {
pathMatch = p.match.test && p.match.test(url) && (p.redirect || 301); pathMatch =
p.match.test && p.match.test(url) && (p.redirect || 301);
} }
if (pathMatch) { if (pathMatch) {
redirect = pathMatch; redirect = pathMatch;
@ -89,9 +90,9 @@ module.exports = function (opts) {
// If it's not a non-0 number (because null is 0) then 'meta' is assumed. // If it's not a non-0 number (because null is 0) then 'meta' is assumed.
if (redirect && isFinite(redirect)) { if (redirect && isFinite(redirect)) {
res.statusCode = redirect; res.statusCode = redirect;
res.setHeader('Location', newLocation); res.setHeader("Location", newLocation);
} }
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(metaRedirect); res.end(metaRedirect);
}; };
}; };

13
package-lock.json generated Normal file
View File

@ -0,0 +1,13 @@
{
"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.0", "version": "1.3.1",
"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": "git+https://git.coolaj86.com/coolaj86/redirect-https.js.git" "url": "https://git.coolaj86.com/coolaj86/redirect-https.js.git"
}, },
"keywords": [ "keywords": [
"https", "https",
@ -27,5 +27,6 @@
"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": {}
} }