From a524622a4e69baa8e44f7b611ac6697095baa751 Mon Sep 17 00:00:00 2001 From: orthagonal Date: Wed, 19 Sep 2018 17:48:25 +0000 Subject: [PATCH 1/4] static redirects --- lib/index.js | 11 +++++------ lib/paramReplacer.js | 4 ++-- test/redirects.test.js | 14 +++++++------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/index.js b/lib/index.js index 578cd0e..296db85 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,24 +24,23 @@ module.exports = async (server, pluginOptions) => { server.event('redirect'); // let them add additional routes: - server.expose('register', async (additionalRoutes) => { + server.expose('register', (additionalRoutes) => { options.redirects = Object.assign({}, options.redirects, additionalRoutes.redirects); options.vhosts = Object.assign({}, options.vhosts, additionalRoutes.vhosts); }); server.ext('onPreResponse', async (request, h) => { - // does not interfere if it's not a 404 error if ((request.response.statusCode && request.response.statusCode !== 404) || (request.response.isBoom && request.response.output.statusCode !== 404)) { return h.continue; } // if it's 404 then look up a redirect, first get dynamic redirects: - const getRedirects = options.getRedirects ? options.getRedirects : async (redirectOptions) => []; + const dynamicRedirects = options.dynamicRedirects ? options.dynamicRedirects : (redirectOptions) => []; - // the plugin options will be passed to your custom 'getRedirects' function: - let dynamicRouteTable + // the plugin options will be passed to your custom 'dynamicRedirects' function: + let dynamicRouteTable; try { - dynamicRouteTable = await getRedirects(options); + dynamicRouteTable = await dynamicRedirects(options); } catch (err) { server.log(['hapi-redirect', 'error'], err); return h.continue; diff --git a/lib/paramReplacer.js b/lib/paramReplacer.js index 219c402..bd4fff2 100644 --- a/lib/paramReplacer.js +++ b/lib/paramReplacer.js @@ -3,8 +3,8 @@ const ParamMatchingRegEx = new RegExp('(\\{)((?:[a-z][a-z0-9_]*))(\\*)(\\d+)(\\} const replaceRouteParamWithValues = (route, paramName, paramValue) => { // replace the most-common types of param: route = route.replace(`{${paramName}}`, paramValue) - .replace(`{${paramName}?}`, paramValue) - .replace(`{${paramName}*}`, paramValue); + .replace(`{${paramName}?}`, paramValue) + .replace(`{${paramName}*}`, paramValue); // match and replaces params of the form "{myParam*2}, {myParam*4}" as well: const matchedValue = ParamMatchingRegEx.exec(route); diff --git a/test/redirects.test.js b/test/redirects.test.js index ed4e901..7c7af83 100644 --- a/test/redirects.test.js +++ b/test/redirects.test.js @@ -14,21 +14,21 @@ lab.experiment('hapi-redirect', () => { { method: 'GET', path: '/it/works', - handler: (request, h) => { + handler(request, h) { return 'redirects totally working'; } }, { method: 'GET', path: '/newtest', - handler: (request, h) => { + handler(request, h) { return 'vhost redirects totally working '; } }, { method: 'GET', path: '/newtest/{param*2}', - handler: (request, h) => { + handler(request, h) { return `redirects totally working and param passed was ${request.params.param}`; } } @@ -420,7 +420,7 @@ lab.experiment('hapi-redirect', () => { redirects: { '/test301': '/it/works', }, - getRedirects: async (pluginOptions) => { + dynamicRedirects: (pluginOptions) => { // dynamic method takes callback from the plugin: Code.expect(pluginOptions.log).to.equal(true); count++; @@ -463,7 +463,7 @@ lab.experiment('hapi-redirect', () => { redirects: { '/test': '/it/works', }, - getRedirects(pluginOptions, redirectDone) { + dynamicRedirects(pluginOptions, redirectDone) { throw new Error('an error'); } } @@ -484,7 +484,7 @@ lab.experiment('hapi-redirect', () => { redirects: { '/test': '/it/works', }, - getRedirects(pluginOptions) { + dynamicRedirects(pluginOptions) { // duplicate will result in a 500: return { '/test': '/newtest' @@ -509,7 +509,7 @@ lab.experiment('hapi-redirect', () => { }, } }); - server.events.on('redirect', async (redirectInfo) => { + server.events.on('redirect', (redirectInfo) => { Code.expect(redirectInfo).to.equal('/it/works'); }); await server.start(); From 51036f1fdded978d83840fb48ca6f2b2fc283c91 Mon Sep 17 00:00:00 2001 From: orthagonal Date: Thu, 20 Sep 2018 02:53:01 +0000 Subject: [PATCH 2/4] static redirects --- lib/index.js | 120 ++++++++++++++++++++--------------------- test/redirects.test.js | 90 +++---------------------------- 2 files changed, 66 insertions(+), 144 deletions(-) diff --git a/lib/index.js b/lib/index.js index 296db85..f03f6c6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,7 +13,7 @@ const defaults = { verbose: false }; -module.exports = async (server, pluginOptions) => { +module.exports = (server, pluginOptions) => { const options = Object.assign({}, defaults, pluginOptions); if (options.statusCode === 'temporary') { options.statusCode = 302; @@ -23,12 +23,64 @@ module.exports = async (server, pluginOptions) => { } server.event('redirect'); + // turn a redirect into a redirection URL and a status code: + const processRedirect = async(request, routeSpec, match = false) => { + // get all the info for doing the redirect from the route spec: + const statusCode = routeSpec.statusCode || options.statusCode; + const routePath = typeof routeSpec === 'string' ? routeSpec : routeSpec.destination; + const route = request.route.path; + const redirectTo = paramReplacer(routePath, match ? match.params : request.params); + const redirectToUrl = parse(redirectTo, true); + // lump all queries together: + const allQueries = Object.assign({}, request.query, redirectToUrl.query); + // if needed, add the queries to the parsed url: + if (Object.keys(allQueries).length > 0) { + redirectToUrl.search = `?${querystring.stringify(allQueries)}`; + } + // let the url parser format the correct redirect Location: + const fullRedirectLocation = redirectToUrl.format(); + + let from = request.path; + if (Object.keys(request.query).length !== 0) { + from = `${from}?${querystring.stringify(request.query)}`; + } + // log the route info: + server.log(['hapi-redirect', 'redirect', 'info'], { + remoteAddress: `${request.info.remoteAddress}:${request.info.remotePort}`, + host: request.info.host, + userAgent: request.headers['user-agent'], + browser: useragent.parse(request.headers['user-agent']).toString(), + referrer: request.info.referrer, + routePath: route, + to: fullRedirectLocation, + from + }); + + // now emit the event and do the redirect: + await server.events.emit('redirect', (request, fullRedirectLocation)); + return { statusCode, fullRedirectLocation }; + }; + // let them add additional routes: server.expose('register', (additionalRoutes) => { - options.redirects = Object.assign({}, options.redirects, additionalRoutes.redirects); - options.vhosts = Object.assign({}, options.vhosts, additionalRoutes.vhosts); + // support both '{ /path: /redirect}' and { redirects : { /path: /redirect} } + additionalRoutes = additionalRoutes.redirects || additionalRoutes; + Object.keys(additionalRoutes).forEach(path => { + server.route({ + method: '*', + path, + async handler(request, h) { + const { statusCode, fullRedirectLocation } = await processRedirect(request, additionalRoutes[path]); + return h.redirect(fullRedirectLocation).code(statusCode); + } + }); + }); }); + if (options.redirects) { + server.plugins['hapi-redirects'].register(options.redirects); + } + server.ext('onPreResponse', async (request, h) => { // does not interfere if it's not a 404 error if ((request.response.statusCode && request.response.statusCode !== 404) || (request.response.isBoom && request.response.output.statusCode !== 404)) { @@ -45,36 +97,13 @@ module.exports = async (server, pluginOptions) => { server.log(['hapi-redirect', 'error'], err); return h.continue; } - // if any routes are duplicates throw an error: - const duplicateRoutes = Object.keys(dynamicRouteTable).reduce((list, key) => { - if (Object.keys(options.redirects).indexOf(key) !== -1) { - list.push(key); - } - return list; - }, []); - if (duplicateRoutes.length !== 0) { - server.log(['hapi-redirect', 'error'], `the following routes are already registered: ${duplicateRoutes}`); - return h.response().code(500).takeover(); - } - - // combine the dynamic and static redirects: - const redirectTable = Object.assign({}, dynamicRouteTable, options.redirects); const router = new Call.Router(); - const route = request.route.path; const path = request.path; // load the routes: - Object.keys(redirectTable).forEach((source) => { + Object.keys(dynamicRouteTable).forEach((source) => { router.add({ method: 'get', path: source }); }); - // also load routes that have a vhost: - if (options.vhosts) { - Object.keys(options.vhosts).forEach((vhost) => { - Object.keys(options.vhosts[vhost]).forEach((source) => { - router.add({ method: 'get', path: source, vhost }); - }); - }); - } // try to match the incoming route: const host = request.headers.host; const match = router.route('get', path, host); @@ -82,41 +111,8 @@ module.exports = async (server, pluginOptions) => { return h.continue; } // get the spec data for the matching route from either vhost or the route table: - const routeSpec = (options.vhosts && options.vhosts[host]) ? - options.vhosts[host][match.route] : redirectTable[match.route]; - // get all the info for doing the redirect from the route spec: - const statusCode = routeSpec.statusCode || options.statusCode; - const routePath = typeof routeSpec === 'string' ? routeSpec : routeSpec.destination; - const redirectTo = paramReplacer(routePath, match.params); - const redirectToUrl = parse(redirectTo, true); - // lump all queries together: - const allQueries = Object.assign({}, request.query, redirectToUrl.query); - // if needed, add the queries to the parsed url: - if (Object.keys(allQueries).length > 0) { - redirectToUrl.search = `?${querystring.stringify(allQueries)}`; - } - // let the url parser format the correct redirect Location: - const fullRedirectLocation = redirectToUrl.format(); - - let from = request.path; - if (Object.keys(request.query).length !== 0) { - from = `${from}?${querystring.stringify(request.query)}`; - } - // log the route info: - server.log(['hapi-redirect', 'redirect', 'info'], { - remoteAddress: `${request.info.remoteAddress}:${request.info.remotePort}`, - host: request.info.host, - userAgent: request.headers['user-agent'], - browser: useragent.parse(request.headers['user-agent']).toString(), - referrer: request.info.referrer, - routePath: route, - to: fullRedirectLocation, - from - }); - - // now emit the event and do the redirect: - await server.events.emit('redirect', (request, fullRedirectLocation)); - + const routeSpec = dynamicRouteTable[match.route]; + const { statusCode, fullRedirectLocation } = await processRedirect(request, routeSpec, match); return h.redirect(fullRedirectLocation).code(statusCode).takeover(); }); }; diff --git a/test/redirects.test.js b/test/redirects.test.js index 7c7af83..dc86067 100644 --- a/test/redirects.test.js +++ b/test/redirects.test.js @@ -18,13 +18,6 @@ lab.experiment('hapi-redirect', () => { return 'redirects totally working'; } }, - { - method: 'GET', - path: '/newtest', - handler(request, h) { - return 'vhost redirects totally working '; - } - }, { method: 'GET', path: '/newtest/{param*2}', @@ -214,36 +207,6 @@ lab.experiment('hapi-redirect', () => { Code.expect(result.headers.location).to.equal('/newtest/param1/param2'); }); - lab.test(' blahblah.localhost.com/test -> /newtest', async() => { - await server.register({ - plugin: redirectModule, - options: { - log: true, - log404: true, - redirects: { - '/test': '/it/works' - }, - vhosts: { - 'blahblah.com.localhost': { - '/test': '/newtest', - '/post/(.*)/': '/newtest', - '/*': '/newtest', - } - } - } - }); - await server.start(); - const result = await server.inject({ - method: 'get', - url: '/test', - headers: { - Host: 'blahblah.com.localhost' - } - }); - Code.expect(result.statusCode).to.equal(301); - Code.expect(result.headers.location).to.equal('/newtest'); - }); - lab.test('expose plugin', async() => { await server.register({ plugin: redirectModule, @@ -254,24 +217,19 @@ lab.experiment('hapi-redirect', () => { }); server.plugins['hapi-redirects'].register({ redirects: { - '/test': '/it/works' - }, - vhosts: { - 'blahblah.com.localhost': { - '/test': '/newtest', + '/test': { + destination: '/it/works', + statusCode: 301 } - } + }, }); await server.start(); const result = await server.inject({ method: 'get', url: '/test', - headers: { - Host: 'blahblah.com.localhost' - } }); Code.expect(result.statusCode).to.equal(301); - Code.expect(result.headers.location).to.equal('/newtest'); + Code.expect(result.headers.location).to.equal('/it/works'); }); lab.test('set default status code', async() => { @@ -380,36 +338,6 @@ lab.experiment('hapi-redirect', () => { Code.expect(result.headers.location).to.equal('/newtest/param1/param2'); }); - lab.test(' set status code for specific route (with vhost) ', async() => { - await server.register({ - plugin: redirectModule, - options: { - log: true, - log404: true, - vhosts: { - 'blahblah.com.localhost': { - '/test': { - destination: '/newtest', - statusCode: 302 - }, - '/post/(.*)/': '/newtest', - '/*': '/newtest', - } - } - } - }); - await server.start(); - const result = await server.inject({ - method: 'get', - url: '/test', - headers: { - Host: 'blahblah.com.localhost' - } - }); - Code.expect(result.statusCode).to.equal(302); - Code.expect(result.headers.location).to.equal('/newtest'); - }); - lab.test(' accepts a callback that adds additional dynamic routes', async() => { let count = 0; await server.register({ @@ -460,9 +388,6 @@ lab.experiment('hapi-redirect', () => { options: { log: true, log404: true, - redirects: { - '/test': '/it/works', - }, dynamicRedirects(pluginOptions, redirectDone) { throw new Error('an error'); } @@ -475,7 +400,7 @@ lab.experiment('hapi-redirect', () => { Code.expect(result.statusCode).to.equal(404); }); - lab.test(' will throw an error if dynamic routes clash with existing routes', async() => { + lab.test(' static routes take precence when dynamic routes clash with existing routes', async() => { await server.register({ plugin: redirectModule, options: { @@ -496,7 +421,8 @@ lab.experiment('hapi-redirect', () => { method: 'get', url: '/test' }); - Code.expect(result.statusCode).to.equal(500); + Code.expect(result.statusCode).to.equal(301); + Code.expect(result.headers.location).to.equal('/it/works'); }); lab.test('emits event when redirect occurs', async() => { From d7a8f3c6d602d4a5e62d60bceeb20ba50cf05b8b Mon Sep 17 00:00:00 2001 From: orthagonal Date: Thu, 20 Sep 2018 02:54:19 +0000 Subject: [PATCH 3/4] test for node 9 too --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index efb0983..2edf090 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: node_js node_js: + - "9" - "8" From 355d195fe5238185e438af3352e61b2fb8b752c6 Mon Sep 17 00:00:00 2001 From: orthagonal Date: Tue, 25 Sep 2018 23:02:25 +0000 Subject: [PATCH 4/4] processRedirect --- lib/index.js | 107 ++++++++++++++--------------------------- lib/processRedirect.js | 41 ++++++++++++++++ 2 files changed, 76 insertions(+), 72 deletions(-) create mode 100644 lib/processRedirect.js diff --git a/lib/index.js b/lib/index.js index f03f6c6..10b173f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,5 @@ 'use strict'; -const useragent = require('useragent'); -const paramReplacer = require('./paramReplacer'); const Call = require('call'); -const querystring = require('query-string'); -const { parse } = require('url'); const defaults = { statusCode: 301, @@ -24,42 +20,7 @@ module.exports = (server, pluginOptions) => { server.event('redirect'); // turn a redirect into a redirection URL and a status code: - const processRedirect = async(request, routeSpec, match = false) => { - // get all the info for doing the redirect from the route spec: - const statusCode = routeSpec.statusCode || options.statusCode; - const routePath = typeof routeSpec === 'string' ? routeSpec : routeSpec.destination; - const route = request.route.path; - const redirectTo = paramReplacer(routePath, match ? match.params : request.params); - const redirectToUrl = parse(redirectTo, true); - // lump all queries together: - const allQueries = Object.assign({}, request.query, redirectToUrl.query); - // if needed, add the queries to the parsed url: - if (Object.keys(allQueries).length > 0) { - redirectToUrl.search = `?${querystring.stringify(allQueries)}`; - } - // let the url parser format the correct redirect Location: - const fullRedirectLocation = redirectToUrl.format(); - - let from = request.path; - if (Object.keys(request.query).length !== 0) { - from = `${from}?${querystring.stringify(request.query)}`; - } - // log the route info: - server.log(['hapi-redirect', 'redirect', 'info'], { - remoteAddress: `${request.info.remoteAddress}:${request.info.remotePort}`, - host: request.info.host, - userAgent: request.headers['user-agent'], - browser: useragent.parse(request.headers['user-agent']).toString(), - referrer: request.info.referrer, - routePath: route, - to: fullRedirectLocation, - from - }); - - // now emit the event and do the redirect: - await server.events.emit('redirect', (request, fullRedirectLocation)); - return { statusCode, fullRedirectLocation }; - }; + const processRedirect = require('./processRedirect'); // let them add additional routes: server.expose('register', (additionalRoutes) => { @@ -70,7 +31,7 @@ module.exports = (server, pluginOptions) => { method: '*', path, async handler(request, h) { - const { statusCode, fullRedirectLocation } = await processRedirect(request, additionalRoutes[path]); + const { statusCode, fullRedirectLocation } = await processRedirect(request, additionalRoutes[path], server, options); return h.redirect(fullRedirectLocation).code(statusCode); } }); @@ -81,38 +42,40 @@ module.exports = (server, pluginOptions) => { server.plugins['hapi-redirects'].register(options.redirects); } - server.ext('onPreResponse', async (request, h) => { - // does not interfere if it's not a 404 error - if ((request.response.statusCode && request.response.statusCode !== 404) || (request.response.isBoom && request.response.output.statusCode !== 404)) { - return h.continue; - } - // if it's 404 then look up a redirect, first get dynamic redirects: - const dynamicRedirects = options.dynamicRedirects ? options.dynamicRedirects : (redirectOptions) => []; + if (options.dynamicRedirects) { + server.ext('onPreResponse', async (request, h) => { + // does not interfere if it's not a 404 error + if ((request.response.statusCode && request.response.statusCode !== 404) || (request.response.isBoom && request.response.output.statusCode !== 404)) { + return h.continue; + } + // if it's 404 then look up a redirect, first get dynamic redirects: + const dynamicRedirects = options.dynamicRedirects ? options.dynamicRedirects : (redirectOptions) => []; - // the plugin options will be passed to your custom 'dynamicRedirects' function: - let dynamicRouteTable; - try { - dynamicRouteTable = await dynamicRedirects(options); - } catch (err) { - server.log(['hapi-redirect', 'error'], err); - return h.continue; - } - const router = new Call.Router(); - const path = request.path; + // the plugin options will be passed to your custom 'dynamicRedirects' function: + let dynamicRouteTable; + try { + dynamicRouteTable = await dynamicRedirects(options); + } catch (err) { + server.log(['hapi-redirect', 'error'], err); + return h.continue; + } + const router = new Call.Router(); + const path = request.path; - // load the routes: - Object.keys(dynamicRouteTable).forEach((source) => { - router.add({ method: 'get', path: source }); + // load the routes: + Object.keys(dynamicRouteTable).forEach((source) => { + router.add({ method: 'get', path: source }); + }); + // try to match the incoming route: + const host = request.headers.host; + const match = router.route('get', path, host); + if (!match.route) { + return h.continue; + } + // get the spec data for the matching route from either vhost or the route table: + const routeSpec = dynamicRouteTable[match.route]; + const { statusCode, fullRedirectLocation } = await processRedirect(request, routeSpec, server, options, match); + return h.redirect(fullRedirectLocation).code(statusCode).takeover(); }); - // try to match the incoming route: - const host = request.headers.host; - const match = router.route('get', path, host); - if (!match.route) { - return h.continue; - } - // get the spec data for the matching route from either vhost or the route table: - const routeSpec = dynamicRouteTable[match.route]; - const { statusCode, fullRedirectLocation } = await processRedirect(request, routeSpec, match); - return h.redirect(fullRedirectLocation).code(statusCode).takeover(); - }); + } }; diff --git a/lib/processRedirect.js b/lib/processRedirect.js new file mode 100644 index 0000000..c59d1ec --- /dev/null +++ b/lib/processRedirect.js @@ -0,0 +1,41 @@ +const paramReplacer = require('./paramReplacer'); +const querystring = require('query-string'); +const { parse } = require('url'); +const useragent = require('useragent'); + +module.exports = async(request, routeSpec, server, options, match = false) => { + // get all the info for doing the redirect from the route spec: + const statusCode = routeSpec.statusCode || options.statusCode; + const routePath = typeof routeSpec === 'string' ? routeSpec : routeSpec.destination; + const route = request.route.path; + const redirectTo = paramReplacer(routePath, match ? match.params : request.params); + const redirectToUrl = parse(redirectTo, true); + // lump all queries together: + const allQueries = Object.assign({}, request.query, redirectToUrl.query); + // if needed, add the queries to the parsed url: + if (Object.keys(allQueries).length > 0) { + redirectToUrl.search = `?${querystring.stringify(allQueries)}`; + } + // let the url parser format the correct redirect Location: + const fullRedirectLocation = redirectToUrl.format(); + + let from = request.path; + if (Object.keys(request.query).length !== 0) { + from = `${from}?${querystring.stringify(request.query)}`; + } + // log the route info: + server.log(['hapi-redirect', 'redirect', 'info'], { + remoteAddress: `${request.info.remoteAddress}:${request.info.remotePort}`, + host: request.info.host, + userAgent: request.headers['user-agent'], + browser: useragent.parse(request.headers['user-agent']).toString(), + referrer: request.info.referrer, + routePath: route, + to: fullRedirectLocation, + from + }); + + // now emit the event and do the redirect: + await server.events.emit('redirect', (request, fullRedirectLocation)); + return { statusCode, fullRedirectLocation }; +};