diff --git a/lib/hostname-redirects.js b/lib/hostname-redirects.js new file mode 100644 index 0000000..5053d2e --- /dev/null +++ b/lib/hostname-redirects.js @@ -0,0 +1,123 @@ +'use strict'; + +// TODO detect infinite redirects + +module.exports.compile = module.exports.sortOpts = function (opts) { + var redirects = opts.redirects; + var dups = {}; + var results = { + conflicts: {} + , patterns: [] + , matchesMap: {} + }; + + redirects.forEach(function (r) { + var bare; + var www; + + if ('.' === r.id[0]) { + // for consistency + // TODO this should happen at the database level + r.id = '*' + r.id; + } + if ('*' === r.id[0]) { + // TODO check that we are not trying to redirect a tld (.com, .co.uk, .org, etc) + // tlds should follow the global policy + if (r.id[1] && '.' !== r.id[1]) { + // this is not a good place to throw as the consequences of a bug would be + // very bad, but errors should never be silent, so we'll compromise + console.warn("[NON-FATAL ERROR]: ignoring redirect pattern '" + r.id + "'"); + results.conflicts[r.id] = r; + } + + // nix the '*' for easier matching + r.id = r.id.slice(1); + if (!r.id) { + r.id = '*'; + } + if (dups[r.id]) { + results.conflicts[r.id] = r; + console.warn("[NON-FATAL ERROR]: duplicate entry for redirect pattern '" + r.id + "'"); + } + dups[r.id] = true; + results.patterns.push(r); + return; + } + + bare = r.id.replace(/^www\./i, ''); + www = r.id.replace(/^(www\.)?/i, 'www.'); + + if (true === r.value) { + // implicit add www + results.matchesMap[bare] = www; + results.matchesMap[www] = www; + } else if (false === r.value) { + // implicit remove www + results.matchesMap[bare] = bare; + results.matchesMap[www] = bare; + } else if (!r.value) { + // (null, '', 0, undefined) + // explicitly no change + results.matchesMap[r.id] = r.id; + } else { + // explicit value + results.matchesMap[r.id] = r.value; + } + }); + + results.patterns.sort(function (a, b) { + return b.id.length - a.id.length; + }); + + return results; +}; + +module.exports.redirectTo = function (hostname, opts) { + var redir = opts.matchesMap[hostname]; + + if (redir) { + if (redir === hostname) { + return false; + } + return redir; + } + + // longest to shortest + var hasWww = ('www.' === hostname.slice(0, 4)); + //var noWww = (hasWww && hostname.slice(4)) || hostname; + //var yesWww = (hasWww && hostname) || ('www.' + hostname); + + redir = false; + opts.patterns.some(function (r) { + // r.id begins with a dot, such as '.foo.example.com' + if (r.id !== hostname.slice(hostname.length - r.id.length)) { + // except for the default, which is an * + if ('*' !== r.id) { + return false; + } + } + + if (true === r.value) { + // implicit add www + redir = hasWww ? hostname : ('www.' + hostname); + } else if (false === r.value) { + // implicit remove www + redir = hasWww ? hostname.slice(4) : hostname; + } else if (!r.value) { + // (null, '', 0, undefined) + // explicitly no change + redir = false; + } else { + // explicit value + redir = r.value; + } + + return true; + }); + + if (redir === hostname) { + return false; + } + + return redir; +}; diff --git a/lib/no-www.js b/lib/no-www.js index 8237483..f71e3ec 100644 --- a/lib/no-www.js +++ b/lib/no-www.js @@ -1,14 +1,26 @@ -module.exports.scrubTheDub = function (req, res) { +'use strict'; + +module.exports.scrubTheDub = function (req, res, redirectives) { // hack for bricked app-cache // Also 301 redirects will not work for appcache (must issue html) if (require('./unbrick-appcache').unbrick(req, res)) { - return; + return true; } // TODO port number for non-443 var escapeHtml = require('escape-html'); - var newLocation = 'https://' + req.hostname.replace(/^www\./, '') + req.url; - var safeLocation = escapeHtml(newLocation); + var newLocation; + var safeLocation; + + if (redirectives) { + newLocation = require('./hostname-redirects').redirectTo(req.hostname, redirectives); + if (!newLocation) { + return false; + } + } else { + newLocation = 'https://' + req.hostname.replace(/^www\./, '') + req.url; + } + safeLocation = escapeHtml(newLocation); var metaRedirect = '' + '\n' @@ -24,4 +36,6 @@ module.exports.scrubTheDub = function (req, res) { ; res.end(metaRedirect); + + return true; }; diff --git a/lib/pathname-redirects.js b/lib/pathname-redirects.js new file mode 100644 index 0000000..d4e9b6f --- /dev/null +++ b/lib/pathname-redirects.js @@ -0,0 +1,38 @@ + /* + //var escapeRe; + //var insecureRedirects; + if (require('./unbrick-appcache').unbrick(req, res)) { + return; + } + + // because I have domains for which I don't want to pay for SSL certs + insecureRedirects = (redirects||[]).sort(function (a, b) { + var hlen = b.from.hostname.length - a.from.hostname.length; + var plen; + if (!hlen) { + plen = b.from.path.length - a.from.path.length; + return plen; + } + return hlen; + }).forEach(function (redirect) { + var origHost = host; + + if (!escapeRe) { + escapeRe = require('escape-string-regexp'); + } + + // TODO if '*' === hostname[0], omit '^' + host = host.replace( + new RegExp('^' + escapeRe(redirect.from.hostname)) + , redirect.to.hostname + ); + if (host === origHost) { + return; + } + url = url.replace( + new RegExp('^' + escapeRe(redirect.from.path)) + , redirect.to.path + ); + }); + */ + diff --git a/tests/hostname-redirects.js b/tests/hostname-redirects.js new file mode 100644 index 0000000..d4c8e3b --- /dev/null +++ b/tests/hostname-redirects.js @@ -0,0 +1,72 @@ +'use strict'; + +var opts = { + redirects: [ + { "id": "*", "value": true } + , { "id": "ns2.redirect-www.org", "value": false } + , { "id": "hellabit.com", "value": false } + , { "id": "*.hellabit.com", "value": false } + , { "id": "redirect-www.org", "value": null } + , { "id": "www.redirect-www.org", "value": null } + , { "id": "no.redirect-www.org", "value": false } + , { "id": "*.redirect-www.org", "value": false } + , { "id": "*.yes.redirect-www.org", "value": true } + , { "id": "yes.redirect-www.org", "value": true } + , { "id": "*.maybe.redirect-www.org", "value": null } + , { "id": "maybe.redirect-www.org", "value": null } + , { "id": "blog.coolaj86.com", "value": 'coolaj86.com' } // TODO pathname +] +, matchesMap: null +, patternsMap: null +, patterns: null +}; + +var redirectTo = require('../lib/hostname-redirects').redirectTo; +var sortOpts = require('../lib/hostname-redirects').sortOpts; + +var domains = { +// maybewww + 'redirect-www.org': false +, 'www.redirect-www.org': false +, 'maybe.redirect-www.org': false +, 'www.maybe.redirect-www.org': false + +// yeswww +, 'yes.redirect-www.org': 'www.yes.redirect-www.org' +, 'foo.yes.redirect-www.org': 'www.foo.yes.redirect-www.org' + +// nowww +, 'www.no.redirect-www.org': 'no.redirect-www.org' +, 'www.foo.no.redirect-www.org': 'foo.no.redirect-www.org' + +, 'ns2.redirect-www.org': false +, 'www.ns2.redirect-www.org': 'ns2.redirect-www.org' + +, 'ns1.redirect-www.org': false +, 'www.ns1.redirect-www.org': 'ns1.redirect-www.org' + +, 'hellabit.com': false +, 'www.hellabit.com': 'hellabit.com' + +// default policy (yeswww) +, 'ahellabit.com': 'www.ahellabit.com' +, 'www.ahellabit.com': false +, 'example.com': 'www.example.com' +, 'www.example.com': false +}; + +var redirects = sortOpts(opts); + +console.log(redirects); + +Object.keys(domains).forEach(function (domain, i) { + var redir = domains[domain]; + var result = redirectTo(domain, redirects); + + if (redir !== result) { + throw new Error("For domain #" + i + " '" + domain + "' expected '" + redir + "' but got '" + result + "'"); + } +}); + +console.log("Didn't throw any errors. Must have worked, eh?"); +console.log("TODO: detect and report infinite redirects");