From 4635b1894442e47b14c7192723c413b3589a629a Mon Sep 17 00:00:00 2001 From: David Beale Date: Mon, 4 May 2015 21:57:25 +0100 Subject: [PATCH 1/8] Add indexDocument option - which servers static web content --- lib/app.js | 4 +- lib/controllers.js | 93 ++++++++++++++++++++++++++-------- lib/index.js | 14 ++++- package.json | 1 + test/test.js | 124 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 23 deletions(-) diff --git a/lib/app.js b/lib/app.js index 9ffc0908..26e0d418 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,10 +1,10 @@ 'use strict'; -var app = function (hostname, port, directory, silent) { +var app = function (hostname, port, directory, silent, indexDocument, errorDocument) { var express = require('express'), app = express(), logger = require('./logger')(silent), Controllers = require('./controllers'), - controllers = new Controllers(directory, logger), + controllers = new Controllers(directory, logger, indexDocument, errorDocument), concat = require('concat-stream'); /** diff --git a/lib/controllers.js b/lib/controllers.js index 558622e7..04ff9080 100644 --- a/lib/controllers.js +++ b/lib/controllers.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = function (rootDirectory, logger) { +module.exports = function (rootDirectory, logger, indexDocument, errorDocument) { var FileStore = require('./file-store'), fileStore = new FileStore(rootDirectory), templateBuilder = require('./xml-template-builder'); @@ -10,6 +10,23 @@ module.exports = function (rootDirectory, logger) { return res.send(template); }; + var buildResponse = function (req, res, status, object, data) { + res.header('Etag', object.md5); + res.header('Last-Modified', new Date(object.modifiedDate).toUTCString()); + res.header('Content-Type', object.contentType); + res.header('Content-Length', object.size); + if (object.customMetaData.length > 0) { + object.customMetaData.forEach(function (metaData) { + res.header(metaData.key, metaData.value); + }); + } + res.status(200); + if (req.method === 'HEAD') { + return res.end(); + } + return res.end(data); + }; + /** * The following methods correspond the S3 api. For more information visit: * http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html @@ -37,6 +54,7 @@ module.exports = function (rootDirectory, logger) { return buildXmlResponse(res, 200, template); }, getBucket: function (req, res) { + var options = { marker: req.query.marker || null, prefix: req.query.prefix || null, @@ -46,8 +64,36 @@ module.exports = function (rootDirectory, logger) { logger.info('Fetched bucket "%s" with options %s', req.bucket.name, options); fileStore.getObjects(req.bucket, options, function (err, results) { logger.info('Found %d objects for bucket "%s"', results.length, req.bucket.name); - var template = templateBuilder.buildBucketQuery(options, results); - return buildXmlResponse(res, 200, template); + + var match = false; + if (indexDocument) + { + results.forEach(function(result){ + if (result.key === indexDocument) + { + match = true; + fileStore.getObject(req.bucket, result.key, function (err, object, data) { + if (err) + { + var template = templateBuilder.buildKeyNotFound(keyName); + logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); + return buildXmlResponse(res, 404, template); + } + else + { + logger.info('Serving Page: %s', object.key); + return buildResponse(req, res, 200, object, data); + } + }); + } + }); + } + + if (!match) + { + var template = templateBuilder.buildBucketQuery(options, results); + return buildXmlResponse(res, 200, template); + } }); }, putBucket: function (req, res) { @@ -107,9 +153,28 @@ module.exports = function (rootDirectory, logger) { } fileStore.getObject(req.bucket, keyName, function (err, object, data) { if (err) { - var template = templateBuilder.buildKeyNotFound(keyName); - logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); - return buildXmlResponse(res, 404, template); + + if (indexDocument) + { + return fileStore.getObject(req.bucket, keyName + indexDocument, function (err, object, data) { + if (err) + { + var template = templateBuilder.buildKeyNotFound(keyName); + logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); + return buildXmlResponse(res, 404, template); + } + else + { + return buildResponse(req, res, 200, object, data); + } + }); + } + else + { + var template = templateBuilder.buildKeyNotFound(keyName); + logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); + return buildXmlResponse(res, 404, template); + } } var noneMatch = req.headers['if-none-match']; @@ -124,20 +189,8 @@ module.exports = function (rootDirectory, logger) { return res.status(304).end(); } } - res.header('Etag', object.md5); - res.header('Last-Modified', new Date(object.modifiedDate).toUTCString()); - res.header('Content-Type', object.contentType); - res.header('Content-Length', object.size); - if (object.customMetaData.length > 0) { - object.customMetaData.forEach(function (metaData) { - res.header(metaData.key, metaData.value); - }); - } - res.status(200); - if (req.method === 'HEAD') { - return res.end(); - } - return res.end(data); + + return buildResponse(req, res, 200, object, data); }); }, putObject: function (req, res) { diff --git a/lib/index.js b/lib/index.js index 814afb57..5325417e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,8 @@ var S3rver = function () { this.port = 4578; this.hostname = 'localhost'; this.silent = false; + this.indexDocument = ''; + this.errorDocument = ''; }; S3rver.prototype.setPort = function (port) { @@ -26,8 +28,18 @@ S3rver.prototype.setSilent = function (silent) { return this; }; +S3rver.prototype.setIndexDocument = function (indexDocument) { + this.indexDocument = indexDocument; + return this; +}; + +S3rver.prototype.setErrorDocument = function (errorDocument) { + this.errorDocument = errorDocument; + return this; +}; + S3rver.prototype.run = function (done) { - var app = new App(this.hostname, this.port, this.directory, this.silent); + var app = new App(this.hostname, this.port, this.directory, this.silent, this.indexDocument, this.errorDocument); return app.serve(done); }; diff --git a/package.json b/package.json index 2a9e586a..a91ade45 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "fs-extra": "^0.14.0", "mocha": "^2.1.0", "moment": "^2.8.4", + "request": "^2.55.0", "should": "^4.4.2", "xml2js": "^0.4.4" }, diff --git a/test/test.js b/test/test.js index ececa6b9..c830dd6c 100644 --- a/test/test.js +++ b/test/test.js @@ -11,6 +11,7 @@ var path = require('path'); var md5 = require('MD5'); var S3rver = require('../lib'); var util = require('util'); +var request = require('request'); describe('S3rver Tests', function () { var s3Client; @@ -550,3 +551,126 @@ describe('S3rver Tests', function () { }); }); }); + + +describe('S3rver Tests with Static Web Hosting', function () { + var s3Client; + before(function (done) { + /** + * Start the server + */ + var s3rver = new S3rver(); + s3rver.setHostname('localhost') + .setPort(5694) + .setDirectory('/tmp/s3rver_test_directory') + .setSilent(true) + .setIndexDocument('index.html') + .run(function (err, hostname, port, directory) { + if (err) { + return done('Error starting server', err); + } + var config = { + accessKeyId: '123', + secretAccessKey: 'abc', + endpoint: util.format('%s:%d', hostname, port), + sslEnabled: false, + s3ForcePathStyle: true + }; + AWS.config.update(config); + s3Client = new AWS.S3(); + s3Client.endpoint = new AWS.Endpoint(config.endpoint); + /** + * Remove if exists and recreate the temporary directory + */ + fs.remove(directory, function (err) { + if (err) { + return done(err); + } + fs.mkdirs(directory, function (err) { + if (err) { + return done(err); + } + done(); + }); + }); + }); + }); + + + + it('should create a site bucket', function (done) { + s3Client.createBucket({Bucket: 'site'}, function (err) { + if (err) { + return done(err); + } + done(); + }); + }); + + + it('should upload a html page to / path', function (done) { + var params = {Bucket: 'site', Key: 'index.html', Body: 'Hello'}; + s3Client.putObject(params, function (err, data) { + /[a-fA-F0-9]{32}/.test(data.ETag).should.equal(true); + if (err) { + return done(err); + } + done(); + }); + }); + + + it('should upload a html page to a directory path', function (done) { + var params = {Bucket: 'site', Key: 'page/index.html', Body: 'Hello'}; + s3Client.putObject(params, function (err, data) { + /[a-fA-F0-9]{32}/.test(data.ETag).should.equal(true); + if (err) { + return done(err); + } + done(); + }); + }); + + + it('should get an index page at / path', function (done) { + request('http://localhost:5694/site/', function (error, response, body) { + if (error) + { + return done(error); + } + + if (response.statusCode !== 200) { + return done(new Error('Invalid status: ' + response.statusCode)); + } + + if (body !== 'Hello') + { + return done(new Error('Invalid Content: ' + body)); + } + + done(); + }); + }); + + + it('should get an index page at /page/ path', function (done) { + request('http://localhost:5694/site/page/', function (error, response, body) { + if (error) + { + return done(error); + } + + if (response.statusCode !== 200) { + return done(new Error('Invalid status: ' + response.statusCode)); + } + + if (body !== 'Hello') + { + return done(new Error('Invalid Content: ' + body)); + } + + done(); + }); + }); + +}); From 014fe8b47ce878671712512261ab4b8762cee73f Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 09:14:28 +0100 Subject: [PATCH 2/8] Describe static hosting --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 2b6f743f..52aa986e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,21 @@ Please see [Fake S3s wiki page](https://github.com/jubos/fake-s3/wiki/Supported- Please test, if you encounter any problems please do not hesitate to open an issue :) +## Static Website Hosting + +If you specify an *indexDocument* then ```get``` requests will serve the *indexDocument* if it found, simulating the static website mode of AWS S3. An *errorDocument* can also be set, to serve a custom 404 page. + +### Hostname Resolution + +By default a bucket name needs to be given. So for a bucket called ```mysite.local```, with an indexDocument of ```index.html```. Visiting ```http://localhost:4568/mysite.local/``` in your browser will display the ```index.html``` file uploaded to the bucket. + +However you can also setup a local hostname in your /etc/hosts file pointing at 127.0.0.1 +``` +localhost 127.0.0.1 +mysite.local 127.0.0.1 +``` +Now you can access the served content at ```http://mysite.local:4568/``` + ## Tests > When running the tests with node v0.10.0 the following [error](https://github.com/mochajs/mocha/issues/777) is encountered. This is resolved by running the tests with v0.11.*. I recommend using [NVM](https://github.com/creationix/nvm) to manage your node versions. From 90d706465f68a87b344f5b42b62860d5ad7b5ff5 Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 09:15:03 +0100 Subject: [PATCH 3/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52aa986e..5606cf3c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Please test, if you encounter any problems please do not hesitate to open an iss ## Static Website Hosting -If you specify an *indexDocument* then ```get``` requests will serve the *indexDocument* if it found, simulating the static website mode of AWS S3. An *errorDocument* can also be set, to serve a custom 404 page. +If you specify an *indexDocument* then ```get``` requests will serve the *indexDocument* if it is found, simulating the static website mode of AWS S3. An *errorDocument* can also be set, to serve a custom 404 page. ### Hostname Resolution From 8d05c54654f41d49c2e9511ffea646308fd403fc Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 10:31:13 +0100 Subject: [PATCH 4/8] Stock 404 response --- lib/controllers.js | 48 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/controllers.js b/lib/controllers.js index 04ff9080..a6ad22eb 100644 --- a/lib/controllers.js +++ b/lib/controllers.js @@ -1,8 +1,11 @@ 'use strict'; -module.exports = function (rootDirectory, logger, indexDocument, errorDocument) { + var FileStore = require('./file-store'), - fileStore = new FileStore(rootDirectory), - templateBuilder = require('./xml-template-builder'); + templateBuilder = require('./xml-template-builder'), + path = require('path'); + +module.exports = function (rootDirectory, logger, indexDocument, errorDocument) { + var fileStore = new FileStore(rootDirectory); var buildXmlResponse = function (res, status, template) { res.header('Content-Type', 'application/xml'); @@ -20,13 +23,35 @@ module.exports = function (rootDirectory, logger, indexDocument, errorDocument) res.header(metaData.key, metaData.value); }); } - res.status(200); + res.status(status); if (req.method === 'HEAD') { return res.end(); } return res.end(data); }; + var errorResponse = function(req, res, keyName) { + logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); + + if (indexDocument) + { + var ErrorDoc = '\n404 - Resource Not Found

404 - Resource Not Found

'; + + return buildResponse(req, res, 404, { + md4: '', + modifiedDate: new Date(), + contentType: 'text/html', + customMetaData: [], + size: ErrorDoc.length + }, ErrorDoc); + } + else + { + var template = templateBuilder.buildKeyNotFound(keyName); + return buildXmlResponse(res, 404, template); + } + }; + /** * The following methods correspond the S3 api. For more information visit: * http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html @@ -75,9 +100,7 @@ module.exports = function (rootDirectory, logger, indexDocument, errorDocument) fileStore.getObject(req.bucket, result.key, function (err, object, data) { if (err) { - var template = templateBuilder.buildKeyNotFound(keyName); - logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); - return buildXmlResponse(res, 404, template); + return errorResponse(req, res, object.key); } else { @@ -156,12 +179,11 @@ module.exports = function (rootDirectory, logger, indexDocument, errorDocument) if (indexDocument) { - return fileStore.getObject(req.bucket, keyName + indexDocument, function (err, object, data) { + keyName = path.join(keyName, indexDocument); + return fileStore.getObject(req.bucket, keyName, function (err, object, data) { if (err) { - var template = templateBuilder.buildKeyNotFound(keyName); - logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); - return buildXmlResponse(res, 404, template); + return errorResponse(req, res, keyName); } else { @@ -171,9 +193,7 @@ module.exports = function (rootDirectory, logger, indexDocument, errorDocument) } else { - var template = templateBuilder.buildKeyNotFound(keyName); - logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name); - return buildXmlResponse(res, 404, template); + return errorResponse(req, res, keyName); } } From 4f04f7e119d932272b277f7992e3851c91f3c2b3 Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 10:47:13 +0100 Subject: [PATCH 5/8] Stock 404 response --- lib/controllers.js | 19 ++++++++++++------- test/test.js | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/controllers.js b/lib/controllers.js index a6ad22eb..89261747 100644 --- a/lib/controllers.js +++ b/lib/controllers.js @@ -35,21 +35,26 @@ module.exports = function (rootDirectory, logger, indexDocument, errorDocument) if (indexDocument) { + return notFoundResponse(req, res); + } + else + { + var template = templateBuilder.buildKeyNotFound(keyName); + return buildXmlResponse(res, 404, template); + } + }; + + + var notFoundResponse = function(req, res) + { var ErrorDoc = '\n404 - Resource Not Found

404 - Resource Not Found

'; return buildResponse(req, res, 404, { - md4: '', modifiedDate: new Date(), contentType: 'text/html', customMetaData: [], size: ErrorDoc.length }, ErrorDoc); - } - else - { - var template = templateBuilder.buildKeyNotFound(keyName); - return buildXmlResponse(res, 404, template); - } }; /** diff --git a/test/test.js b/test/test.js index c830dd6c..2e85e96a 100644 --- a/test/test.js +++ b/test/test.js @@ -673,4 +673,25 @@ describe('S3rver Tests with Static Web Hosting', function () { }); }); + + it('should get a 404 error page', function (done) { + request('http://localhost:5694/site/page/not-exists', function (error, response, body) { + if (error) + { + return done(error); + } + + if (response.statusCode !== 404) { + return done(new Error('Invalid status: ' + response.statusCode)); + } + + if (response.headers['content-type'] !== 'text/html; charset=utf-8') + { + return done(new Error('Invalid ContentType: ' + response.headers['content-type'])); + } + + done(); + }); + }); + }); From 1a69b8e95ac938561beff194668e8a80ee6e897c Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 11:00:14 +0100 Subject: [PATCH 6/8] Command Line options --- bin/s3rver.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/s3rver.js b/bin/s3rver.js index 8a53bdd0..0648ddde 100755 --- a/bin/s3rver.js +++ b/bin/s3rver.js @@ -10,6 +10,8 @@ program.version(version, '--version'); program.option('-h, --hostname [value]', 'Set the host name or ip for the server', 'localhost') .option('-p, --port ', 'Set the port of the http server', 4568) .option('-s, --silent', 'Suppress log messages', false) + .option('-i, --indexDocumement', 'Index Document for Static Web Hosting', '') + .option('-e, --errorDocument', 'Custom Error Document for Static Web Hosting', '') .option('-d, --directory [path]', 'Data directory') .parse(process.argv); @@ -34,6 +36,8 @@ s3rver.setHostname(program.hostname) .setPort(program.port) .setDirectory(program.directory) .setSilent(program.silent) + .setIndexDocument(program.indexDocumement) + .setErrorDocument(program.errorDocument) .run(function (err, host, port) { console.log('now listening on host %s and port %d', host, port); }); From 89109a71b807f697cc82b53d45930924b176b4fd Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 11:16:38 +0100 Subject: [PATCH 7/8] Custom Error page --- lib/controllers.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/controllers.js b/lib/controllers.js index 89261747..e68378d9 100644 --- a/lib/controllers.js +++ b/lib/controllers.js @@ -35,7 +35,24 @@ module.exports = function (rootDirectory, logger, indexDocument, errorDocument) if (indexDocument) { - return notFoundResponse(req, res); + if (errorDocument) + { + fileStore.getObject(req.bucket, errorDocument, function (err, object, data) { + if (err) + { + console.error('Custom Error Document not found: ' + errorDocument); + return notFoundResponse(req, res); + } + else + { + return buildResponse(req, res, 404, object, data); + } + }); + } + else + { + return notFoundResponse(req, res); + } } else { From 600afc05c8f69439562d4fd73f418dd77643986d Mon Sep 17 00:00:00 2001 From: David Beale Date: Tue, 5 May 2015 12:17:11 +0100 Subject: [PATCH 8/8] Use custom hostname as bucket name --- lib/app.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/app.js b/lib/app.js index 26e0d418..e9ac59c9 100644 --- a/lib/app.js +++ b/lib/app.js @@ -5,7 +5,8 @@ var app = function (hostname, port, directory, silent, indexDocument, errorDocum logger = require('./logger')(silent), Controllers = require('./controllers'), controllers = new Controllers(directory, logger, indexDocument, errorDocument), - concat = require('concat-stream'); + concat = require('concat-stream'), + path = require('path'); /** * Log all requests @@ -24,6 +25,17 @@ var app = function (hostname, port, directory, silent, indexDocument, errorDocum })); }); + app.use(function(req, res, next) { + var host = req.headers.host.split(':')[0]; + + if (indexDocument && host !== 'localhost' && host !== '127.0.0.1') + { + req.url = path.join('/', host, req.url); + } + + next(); + }); + app.disable('x-powered-by'); /**