From 3269258ecebf8936c8b03a88f2df3f6bb1db554c Mon Sep 17 00:00:00 2001 From: captain-digitalsailors Date: Sat, 23 Dec 2017 10:40:57 +0100 Subject: [PATCH] reverse index.html implemented --- .gitignore | 7 ++++ README.md | 29 +++++++++++++- index.js | 21 ++++++++-- package.json | 22 +++++++++++ test/unit.js | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 test/unit.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb995f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.DS_Store +node_modules +npm-debug.log +*.log +git-commit.json +.vscode diff --git a/README.md b/README.md index 542ef74..271dacd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,31 @@ A Lambda@Edge function that implements standard web server redirects: -URIs ending with a slash (e.g. "/something/") are "internally" redirected to "/something/index.html". +URIs ending with a slash (e.g. "/something/") are "internally" redirected to "/something/index.html", i.e. the browser sees "/something/" but on the server-side the content is taken from "/something/index.html". -URIs without a suffix (and not ending with a slash) will redirect with an HTTP status 301 Moved Permanently to the same URL with a slash appended. +URIs without an extension (and not ending with a slash) will redirect with an HTTP status 301 (Moved Permanently) to the same URL with a slash appended. + +## Examples + + / -> internal redirect -> /index.html + /foo/bar/ -> internal redirect -> /foo/bar/index.html + /foo -> external redirect (301) -> /foo/ + /foo.html -> no redirect + /foo/bar.html -> no redirect + /foo/index.html -> external redirect (301) -> /foo/ + +## Notes + +This URL scheme is somewhat opinionated. It tries to balance SEO requirements with server-side tooling. (E.g. S3 tooling tries to infer the content-type from the file extension.) + +It allows you to have very nice outward facing URLs like "/cooltopic", that internally use a file with a correct extension: "cooltopic/index.html". To have content other than index.html in a folder, you need to expose the file extension: "/cooltopic/somecontent.html" + +## Installation + +1. Create a function called "LATE-standard-redirects-for-cloudfront" in N. Virginia (us-east-1) +2. Run "npm run deploy" + +This function assumes that your CloudFront distribution handles the URL "/" directly by having the property "Default Root Object" +set to "index.html". + +TODO: IAM, SAM \ No newline at end of file diff --git a/index.js b/index.js index 87da272..8fdd433 100644 --- a/index.js +++ b/index.js @@ -20,13 +20,26 @@ const http = require('http'); exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; - - if (request.uri.endsWith('/')) { + + let prefixPath; // needed for 2nd condition + + if (request.uri.match('.+/$')) { request.uri += 'index.html'; callback(null, request); - } else if (request.uri.match('/[^/.]*$')) { + } else if (prefixPath = request.uri.match('(.+)/index.html')) { const response = { - status: '302', + status: '301', + statusDescription: 'Found', + headers: { + location: [{ + key: 'Location', value: prefixPath[1] + '/', + }], + } + }; + callback(null, response); + } else if (request.uri.match('/[^/.]+$')) { + const response = { + status: '301', statusDescription: 'Found', headers: { location: [{ diff --git a/package.json b/package.json new file mode 100644 index 0000000..e7c7104 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "standard-redirects-for-cloudfront", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "mocha", + "test-coverage": "istanbul cover _mocha", + "predeploy": "git log -1 --pretty=format:'{ \"date\":\"%cI\", \"commit\":\"%H\" }' > git-commit.json", + "deploy": "zip -r LATE-standard-redirects-for-cloudfront.zip . -i \\*.js -i git-commit.json -i node_modules/\\* -x test/\\* -x node_modules/aws-sdk/\\* -x node_modules/mocha/\\* && echo Uploading... && aws lambda update-function-code --region us-east-1 --function-name LATE-standard-redirects-for-cloudfront --zip-file fileb://LATE-standard-redirects-for-cloudfront.zip --publish" + }, + "dependencies": {}, + "license": "Apache-2.0", + "author": { + "name": "DigitalSailors e.K.", + "email": "contact@digital-sailors.de", + "url": "https://www.digital-sailors.de" + }, + "devDependencies": { + "mocha": "^4.0.1" + } +} diff --git a/test/unit.js b/test/unit.js new file mode 100644 index 0000000..0f10417 --- /dev/null +++ b/test/unit.js @@ -0,0 +1,109 @@ +'use strict'; + +/* + Copyright 2017 DigitalSailors e.K. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +const assert = require('assert'); + +const index = require('../index.js'); + +describe('Testing index.js', function() { + it('/ -> no redirect', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert(data.uri === '/')); + }); + }); + + it('/foo/ -> internal redirect -> /foo/index.html', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/foo/' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert.strictEqual(data.uri, '/foo/index.html')); + }); + }); + + it('/foo/bar/ -> internal redirect -> /foo/bar/index.html', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/foo/bar/' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert.strictEqual(data.uri, '/foo/bar/index.html')); + }); + }); + + it('/foo -> external redirect (301) -> /foo/', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/foo' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert.strictEqual(data.status, '301') + || assert.strictEqual(data.headers.location[0].key, 'Location') + || assert.strictEqual(data.headers.location[0].value, '/foo/')); + }); + }); + + it('/foo.html -> no redirect', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/foo.html' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert.strictEqual(data.uri, '/foo.html')); }); + }); + + it('/foo/bar.html -> no redirect', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/foo/bar.html' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert.strictEqual(data.uri, '/foo/bar.html')); }); + }); + + it('/foo/index.html -> external redirect (301) -> /foo/', function(done) { + const event = { + Records:[{ cf: { + request: { + uri: '/foo/index.html' + } + } }] }; + index.handler(event, {}, (err, data) => { + done(assert.strictEqual(data.status, '301') + || assert.strictEqual(data.headers.location[0].key, 'Location') + || assert.strictEqual(data.headers.location[0].value, '/foo/')); + }); + }); +}); \ No newline at end of file