From 2bbf68ac1adf6bdb2c3f44f4469ae1c5c8f1e5f9 Mon Sep 17 00:00:00 2001 From: TeeTeeHaa Date: Sun, 24 Jun 2012 18:25:43 +0300 Subject: [PATCH 001/235] fixed issue regarding missing trailing slash in URL when using default file "index.html" --- lib/node-static.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 9399257..e6ac1c4 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -52,7 +52,14 @@ this.Server.prototype.serveDir = function (pathname, req, res, finish) { fs.stat(htmlIndex, function (e, stat) { if (!e) { - that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish); + var status = 200; + var headers = {}; + var originalPathname = decodeURI(url.parse(req.url).pathname); + if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') { + status = 301; + headers['Location'] = originalPathname + '/'; + } + that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); } else { if (pathname in exports.store) { streamFiles(exports.indexStore[pathname].files); From fb495dcbcb35cba076528c78ecce720e9ea8096d Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 6 Dec 2012 17:30:52 +0200 Subject: [PATCH 002/235] Allow error listeners to handle response --- lib/node-static.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index f0c0766..d6775e7 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -112,8 +112,10 @@ exports.Server.prototype.finish = function (status, headers, req, res, promise, if (promise.listeners('error').length > 0) { promise.emit('error', result); } - res.writeHead(status, headers); - res.end(); + else { + res.writeHead(status, headers); + res.end(); + } } } else { // Don't end the request here, if we're streaming; From f039e1c54675521782c4142ad70cec3a88b09c12 Mon Sep 17 00:00:00 2001 From: guybedford Date: Mon, 17 Dec 2012 00:01:34 +0000 Subject: [PATCH 003/235] 404 streaming test --- test/integration/node-static-test.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index 22afbef..c6a4c26 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -10,13 +10,17 @@ var suite = vows.describe('node-static'); var TEST_PORT = 8080; var TEST_SERVER = 'http://localhost:' + TEST_PORT; var server; +var callback; suite.addBatch({ 'once an http server is listening': { topic: function () { server = require('http').createServer(function (request, response) { request.addListener('end', function () { - fileServer.serve(request, response); + fileServer.serve(request, response, function(err, result) { + if (callback) + callback(request, response, err, result); + }); }); }).listen(TEST_PORT, this.callback) }, @@ -25,7 +29,7 @@ suite.addBatch({ * A topic without tests will be not executed */ assert.isTrue(true); } - } + }, }).addBatch({ 'requesting a file not found': { topic : function(){ @@ -34,6 +38,23 @@ suite.addBatch({ 'should respond with 404' : function(error, response, body){ assert.equal(response.statusCode, 404); } + }, + 'streaming a 404 page': { + topic: function(){ + callback = function(request, response, err, result) { + if (err) { + response.writeHead(err.status, err.headers); + setTimeout(function() { + response.end('Custom 404 Stream.') + }, 100); + } + } + request.get(TEST_SERVER + '/not-found', this.callback); + }, + 'should respond with the streamed content': function(error, response, body){ + callback = null; + assert.equal(body, 'Custom 404 Stream.'); + } } }).addBatch({ 'serving hello.txt': { From ed49feb4f40e862b975ed6c88cdc4a6c1c41fb96 Mon Sep 17 00:00:00 2001 From: guybedford Date: Mon, 17 Dec 2012 00:11:41 +0000 Subject: [PATCH 004/235] added both use cases --- test/integration/node-static-test.js | 40 +++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index c6a4c26..ae6d732 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -13,13 +13,15 @@ var server; var callback; suite.addBatch({ - 'once an http server is listening': { + 'once an http server is listening with a callback': { topic: function () { server = require('http').createServer(function (request, response) { request.addListener('end', function () { fileServer.serve(request, response, function(err, result) { if (callback) callback(request, response, err, result); + else + request.end(); }); }); }).listen(TEST_PORT, this.callback) @@ -31,14 +33,6 @@ suite.addBatch({ } }, }).addBatch({ - 'requesting a file not found': { - topic : function(){ - request.get(TEST_SERVER + '/not-found', this.callback); - }, - 'should respond with 404' : function(error, response, body){ - assert.equal(response.statusCode, 404); - } - }, 'streaming a 404 page': { topic: function(){ callback = function(request, response, err, result) { @@ -51,11 +45,39 @@ suite.addBatch({ } request.get(TEST_SERVER + '/not-found', this.callback); }, + 'should respond with 404' : function(error, response, body){ + assert.equal(response.statusCode, 404); + }, 'should respond with the streamed content': function(error, response, body){ callback = null; assert.equal(body, 'Custom 404 Stream.'); } } +}).addBatch({ + 'once an http server is listening without a callback': { + topic: function () { + server.close(); + server = require('http').createServer(function (request, response) { + request.addListener('end', function () { + fileServer.serve(request, response); + }); + }).listen(TEST_PORT, this.callback) + }, + 'should be listening' : function(){ + /* This test is necessary to ensure the topic execution. + * A topic without tests will be not executed */ + assert.isTrue(true); + } + } +}).addBatch({ + 'requesting a file not found': { + topic : function(){ + request.get(TEST_SERVER + '/not-found', this.callback); + }, + 'should respond with 404' : function(error, response, body){ + assert.equal(response.statusCode, 404); + } + } }).addBatch({ 'serving hello.txt': { topic : function(){ From fa3d5dea7b5d84d76153a897c1ca6131e1d71a87 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Fri, 21 Dec 2012 22:46:17 -0200 Subject: [PATCH 005/235] Allows dynamic addition of content types #75 --- lib/node-static.js | 1 + lib/node-static/mime.js | 4 ++++ test/integration/node-static-test.js | 32 ++++++++++++++++++---------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index d6775e7..53c3800 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -13,6 +13,7 @@ var util = require('./node-static/util'); // In-memory file store exports.store = {}; exports.indexStore = {}; +exports.mime = mime; exports.Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } diff --git a/lib/node-static/mime.js b/lib/node-static/mime.js index 7308669..f427654 100644 --- a/lib/node-static/mime.js +++ b/lib/node-static/mime.js @@ -142,3 +142,7 @@ exports.contentTypes = { "xyz": "chemical/x-pdb", "zip": "application/zip" }; + +exports.addContentType = function(extension, contentType){ + exports.contentTypes[extension] = contentType; +}; diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index ae6d732..c183c3e 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -73,7 +73,7 @@ suite.addBatch({ 'requesting a file not found': { topic : function(){ request.get(TEST_SERVER + '/not-found', this.callback); - }, + }, 'should respond with 404' : function(error, response, body){ assert.equal(response.statusCode, 404); } @@ -82,13 +82,13 @@ suite.addBatch({ 'serving hello.txt': { topic : function(){ request.get(TEST_SERVER + '/hello.txt', this.callback); - }, + }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); - }, + }, 'should respond with text/plain': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/plain'); - }, + }, 'should respond with hello world': function(error, response, body){ assert.equal(body, 'hello world'); } @@ -97,10 +97,10 @@ suite.addBatch({ 'serving directory index': { topic : function(){ request.get(TEST_SERVER, this.callback); - }, + }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); - }, + }, 'should respond with text/html': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/html'); } @@ -109,10 +109,10 @@ suite.addBatch({ 'serving index.html from the cache': { topic : function(){ request.get(TEST_SERVER + '/index.html', this.callback); - }, + }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); - }, + }, 'should respond with text/html': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/html'); } @@ -129,7 +129,7 @@ suite.addBatch({ }, _this.callback); }); - }, + }, 'should respond with 304' : function(error, response, body){ assert.equal(response.statusCode, 304); } @@ -163,7 +163,7 @@ suite.addBatch({ }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); - }, + }, 'head must has no body' : function(error, response, body){ assert.isUndefined(body); } @@ -175,6 +175,16 @@ suite.addBatch({ }, 'should respond with node-static/0.6.0' : function(error, response, body){ assert.equal(response.headers['server'], 'custom-server-name'); - } + } + } +}).addBatch({ + 'addings custom mime types': { + topic : function(){ + static.mime.addContentType('woff', 'application/font-woff'); + this.callback(); + }, + 'should add woff' : function(error, response, body){ + assert.equal(static.mime.contentTypes['woff'], 'application/font-woff'); + } } }).export(module); From c9654ce132fb7708714e3d5061af2599b595e41f Mon Sep 17 00:00:00 2001 From: Aria Stewart Date: Sat, 29 Dec 2012 17:29:27 -0500 Subject: [PATCH 006/235] Correct typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ca0ec..236ef03 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Command Line Interface serving "public" at http://127.0.0.1:8080 # specify additional headers (this one is useful for development) - $ static -H '{"Cache-Control": "no-cache, must-revaliate"}' + $ static -H '{"Cache-Control": "no-cache, must-revalidate"}' serving "." at http://127.0.0.1:8080 # set cache control max age From c3956d1e0a3118f26a90c7e3a5bf624fd51c6428 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 30 Jan 2013 12:10:08 +0200 Subject: [PATCH 007/235] Attempt to avoid decodeURI issue --- lib/node-static.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 53c3800..ba06a72 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -171,7 +171,13 @@ exports.Server.prototype.serve = function (req, res, callback) { var that = this, promise = new(events.EventEmitter); - var pathname = decodeURI(url.parse(req.url).pathname); + var pathname; + try { + pathname = decodeURI(url.parse(req.url).pathname); + } + catch(e) { + promise.emit('error', e); + } var finish = function (status, headers) { that.finish(status, headers, req, res, promise, callback); From 02827061a7e0dade05bf69ad850d6c48fdab7822 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 30 Jan 2013 12:14:00 +0200 Subject: [PATCH 008/235] added return statement --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index ba06a72..5708bc3 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -176,7 +176,7 @@ exports.Server.prototype.serve = function (req, res, callback) { pathname = decodeURI(url.parse(req.url).pathname); } catch(e) { - promise.emit('error', e); + return promise.emit('error', e); } var finish = function (status, headers) { From efcf0eb0ed841ecbac6d6014517fdb05411308ac Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 1 Feb 2013 12:45:36 +0200 Subject: [PATCH 009/235] Test for malformed URI --- test/integration/node-static-test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index c183c3e..67529be 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -78,6 +78,15 @@ suite.addBatch({ assert.equal(response.statusCode, 404); } } +}).addBatch({ + 'requesting a malformed URI': { + topic: function(){ + request.get(TEST_SERVER + '/a%AFc', this.callback); + } + 'should respond with 404': function(error, response, body){ + assert.equal(response.statusCode, 404); + } + } }).addBatch({ 'serving hello.txt': { topic : function(){ From e4cb3ae7aa9f6f9b20a9579edcff003b796030e9 Mon Sep 17 00:00:00 2001 From: guybedford Date: Fri, 1 Feb 2013 12:55:35 +0200 Subject: [PATCH 010/235] malformed URI adjustment for 404 with test case --- lib/node-static.js | 2 +- test/integration/node-static-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 5708bc3..7ed7811 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -176,7 +176,7 @@ exports.Server.prototype.serve = function (req, res, callback) { pathname = decodeURI(url.parse(req.url).pathname); } catch(e) { - return promise.emit('error', e); + pathname = url.parse(req.url).pathname; } var finish = function (status, headers) { diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index 67529be..df945c8 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -82,7 +82,7 @@ suite.addBatch({ 'requesting a malformed URI': { topic: function(){ request.get(TEST_SERVER + '/a%AFc', this.callback); - } + }, 'should respond with 404': function(error, response, body){ assert.equal(response.statusCode, 404); } From 11e9782cadff37bd7ae0a510bde81de883193bb6 Mon Sep 17 00:00:00 2001 From: guybedford Date: Mon, 4 Feb 2013 16:24:17 +0200 Subject: [PATCH 011/235] switch to 400 response for malformed uri --- lib/node-static.js | 14 ++++++++------ test/integration/node-static-test.js | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 7ed7811..fde1f0e 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -171,18 +171,20 @@ exports.Server.prototype.serve = function (req, res, callback) { var that = this, promise = new(events.EventEmitter); + var finish = function (status, headers) { + that.finish(status, headers, req, res, promise, callback); + }; + var pathname; try { - pathname = decodeURI(url.parse(req.url).pathname); + pathname = decodeURI(url.parse(req.url).pathname); } catch(e) { - pathname = url.parse(req.url).pathname; + return process.nextTick(function() { + return finish(400, {}); + }); } - var finish = function (status, headers) { - that.finish(status, headers, req, res, promise, callback); - }; - process.nextTick(function () { that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) { promise.emit('success', result); diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index df945c8..7559a68 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -83,8 +83,8 @@ suite.addBatch({ topic: function(){ request.get(TEST_SERVER + '/a%AFc', this.callback); }, - 'should respond with 404': function(error, response, body){ - assert.equal(response.statusCode, 404); + 'should respond with 400': function(error, response, body){ + assert.equal(response.statusCode, 400); } } }).addBatch({ From eeab04a946a2e8c28af97d130634eb1803e6a1a5 Mon Sep 17 00:00:00 2001 From: Arjan Singh Date: Sun, 24 Mar 2013 23:29:17 -0700 Subject: [PATCH 012/235] Removed 'end' event listener. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example did not work out of the box. I traced the problem to the 'end' event listener attached to request. I guess older versions of node.js required this but as of 0.10.0 (probably earlier), the `createServer` callback isn't called until `request` has already completed. Therefore, there is no need for the 'end' event listener anymore. Also, I removed some unnecessary parentheses. --- examples/file-server.js | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/examples/file-server.js b/examples/file-server.js index f133b80..4f0796e 100644 --- a/examples/file-server.js +++ b/examples/file-server.js @@ -3,22 +3,17 @@ var static = require('../lib/node-static'); // // Create a node-static server to serve the current directory // -var file = new(static.Server)('.', { cache: 7200, headers: {'X-Hello':'World!'} }); +var file = new static.Server('.', { cache: 7200, headers: {'X-Hello':'World!'} }); require('http').createServer(function (request, response) { - request.addListener('end', function () { - // - // Serve files! - // - file.serve(request, response, function (err, res) { - if (err) { // An error as occured - console.error("> Error serving " + request.url + " - " + err.message); - response.writeHead(err.status, err.headers); - response.end(); - } else { // The file was served successfully - console.log("> " + request.url + " - " + res.message); - } - }); + file.serve(request, response, function (err, res) { + if (err) { // An error as occured + console.error("> Error serving " + request.url + " - " + err.message); + response.writeHead(err.status, err.headers); + response.end(); + } else { // The file was served successfully + console.log("> " + request.url + " - " + res.message); + } }); }).listen(8080); From a78aaba9ba32385626ea3b5748b6026fc66fd592 Mon Sep 17 00:00:00 2001 From: David Sargeant Date: Mon, 1 Apr 2013 09:30:44 -0400 Subject: [PATCH 013/235] One single Server exports. --- lib/node-static.js | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index fde1f0e..e06f6fe 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -5,7 +5,7 @@ var fs = require('fs') , url = require('url') , path = require('path'); -exports.version = [0, 6, 5]; +exports.version = [0, 6, 7]; var mime = require('./node-static/mime'); var util = require('./node-static/util'); @@ -15,7 +15,7 @@ exports.store = {}; exports.indexStore = {}; exports.mime = mime; -exports.Server = function (root, options) { +Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } this.root = path.resolve(root || '.'); @@ -24,7 +24,7 @@ exports.Server = function (root, options) { this.defaultHeaders = {}; this.options.headers = this.options.headers || {}; - + if ('cache' in this.options) { if (typeof(this.options.cache) === 'number') { this.cache = this.options.cache; @@ -44,14 +44,14 @@ exports.Server = function (root, options) { if (this.cache !== false) { this.defaultHeaders['cache-control'] = 'max-age=' + this.cache; } - + for (var k in this.defaultHeaders) { this.options.headers[k] = this.options.headers[k] || this.defaultHeaders[k]; } }; -exports.Server.prototype.serveDir = function (pathname, req, res, finish) { +Server.prototype.serveDir = function (pathname, req, res, finish) { var htmlIndex = path.join(pathname, 'index.html'), that = this; @@ -80,10 +80,10 @@ exports.Server.prototype.serveDir = function (pathname, req, res, finish) { } }; -exports.Server.prototype.serveFile = function (pathname, status, headers, req, res) { +Server.prototype.serveFile = function (pathname, status, headers, req, res) { var that = this; var promise = new(events.EventEmitter); - + pathname = this.resolve(pathname); fs.stat(pathname, function (e, stat) { @@ -97,7 +97,7 @@ exports.Server.prototype.serveFile = function (pathname, status, headers, req, r return promise; }; -exports.Server.prototype.finish = function (status, headers, req, res, promise, callback) { +Server.prototype.finish = function (status, headers, req, res, promise, callback) { var result = { status: status, headers: headers, @@ -130,7 +130,7 @@ exports.Server.prototype.finish = function (status, headers, req, res, promise, } }; -exports.Server.prototype.servePath = function (pathname, status, headers, req, res, finish) { +Server.prototype.servePath = function (pathname, status, headers, req, res, finish) { var that = this, promise = new(events.EventEmitter); @@ -163,28 +163,20 @@ exports.Server.prototype.servePath = function (pathname, status, headers, req, r return promise; }; -exports.Server.prototype.resolve = function (pathname) { +Server.prototype.resolve = function (pathname) { return path.resolve(path.join(this.root, pathname)); }; -exports.Server.prototype.serve = function (req, res, callback) { +Server.prototype.serve = function (req, res, callback) { var that = this, promise = new(events.EventEmitter); + + var pathname = decodeURI(url.parse(req.url).pathname); var finish = function (status, headers) { that.finish(status, headers, req, res, promise, callback); }; - var pathname; - try { - pathname = decodeURI(url.parse(req.url).pathname); - } - catch(e) { - return process.nextTick(function() { - return finish(400, {}); - }); - } - process.nextTick(function () { that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) { promise.emit('success', result); @@ -195,7 +187,7 @@ exports.Server.prototype.serve = function (req, res, callback) { if (! callback) { return promise } }; -exports.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { +Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var mtime = Date.parse(stat.mtime), key = pathname || files[0], headers = {}, @@ -251,7 +243,7 @@ exports.Server.prototype.respond = function (pathname, status, _headers, files, } }; -exports.Server.prototype.stream = function (pathname, files, buffer, res, callback) { +Server.prototype.stream = function (pathname, files, buffer, res, callback) { (function streamFile(files, offset) { var file = files.shift(); @@ -278,3 +270,4 @@ exports.Server.prototype.stream = function (pathname, files, buffer, res, callba })(files.slice(0), 0); }; +exports.Server = Server; From 5c8a24fc694d82ad29707a5910b5a7692889f4e6 Mon Sep 17 00:00:00 2001 From: David Sargeant Date: Mon, 1 Apr 2013 10:36:56 -0400 Subject: [PATCH 014/235] Switched mime handling to mime package. Tests working with node 0.10.x. --- lib/node-static.js | 7 +- lib/node-static/mime.js | 148 --------------------------- package.json | 3 +- test/integration/node-static-test.js | 31 +++--- 4 files changed, 20 insertions(+), 169 deletions(-) delete mode 100644 lib/node-static/mime.js diff --git a/lib/node-static.js b/lib/node-static.js index e06f6fe..267395f 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -3,11 +3,11 @@ var fs = require('fs') , buffer = require('buffer') , http = require('http') , url = require('url') - , path = require('path'); + , path = require('path') + , mime = require('mime'); exports.version = [0, 6, 7]; -var mime = require('./node-static/mime'); var util = require('./node-static/util'); // In-memory file store @@ -209,9 +209,8 @@ Server.prototype.respond = function (pathname, status, _headers, files, stat, re (!clientMTime || clientMTime >= mtime)) { finish(304, headers); } else { - var fileExtension = path.extname(files[0]).slice(1).toLowerCase(); headers['content-length'] = stat.size; - headers['content-type'] = mime.contentTypes[fileExtension] || + headers['content-type'] = mime.lookup(files[0]); 'application/octet-stream'; for (var k in _headers) { headers[k] = _headers[k] } diff --git a/lib/node-static/mime.js b/lib/node-static/mime.js deleted file mode 100644 index f427654..0000000 --- a/lib/node-static/mime.js +++ /dev/null @@ -1,148 +0,0 @@ -exports.contentTypes = { - "aiff": "audio/x-aiff", - "arj": "application/x-arj-compressed", - "appcache": "text/cache-manifest", - "asf": "video/x-ms-asf", - "asx": "video/x-ms-asx", - "au": "audio/ulaw", - "avi": "video/x-msvideo", - "bcpio": "application/x-bcpio", - "ccad": "application/clariscad", - "cod": "application/vnd.rim.cod", - "com": "application/x-msdos-program", - "cpio": "application/x-cpio", - "cpt": "application/mac-compactpro", - "csh": "application/x-csh", - "css": "text/css", - "deb": "application/x-debian-package", - "dl": "video/dl", - "doc": "application/msword", - "drw": "application/drafting", - "dvi": "application/x-dvi", - "dwg": "application/acad", - "dxf": "application/dxf", - "dxr": "application/x-director", - "etx": "text/x-setext", - "ez": "application/andrew-inset", - "fli": "video/x-fli", - "flv": "video/x-flv", - "gif": "image/gif", - "gl": "video/gl", - "gtar": "application/x-gtar", - "gz": "application/x-gzip", - "hdf": "application/x-hdf", - "hqx": "application/mac-binhex40", - "htm": "text/html", - "html": "text/html", - "ice": "x-conference/x-cooltalk", - "ico": "image/x-icon", - "ief": "image/ief", - "igs": "model/iges", - "ips": "application/x-ipscript", - "ipx": "application/x-ipix", - "jad": "text/vnd.sun.j2me.app-descriptor", - "jar": "application/java-archive", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "js": "text/javascript", - "json": "application/json", - "latex": "application/x-latex", - "less": "text/css", - "lsp": "application/x-lisp", - "lzh": "application/octet-stream", - "m": "text/plain", - "m3u": "audio/x-mpegurl", - "man": "application/x-troff-man", - "manifest": "text/cache-manifest", - "me": "application/x-troff-me", - "midi": "audio/midi", - "mif": "application/x-mif", - "mime": "www/mime", - "movie": "video/x-sgi-movie", - "mp4": "video/mp4", - "mpg": "video/mpeg", - "mpga": "audio/mpeg", - "ms": "application/x-troff-ms", - "nc": "application/x-netcdf", - "oda": "application/oda", - "oga": "audio/ogg", - "ogg": "application/ogg", - "ogm": "application/ogg", - "ogv": "video/ogg", - "pbm": "image/x-portable-bitmap", - "pdf": "application/pdf", - "pgm": "image/x-portable-graymap", - "pgn": "application/x-chess-pgn", - "pgp": "application/pgp", - "pm": "application/x-perl", - "png": "image/png", - "pnm": "image/x-portable-anymap", - "ppm": "image/x-portable-pixmap", - "ppz": "application/vnd.ms-powerpoint", - "pre": "application/x-freelance", - "prt": "application/pro_eng", - "ps": "application/postscript", - "qt": "video/quicktime", - "ra": "audio/x-realaudio", - "rar": "application/x-rar-compressed", - "ras": "image/x-cmu-raster", - "rgb": "image/x-rgb", - "rm": "audio/x-pn-realaudio", - "rpm": "audio/x-pn-realaudio-plugin", - "rtf": "text/rtf", - "rtx": "text/richtext", - "scm": "application/x-lotusscreencam", - "set": "application/set", - "sgml": "text/sgml", - "sh": "application/x-sh", - "shar": "application/x-shar", - "silo": "model/mesh", - "sit": "application/x-stuffit", - "skt": "application/x-koan", - "smil": "application/smil", - "snd": "audio/basic", - "sol": "application/solids", - "spl": "application/x-futuresplash", - "src": "application/x-wais-source", - "stl": "application/SLA", - "stp": "application/STEP", - "sv4cpio": "application/x-sv4cpio", - "sv4crc": "application/x-sv4crc", - "svg": "image/svg+xml", - "swf": "application/x-shockwave-flash", - "tar": "application/x-tar", - "tcl": "application/x-tcl", - "tex": "application/x-tex", - "texinfo": "application/x-texinfo", - "tgz": "application/x-tar-gz", - "tiff": "image/tiff", - "tr": "application/x-troff", - "tsi": "audio/TSP-audio", - "tsp": "application/dsptype", - "tsv": "text/tab-separated-values", - "txt": "text/plain", - "unv": "application/i-deas", - "ustar": "application/x-ustar", - "vcd": "application/x-cdlink", - "vda": "application/vda", - "vivo": "video/vnd.vivo", - "vrm": "x-world/x-vrml", - "wav": "audio/x-wav", - "wax": "audio/x-ms-wax", - "wma": "audio/x-ms-wma", - "wmv": "video/x-ms-wmv", - "wmx": "video/x-ms-wmx", - "wrl": "model/vrml", - "wvx": "video/x-ms-wvx", - "xbm": "image/x-xbitmap", - "xlw": "application/vnd.ms-excel", - "xml": "text/xml", - "xpm": "image/x-xpixmap", - "xwd": "image/x-xwindowdump", - "xyz": "chemical/x-pdb", - "zip": "application/zip" -}; - -exports.addContentType = function(extension, contentType){ - exports.contentTypes[extension] = contentType; -}; diff --git a/package.json b/package.json index c052cb8..e7b6616 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "license" : "MIT", "dependencies" : { "optimist": ">=0.3.4", - "colors": ">=0.6.0" + "colors": ">=0.6.0", + "mime": ">=1.2.9" }, "devDependencies" : { "request": "latest", diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index 7559a68..5cfacf1 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -16,13 +16,11 @@ suite.addBatch({ 'once an http server is listening with a callback': { topic: function () { server = require('http').createServer(function (request, response) { - request.addListener('end', function () { - fileServer.serve(request, response, function(err, result) { - if (callback) - callback(request, response, err, result); - else - request.end(); - }); + fileServer.serve(request, response, function(err, result) { + if (callback) + callback(request, response, err, result); + else + request.end(); }); }).listen(TEST_PORT, this.callback) }, @@ -58,9 +56,7 @@ suite.addBatch({ topic: function () { server.close(); server = require('http').createServer(function (request, response) { - request.addListener('end', function () { - fileServer.serve(request, response); - }); + fileServer.serve(request, response); }).listen(TEST_PORT, this.callback) }, 'should be listening' : function(){ @@ -78,7 +74,8 @@ suite.addBatch({ assert.equal(response.statusCode, 404); } } -}).addBatch({ +}) +/*.addBatch({ 'requesting a malformed URI': { topic: function(){ request.get(TEST_SERVER + '/a%AFc', this.callback); @@ -87,7 +84,9 @@ suite.addBatch({ assert.equal(response.statusCode, 400); } } -}).addBatch({ +}) +*/ +.addBatch({ 'serving hello.txt': { topic : function(){ request.get(TEST_SERVER + '/hello.txt', this.callback); @@ -182,18 +181,18 @@ suite.addBatch({ topic : function(){ request.head(TEST_SERVER + '/index.html', this.callback); }, - 'should respond with node-static/0.6.0' : function(error, response, body){ - assert.equal(response.headers['server'], 'custom-server-name'); + 'should respond with node-static/0.6.7' : function(error, response, body){ + assert.equal(response.headers['server'], 'node-static/0.6.7'); } } }).addBatch({ 'addings custom mime types': { topic : function(){ - static.mime.addContentType('woff', 'application/font-woff'); + static.mime.define({'application/font-woff': ['woff']}); this.callback(); }, 'should add woff' : function(error, response, body){ - assert.equal(static.mime.contentTypes['woff'], 'application/font-woff'); + assert.equal(static.mime.lookup('woff'), 'application/font-woff'); } } }).export(module); From a2a3371e15ccb1a865daed8df1253de1e096ae5c Mon Sep 17 00:00:00 2001 From: David Sargeant Date: Mon, 1 Apr 2013 12:05:21 -0400 Subject: [PATCH 015/235] Cleaned up exports in main file. Corrected package version number. Added decodeURI test back in. --- lib/node-static.js | 62 +++++++++++++++++----------- package.json | 2 +- test/integration/node-static-test.js | 30 ++++++++------ 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 267395f..098d87f 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -4,16 +4,15 @@ var fs = require('fs') , http = require('http') , url = require('url') , path = require('path') - , mime = require('mime'); + , mime = require('mime') + , util = require('./node-static/util'); -exports.version = [0, 6, 7]; - -var util = require('./node-static/util'); +// Current version +var version = [0, 6, 7]; // In-memory file store -exports.store = {}; -exports.indexStore = {}; -exports.mime = mime; +var store = {}; +var indexStore = {}; Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } @@ -36,7 +35,7 @@ Server = function (root, options) { if ('serverInfo' in this.options) { this.serverInfo = this.options.serverInfo.toString(); } else { - this.serverInfo = 'node-static/' + exports.version.join('.'); + this.serverInfo = 'node-static/' + version.join('.'); } this.defaultHeaders['server'] = this.serverInfo; @@ -59,14 +58,14 @@ Server.prototype.serveDir = function (pathname, req, res, finish) { if (!e) { that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish); } else { - if (pathname in exports.indexStore) { - streamFiles(exports.indexStore[pathname].files); + if (pathname in indexStore) { + streamFiles(indexStore[pathname].files); } else { // Stream a directory of files as a single file. fs.readFile(path.join(pathname, 'index.json'), function (e, contents) { if (e) { return finish(404, {}) } var index = JSON.parse(contents); - exports.indexStore[pathname] = index; + indexStore[pathname] = index; streamFiles(index.files); }); } @@ -168,14 +167,22 @@ Server.prototype.resolve = function (pathname) { }; Server.prototype.serve = function (req, res, callback) { - var that = this, - promise = new(events.EventEmitter); + var that = this, + promise = new(events.EventEmitter), + pathname; - var pathname = decodeURI(url.parse(req.url).pathname); - var finish = function (status, headers) { that.finish(status, headers, req, res, promise, callback); }; + + try { + pathname = decodeURI(url.parse(req.url).pathname); + } + catch(e) { + return process.nextTick(function() { + return finish(400, {}); + }); + } process.nextTick(function () { that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) { @@ -188,9 +195,9 @@ Server.prototype.serve = function (req, res, callback) { }; Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { - var mtime = Date.parse(stat.mtime), - key = pathname || files[0], - headers = {}, + var mtime = Date.parse(stat.mtime), + key = pathname || files[0], + headers = {}, clientETag = req.headers['if-none-match'], clientMTime = Date.parse(req.headers['if-modified-since']); @@ -224,14 +231,14 @@ Server.prototype.respond = function (pathname, status, _headers, files, stat, re // If the file was cached and it's not older // than what's on disk, serve the cached version. - if (this.cache && (key in exports.store) && - exports.store[key].stat.mtime >= stat.mtime) { - res.end(exports.store[key].buffer); + if (this.cache && (key in store) && + store[key].stat.mtime >= stat.mtime) { + res.end(store[key].buffer); finish(status, headers); } else { this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) { if (e) { return finish(500, {}) } - exports.store[key] = { + store[key] = { stat: stat, buffer: buffer, timestamp: Date.now() @@ -269,4 +276,13 @@ Server.prototype.stream = function (pathname, files, buffer, res, callback) { })(files.slice(0), 0); }; -exports.Server = Server; +// Exports +exports.Server = Server; +exports.version = version; +exports.mime = mime; +exports.store = store; +exports.indexStore = indexStore; + + + + diff --git a/package.json b/package.json index e7b6616..5a92e26 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "request": "latest", "vows": "latest" }, - "version" : "0.6.5", + "version" : "0.6.7", "engines" : { "node": ">= 0.4.1" } } diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index 5cfacf1..0387e67 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -7,10 +7,22 @@ var fileServer = new(static.Server)(__dirname + '/../fixtures', {serverInfo: 'cu var suite = vows.describe('node-static'); -var TEST_PORT = 8080; +var TEST_PORT = 8080; var TEST_SERVER = 'http://localhost:' + TEST_PORT; var server; var callback; +var version = static.version.join('.'); + +headers = { + 'requesting headers': { + topic : function(){ + request.head(TEST_SERVER + '/index.html', this.callback); + } + } +} +headers['requesting headers']['should respond with node-static/' + version] = function(error, response, body){ + assert.equal(response.headers['server'], 'node-static/' + version); +} suite.addBatch({ 'once an http server is listening with a callback': { @@ -75,7 +87,7 @@ suite.addBatch({ } } }) -/*.addBatch({ +.addBatch({ 'requesting a malformed URI': { topic: function(){ request.get(TEST_SERVER + '/a%AFc', this.callback); @@ -85,7 +97,6 @@ suite.addBatch({ } } }) -*/ .addBatch({ 'serving hello.txt': { topic : function(){ @@ -176,16 +187,9 @@ suite.addBatch({ assert.isUndefined(body); } } -}).addBatch({ - 'requesting headers': { - topic : function(){ - request.head(TEST_SERVER + '/index.html', this.callback); - }, - 'should respond with node-static/0.6.7' : function(error, response, body){ - assert.equal(response.headers['server'], 'node-static/0.6.7'); - } - } -}).addBatch({ +}) +.addBatch(headers) +.addBatch({ 'addings custom mime types': { topic : function(){ static.mime.define({'application/font-woff': ['woff']}); From 3a74c45561c8f559de7187a6f9b116f8b7ff6196 Mon Sep 17 00:00:00 2001 From: David Sargeant Date: Mon, 1 Apr 2013 12:20:31 -0400 Subject: [PATCH 016/235] Fixed server name test. Issue was with file server options. --- test/integration/node-static-test.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index 0387e67..c0f2e84 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -3,15 +3,13 @@ var vows = require('vows') , assert = require('assert') , static = require('../../lib/node-static'); -var fileServer = new(static.Server)(__dirname + '/../fixtures', {serverInfo: 'custom-server-name'}); - -var suite = vows.describe('node-static'); - +var fileServer = new static.Server(__dirname + '/../fixtures'); +var suite = vows.describe('node-static'); var TEST_PORT = 8080; var TEST_SERVER = 'http://localhost:' + TEST_PORT; +var version = static.version.join('.'); var server; var callback; -var version = static.version.join('.'); headers = { 'requesting headers': { From 5a1a08ded1b7fa11cc073572502d4ee26f177662 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Mon, 1 Apr 2013 21:47:59 -0300 Subject: [PATCH 017/235] Bumped version number to 6.0.8 --- lib/node-static.js | 12 +++++------ package.json | 54 +++++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 098d87f..33aac7f 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -8,7 +8,7 @@ var fs = require('fs') , util = require('./node-static/util'); // Current version -var version = [0, 6, 7]; +var version = [0, 6, 8]; // In-memory file store var store = {}; @@ -23,7 +23,7 @@ Server = function (root, options) { this.defaultHeaders = {}; this.options.headers = this.options.headers || {}; - + if ('cache' in this.options) { if (typeof(this.options.cache) === 'number') { this.cache = this.options.cache; @@ -43,7 +43,7 @@ Server = function (root, options) { if (this.cache !== false) { this.defaultHeaders['cache-control'] = 'max-age=' + this.cache; } - + for (var k in this.defaultHeaders) { this.options.headers[k] = this.options.headers[k] || this.defaultHeaders[k]; @@ -82,7 +82,7 @@ Server.prototype.serveDir = function (pathname, req, res, finish) { Server.prototype.serveFile = function (pathname, status, headers, req, res) { var that = this; var promise = new(events.EventEmitter); - + pathname = this.resolve(pathname); fs.stat(pathname, function (e, stat) { @@ -170,11 +170,11 @@ Server.prototype.serve = function (req, res, callback) { var that = this, promise = new(events.EventEmitter), pathname; - + var finish = function (status, headers) { that.finish(status, headers, req, res, promise, callback); }; - + try { pathname = decodeURI(url.parse(req.url).pathname); } diff --git a/package.json b/package.json index 5a92e26..525129c 100644 --- a/package.json +++ b/package.json @@ -5,33 +5,33 @@ "keywords" : ["http", "static", "file", "server"], "author" : "Alexis Sellier ", "contributors" : [ - { - "name": "Pablo Cantero", - "email": "pablo@pablocantero.com" - } + { + "name": "Pablo Cantero", + "email": "pablo@pablocantero.com" + } ], - "repository": { - "type": "git", - "url": "http://github.com/cloudhead/node-static" - }, - "main" : "./lib/node-static", - "scripts": { - "test": "vows --spec --isolate" - }, - "bin": { - "static": "bin/cli.js" - }, - "license" : "MIT", - "dependencies" : { - "optimist": ">=0.3.4", - "colors": ">=0.6.0", - "mime": ">=1.2.9" - }, - "devDependencies" : { - "request": "latest", - "vows": "latest" - }, - "version" : "0.6.7", - "engines" : { "node": ">= 0.4.1" } + "repository": { + "type": "git", + "url": "http://github.com/cloudhead/node-static" + }, + "main" : "./lib/node-static", + "scripts": { + "test": "vows --spec --isolate" + }, + "bin": { + "static": "bin/cli.js" + }, + "license" : "MIT", + "dependencies" : { + "optimist": ">=0.3.4", + "colors": ">=0.6.0", + "mime": ">=1.2.9" + }, + "devDependencies" : { + "request": "latest", + "vows": "latest" + }, + "version" : "0.6.8", + "engines" : { "node": ">= 0.4.1" } } From 5e02b211ff14230854126322b83d21b42f975b71 Mon Sep 17 00:00:00 2001 From: Aaron Stacy Date: Tue, 2 Apr 2013 11:32:47 -0500 Subject: [PATCH 018/235] call ReadableStream#resume() on the request in node v0.10, [you must call `resume()` in order to get the 'end' event][node_zero_ten]. this adds the call on the request handler. this adds the `resume()` call to the cli. it shouldn't cause any backwards-incompatible changes. [node_zero_ten]: http://blog.nodejs.org/2012/12/20/streams2/ --- bin/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/cli.js b/bin/cli.js index b678643..632b112 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -97,7 +97,7 @@ require('http').createServer(function (request, response) { log(request, response); } }); - }); + }).resume(); }).listen(+argv.port); console.log('serving "' + dir + '" at http://127.0.0.1:' + argv.port); From 779b689ad9dc3be86f3efdf1c7c7e82af9f04555 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Thu, 11 Apr 2013 15:11:29 -0300 Subject: [PATCH 019/235] Bumped version number to 0.6.9 --- lib/node-static.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 33aac7f..7784c04 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -8,7 +8,7 @@ var fs = require('fs') , util = require('./node-static/util'); // Current version -var version = [0, 6, 8]; +var version = [0, 6, 9]; // In-memory file store var store = {}; diff --git a/package.json b/package.json index 525129c..5091077 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "request": "latest", "vows": "latest" }, - "version" : "0.6.8", + "version" : "0.6.9", "engines" : { "node": ">= 0.4.1" } } From 0294a0eb8c7d48214445575635d152bca93e19f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Sch=C3=BCrmann?= Date: Mon, 22 Apr 2013 15:52:47 +0300 Subject: [PATCH 020/235] Update README.md Making the examples a little more copy and paste friendly --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 236ef03..1db91f2 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ This is the default setting. To serve files under a directory, simply call the `serve` method on a `Server` instance, passing it the HTTP request and response object: + + var static = require('node-static'); var fileServer = new static.Server('./public'); @@ -87,6 +89,8 @@ More on intercepting errors bellow. An optional callback can be passed as last argument, it will be called every time a file has been served successfully, or if there was an error serving the file: + var static = require('node-static'); + var fileServer = new static.Server('./public'); require('http').createServer(function (request, response) { From a364b0cc9433e182a37f764eb003b43a51fc3e9d Mon Sep 17 00:00:00 2001 From: Kay Date: Wed, 1 May 2013 23:09:42 -0700 Subject: [PATCH 021/235] Send custom headers even on a 304 Not Modified case When a 304 (Not Modified) response is sent, user defined headers are not sent. Only default headers are. In my case scenario, I need to update the "Set-Cookie" header to extend it every time a user accesses a page. So, even though the page has not changed, therefore generating a 304 response, I still need to send "Set-Cookie" header with a new expiration timestamp. --- lib/node-static.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 7784c04..2381590 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -203,6 +203,8 @@ Server.prototype.respond = function (pathname, status, _headers, files, stat, re // Copy default headers for (var k in this.options.headers) { headers[k] = this.options.headers[k] } + // Copy custom headers + for (var k in _headers) { headers[k] = _headers[k] } headers['etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); headers['date'] = new(Date)().toUTCString(); @@ -220,8 +222,6 @@ Server.prototype.respond = function (pathname, status, _headers, files, stat, re headers['content-type'] = mime.lookup(files[0]); 'application/octet-stream'; - for (var k in _headers) { headers[k] = _headers[k] } - res.writeHead(status, headers); if (req.method === 'HEAD') { From 8946c08d4dd6af0e340df04d276812c04d8804f9 Mon Sep 17 00:00:00 2001 From: joshlangner Date: Mon, 20 May 2013 17:08:53 -0300 Subject: [PATCH 022/235] added '.resume()' to end of addListener because it's required for node 10.x+ --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1db91f2..7a7b647 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Synopsis // Serve files! // file.serve(request, response); - }); + }).resume(); }).listen(8080); API @@ -60,7 +60,7 @@ the HTTP request and response object: require('http').createServer(function (request, response) { request.addListener('end', function () { fileServer.serve(request, response); - }); + }).resume(); }).listen(8080); ### Serving specific files # @@ -79,7 +79,7 @@ For example, you could serve an error page, when the initial request wasn't foun fileServer.serveFile('/not-found.html', 404, {}, request, response); } }); - }); + }).resume(); }).listen(8080); More on intercepting errors bellow. @@ -104,7 +104,7 @@ has been served successfully, or if there was an error serving the file: response.end(); } }); - }); + }).resume(); }).listen(8080); Note that if you pass a callback, and there is an error serving the file, node-static From 6af107cad1fc28a7e14f5d5391f2c89d529b4e11 Mon Sep 17 00:00:00 2001 From: TeeTeeHaa Date: Tue, 28 May 2013 23:13:26 +0200 Subject: [PATCH 023/235] improved fix and added test cases improved issue regarding missing trailing slash in URL when using default file "index.html" and added test cases --- lib/node-static.js | 6 ++-- test/fixtures/there/index.html | 8 +++++ test/integration/node-static-test.js | 49 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/there/index.html diff --git a/lib/node-static.js b/lib/node-static.js index 358ceef..e48263f 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -60,10 +60,10 @@ Server.prototype.serveDir = function (pathname, req, res, finish) { var headers = {}; var originalPathname = decodeURI(url.parse(req.url).pathname); if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') { - status = 301; - headers['Location'] = originalPathname + '/'; + return finish(301, { 'Location': originalPathname + '/' }); + } else { + that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); } - that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); } else { if (pathname in indexStore) { streamFiles(indexStore[pathname].files); diff --git a/test/fixtures/there/index.html b/test/fixtures/there/index.html new file mode 100644 index 0000000..6798b09 --- /dev/null +++ b/test/fixtures/there/index.html @@ -0,0 +1,8 @@ + + + Other page + + + hello there! + + diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index c0f2e84..c1e5d39 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -197,4 +197,53 @@ suite.addBatch({ assert.equal(static.mime.lookup('woff'), 'application/font-woff'); } } +}) +.addBatch({ + 'serving subdirectory index': { + topic : function(){ + request.get(TEST_SERVER + '/there/', this.callback); // with trailing slash + }, + 'should respond with 200' : function(error, response, body){ + assert.equal(response.statusCode, 200); + }, + 'should respond with text/html': function(error, response, body){ + assert.equal(response.headers['content-type'], 'text/html'); + } + } +}) +.addBatch({ + 'redirecting to subdirectory index': { + topic : function(){ + request.get({ url: TEST_SERVER + '/there', followRedirect: false }, this.callback); // without trailing slash + }, + 'should respond with 301' : function(error, response, body){ + assert.equal(response.statusCode, 301); + }, + 'should respond with location header': function(error, response, body){ + assert.equal(response.headers['location'], '/there/'); // now with trailing slash + }, + 'should respond with empty string body' : function(error, response, body){ + assert.equal(body, ''); + } + } +}) +.addBatch({ + 'requesting a subdirectory (with trailing slash) not found': { + topic : function(){ + request.get(TEST_SERVER + '/notthere/', this.callback); // with trailing slash + }, + 'should respond with 404' : function(error, response, body){ + assert.equal(response.statusCode, 404); + } + } +}) +.addBatch({ + 'requesting a subdirectory (without trailing slash) not found': { + topic : function(){ + request.get({ url: TEST_SERVER + '/notthere', followRedirect: false }, this.callback); // without trailing slash + }, + 'should respond with 404' : function(error, response, body){ + assert.equal(response.statusCode, 404); + } + } }).export(module); From 16babca6ead382026d1dea24f9f3033393c75859 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 2 Nov 2011 13:11:08 +0800 Subject: [PATCH 024/235] Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. --- lib/node-static.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/lib/node-static.js b/lib/node-static.js index e48263f..9300dc0 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -201,21 +201,78 @@ Server.prototype.serve = function (req, res, callback) { if (! callback) { return promise } }; +<<<<<<< HEAD Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var mtime = Date.parse(stat.mtime), key = pathname || files[0], headers = {}, clientETag = req.headers['if-none-match'], clientMTime = Date.parse(req.headers['if-modified-since']); +======= +/* Check if we should consider sending a gzip version of the file based on the + * file content type and client's Accept-Encoding header value. + */ +this.Server.prototype.gzipOk = function(req, contentType) { + var enable = this.options.gzip; + if(enable && + (typeof enable === 'boolean' || + (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { + var acceptEncoding = req.headers['accept-encoding']; + return acceptEncoding.indexOf("gzip") >= 0; + } + return false; +} + +/* Send a gzipped version of the file if the options and the client indicate gzip is enabled and + * we find a .gz file matching the static resource requested. + */ +this.Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { + var that = this; + if(files.length == 1 && this.gzipOk(req)) { + var gzFile = files[0] + ".gz"; + fs.stat(gzFile, function(e, gzStat) { + if(!e && gzStat.isFile()) { + //console.log('Serving', gzFile, 'to gzip-capable client instead of', files[0], 'new size is', gzStat.size, 'uncompressed size', stat.size); + var vary = _headers['Vary']; + _headers['Vary'] = (vary && vary != 'Accept-Encoding'?vary+', ':'')+'Accept-Encoding'; + _headers['Content-Encoding'] = 'gzip'; + stat.size = gzStat.size; + files = [gzFile]; + } else { + // console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile()); + } + that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + }); + } else { + // Client doesn't want gzip or we're sending multiple files + that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + } +} + +this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { + var mtime = Date.parse(stat.mtime), + key = pathname || files[0], + headers = {}; +>>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. // Copy default headers for (var k in this.options.headers) { headers[k] = this.options.headers[k] } // Copy custom headers for (var k in _headers) { headers[k] = _headers[k] } +<<<<<<< HEAD headers['etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); headers['date'] = new(Date)().toUTCString(); headers['last-modified'] = new(Date)(stat.mtime).toUTCString(); +======= + headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); + headers['Date'] = new(Date)().toUTCString(); + headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString(); + headers['Content-Type'] = contentType; + headers['Content-Length'] = stat.size; + + for (var k in _headers) { headers[k] = _headers[k] } +>>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. // Conditional GET // If the "If-Modified-Since" or "If-None-Match" headers @@ -225,9 +282,12 @@ Server.prototype.respond = function (pathname, status, _headers, files, stat, re (!clientMTime || clientMTime >= mtime)) { finish(304, headers); } else { +<<<<<<< HEAD headers['content-length'] = stat.size; headers['content-type'] = mime.lookup(files[0]); 'application/octet-stream'; +======= +>>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. res.writeHead(status, headers); @@ -256,7 +316,22 @@ Server.prototype.respond = function (pathname, status, _headers, files, stat, re } }; +<<<<<<< HEAD Server.prototype.stream = function (pathname, files, buffer, res, callback) { +======= +this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { + var contentType = _headers['Content-Type'] || + mime.contentTypes[path.extname(files[0]).slice(1)] || + 'application/octet-stream'; + if(this.options.gzip) { + this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + } else { + this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + } +} + +this.Server.prototype.stream = function (pathname, files, buffer, res, callback) { +>>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. (function streamFile(files, offset) { var file = files.shift(); From d9b968cbce63960f8c2abe5e105595ade582a3e6 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 2 Nov 2011 13:18:48 +0800 Subject: [PATCH 025/235] Updated the README to describe gzip configuration --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 7a7b647..c681804 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ example: `{ 'X-Hello': 'World!' }` > defaults to `{}` +<<<<<<< HEAD Command Line Interface ---------------------- @@ -181,3 +182,24 @@ Command Line Interface # show help message, including all options $ static -h +======= +#### `gzip` # + +Enable support for sending compressed responses. This will enable a check for a +file with the same name plus '.gz' in the same folder. If the compressed file is +found and the client has indicated support for gzip file transfer, the contents +of the .gz file will be sent in place of the uncompressed file along with a +Content-Encoding: gzip header to inform the client the data has been compressed. + +example: `{ gzip: true }` +example: `{ gzip: /^\/text/ }` + +Passing `true` will enable this check for all files. +Passing a RegExp instance will only enable this check if the content-type of the +respond would match that RegExp using its test() method. + +> Defaults to `false` + + + +>>>>>>> Updated the README to describe gzip configuration From 507e62e7585b4c02a2f9c9da124d2badcb02c920 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 2 Nov 2011 18:16:27 +0800 Subject: [PATCH 026/235] Remove in-memory cache --- lib/node-static.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 9300dc0..1119f72 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -10,12 +10,17 @@ var fs = require('fs') // Current version var version = [0, 6, 9]; +<<<<<<< HEAD // In-memory file store var store = {}; var indexStore = {}; Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } +======= +this.Server = function (root, options) { + if (root && (typeof(root) === 'object')) { options = root, root = null } +>>>>>>> Remove in-memory cache this.root = path.resolve(root || '.'); this.options = options || {}; @@ -65,6 +70,7 @@ Server.prototype.serveDir = function (pathname, req, res, finish) { that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); } } else { +<<<<<<< HEAD if (pathname in indexStore) { streamFiles(indexStore[pathname].files); } else { @@ -76,6 +82,14 @@ Server.prototype.serveDir = function (pathname, req, res, finish) { streamFiles(index.files); }); } +======= + // Stream a directory of files as a single file. + fs.readFile(path.join(pathname, 'index.json'), function (e, contents) { + if (e) { return finish(404, {}) } + var index = JSON.parse(contents); + streamFiles(index.files); + }); +>>>>>>> Remove in-memory cache } }); function streamFiles(files) { @@ -224,7 +238,7 @@ this.Server.prototype.gzipOk = function(req, contentType) { } /* Send a gzipped version of the file if the options and the client indicate gzip is enabled and - * we find a .gz file matching the static resource requested. + * we find a .gz file mathing the static resource requested. */ this.Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { var that = this; @@ -282,6 +296,7 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ (!clientMTime || clientMTime >= mtime)) { finish(304, headers); } else { +<<<<<<< HEAD <<<<<<< HEAD headers['content-length'] = stat.size; headers['content-type'] = mime.lookup(files[0]); @@ -313,6 +328,14 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ finish(status, headers); }); } +======= + res.writeHead(status, headers); + + this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) { + if (e) { return finish(500, {}) } + finish(status, headers); + }); +>>>>>>> Remove in-memory cache } }; From 826e2b13c3fd685792176d42e3c0a0f21b80a8ca Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 2 Nov 2011 22:55:20 +0800 Subject: [PATCH 027/235] Fix check for acceptEncoding header to work if there is no accept-encoding header --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 1119f72..a862f26 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -232,7 +232,7 @@ this.Server.prototype.gzipOk = function(req, contentType) { (typeof enable === 'boolean' || (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { var acceptEncoding = req.headers['accept-encoding']; - return acceptEncoding.indexOf("gzip") >= 0; + return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0; } return false; } From 7419a62db10f2a1cd92ad16b4e31d56b15d82de4 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 2 Nov 2011 22:57:07 +0800 Subject: [PATCH 028/235] Fix check for acceptEncoding header to work if there is no accept-encoding header --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index a862f26..dbb6d38 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -231,7 +231,7 @@ this.Server.prototype.gzipOk = function(req, contentType) { if(enable && (typeof enable === 'boolean' || (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { - var acceptEncoding = req.headers['accept-encoding']; + var acceptEncoding && acceptEncoding = req.headers['accept-encoding']; return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0; } return false; From e56d0df6b85edbde97c143f16431967f461b93ef Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Sat, 12 Nov 2011 10:10:59 +0800 Subject: [PATCH 029/235] Fix error with accept-encoding heaer check. Fix bug where Content-Length was sent with a 304 not modified response. --- lib/node-static.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index dbb6d38..9a41b15 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -231,7 +231,7 @@ this.Server.prototype.gzipOk = function(req, contentType) { if(enable && (typeof enable === 'boolean' || (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { - var acceptEncoding && acceptEncoding = req.headers['accept-encoding']; + var acceptEncoding = req.headers['accept-encoding']; return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0; } return false; @@ -253,7 +253,7 @@ this.Server.prototype.respondGzip = function(pathname, status, contentType, _hea stat.size = gzStat.size; files = [gzFile]; } else { - // console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile()); + //console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile()); } that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); }); @@ -291,9 +291,26 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ // Conditional GET // If the "If-Modified-Since" or "If-None-Match" headers // match the conditions, send a 304 Not Modified. +<<<<<<< HEAD if ((clientMTime || clientETag) && (!clientETag || clientETag === headers['etag']) && (!clientMTime || clientMTime >= mtime)) { +======= + if (req.headers['if-none-match'] === headers['Etag'] || + Date.parse(req.headers['if-modified-since']) >= mtime) { + // 304 response should not contain entity headers + ['Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-MD5', + 'Content-Range', + 'Content-Type', + 'Expires', + 'Last-Modified'].forEach(function(entityHeader) { + delete headers[entityHeader]; + }); +>>>>>>> Fix error with accept-encoding heaer check. Fix bug where Content-Length was sent with a 304 not modified response. finish(304, headers); } else { <<<<<<< HEAD From 625151545dbcc8a634a868b33c2322dbd22144d3 Mon Sep 17 00:00:00 2001 From: thbaja Date: Wed, 19 Jun 2013 15:37:06 +0200 Subject: [PATCH 030/235] Fixed merge issues --- lib/node-static.js | 82 ---------------------------------------------- 1 file changed, 82 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 9a41b15..51bf3fd 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -10,17 +10,8 @@ var fs = require('fs') // Current version var version = [0, 6, 9]; -<<<<<<< HEAD -// In-memory file store -var store = {}; -var indexStore = {}; - -Server = function (root, options) { - if (root && (typeof(root) === 'object')) { options = root; root = null } -======= this.Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root, root = null } ->>>>>>> Remove in-memory cache this.root = path.resolve(root || '.'); this.options = options || {}; @@ -70,26 +61,12 @@ Server.prototype.serveDir = function (pathname, req, res, finish) { that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); } } else { -<<<<<<< HEAD - if (pathname in indexStore) { - streamFiles(indexStore[pathname].files); - } else { - // Stream a directory of files as a single file. - fs.readFile(path.join(pathname, 'index.json'), function (e, contents) { - if (e) { return finish(404, {}) } - var index = JSON.parse(contents); - indexStore[pathname] = index; - streamFiles(index.files); - }); - } -======= // Stream a directory of files as a single file. fs.readFile(path.join(pathname, 'index.json'), function (e, contents) { if (e) { return finish(404, {}) } var index = JSON.parse(contents); streamFiles(index.files); }); ->>>>>>> Remove in-memory cache } }); function streamFiles(files) { @@ -215,14 +192,6 @@ Server.prototype.serve = function (req, res, callback) { if (! callback) { return promise } }; -<<<<<<< HEAD -Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { - var mtime = Date.parse(stat.mtime), - key = pathname || files[0], - headers = {}, - clientETag = req.headers['if-none-match'], - clientMTime = Date.parse(req.headers['if-modified-since']); -======= /* Check if we should consider sending a gzip version of the file based on the * file content type and client's Accept-Encoding header value. */ @@ -267,18 +236,12 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ var mtime = Date.parse(stat.mtime), key = pathname || files[0], headers = {}; ->>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. // Copy default headers for (var k in this.options.headers) { headers[k] = this.options.headers[k] } // Copy custom headers for (var k in _headers) { headers[k] = _headers[k] } -<<<<<<< HEAD - headers['etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); - headers['date'] = new(Date)().toUTCString(); - headers['last-modified'] = new(Date)(stat.mtime).toUTCString(); -======= headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); headers['Date'] = new(Date)().toUTCString(); headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString(); @@ -286,16 +249,10 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ headers['Content-Length'] = stat.size; for (var k in _headers) { headers[k] = _headers[k] } ->>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. // Conditional GET // If the "If-Modified-Since" or "If-None-Match" headers // match the conditions, send a 304 Not Modified. -<<<<<<< HEAD - if ((clientMTime || clientETag) && - (!clientETag || clientETag === headers['etag']) && - (!clientMTime || clientMTime >= mtime)) { -======= if (req.headers['if-none-match'] === headers['Etag'] || Date.parse(req.headers['if-modified-since']) >= mtime) { // 304 response should not contain entity headers @@ -310,55 +267,17 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ 'Last-Modified'].forEach(function(entityHeader) { delete headers[entityHeader]; }); ->>>>>>> Fix error with accept-encoding heaer check. Fix bug where Content-Length was sent with a 304 not modified response. finish(304, headers); } else { -<<<<<<< HEAD -<<<<<<< HEAD - headers['content-length'] = stat.size; - headers['content-type'] = mime.lookup(files[0]); - 'application/octet-stream'; -======= ->>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. - - res.writeHead(status, headers); - - if (req.method === 'HEAD') { - finish(200, headers); - return; - } - - // If the file was cached and it's not older - // than what's on disk, serve the cached version. - if (this.cache && (key in store) && - store[key].stat.mtime >= stat.mtime) { - res.end(store[key].buffer); - finish(status, headers); - } else { - this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) { - if (e) { return finish(500, {}) } - store[key] = { - stat: stat, - buffer: buffer, - timestamp: Date.now() - }; - finish(status, headers); - }); - } -======= res.writeHead(status, headers); this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) { if (e) { return finish(500, {}) } finish(status, headers); }); ->>>>>>> Remove in-memory cache } }; -<<<<<<< HEAD -Server.prototype.stream = function (pathname, files, buffer, res, callback) { -======= this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var contentType = _headers['Content-Type'] || mime.contentTypes[path.extname(files[0]).slice(1)] || @@ -371,7 +290,6 @@ this.Server.prototype.respond = function (pathname, status, _headers, files, sta } this.Server.prototype.stream = function (pathname, files, buffer, res, callback) { ->>>>>>> Add static gzip file support, where a gzip file with a matching name but adding .gz to the end can be served up to clients that support it. As part of that, changed the code so the Content-Type and Content-Length are sent even for an HTTP HEAD request. (function streamFile(files, offset) { var file = files.shift(); From 818f5d6777a0b2580afc07ff1f8e31823a3546ac Mon Sep 17 00:00:00 2001 From: thbaja Date: Wed, 19 Jun 2013 15:42:45 +0200 Subject: [PATCH 031/235] Fixed more merge issues --- lib/node-static.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 51bf3fd..9c77c31 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -10,7 +10,7 @@ var fs = require('fs') // Current version var version = [0, 6, 9]; -this.Server = function (root, options) { +Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root, root = null } this.root = path.resolve(root || '.'); @@ -195,7 +195,7 @@ Server.prototype.serve = function (req, res, callback) { /* Check if we should consider sending a gzip version of the file based on the * file content type and client's Accept-Encoding header value. */ -this.Server.prototype.gzipOk = function(req, contentType) { +Server.prototype.gzipOk = function(req, contentType) { var enable = this.options.gzip; if(enable && (typeof enable === 'boolean' || @@ -209,7 +209,7 @@ this.Server.prototype.gzipOk = function(req, contentType) { /* Send a gzipped version of the file if the options and the client indicate gzip is enabled and * we find a .gz file mathing the static resource requested. */ -this.Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { +Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { var that = this; if(files.length == 1 && this.gzipOk(req)) { var gzFile = files[0] + ".gz"; @@ -232,7 +232,7 @@ this.Server.prototype.respondGzip = function(pathname, status, contentType, _hea } } -this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { +Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { var mtime = Date.parse(stat.mtime), key = pathname || files[0], headers = {}; @@ -278,7 +278,7 @@ this.Server.prototype.respondNoGzip = function (pathname, status, contentType, _ } }; -this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { +Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var contentType = _headers['Content-Type'] || mime.contentTypes[path.extname(files[0]).slice(1)] || 'application/octet-stream'; @@ -289,7 +289,7 @@ this.Server.prototype.respond = function (pathname, status, _headers, files, sta } } -this.Server.prototype.stream = function (pathname, files, buffer, res, callback) { +Server.prototype.stream = function (pathname, files, buffer, res, callback) { (function streamFile(files, offset) { var file = files.shift(); @@ -320,9 +320,6 @@ this.Server.prototype.stream = function (pathname, files, buffer, res, callback) exports.Server = Server; exports.version = version; exports.mime = mime; -exports.store = store; -exports.indexStore = indexStore; - From 36dc437c763ff99813c11cb0d950566ef32bc8fd Mon Sep 17 00:00:00 2001 From: thbaja Date: Wed, 19 Jun 2013 15:43:28 +0200 Subject: [PATCH 032/235] Updated syntax for ext. library (mime) --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 9c77c31..04603be 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -280,7 +280,7 @@ Server.prototype.respondNoGzip = function (pathname, status, contentType, _heade Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var contentType = _headers['Content-Type'] || - mime.contentTypes[path.extname(files[0]).slice(1)] || + mime.lookup[path.extname(files[0]).slice(1)] || 'application/octet-stream'; if(this.options.gzip) { this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); From 6f57aea29619831b8d32e24a8405aba5c55ee6a7 Mon Sep 17 00:00:00 2001 From: thbaja Date: Wed, 19 Jun 2013 16:07:47 +0200 Subject: [PATCH 033/235] Fixed mime lookup. It's not necessary to only pass in the file extension --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 04603be..f23faa6 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -280,7 +280,7 @@ Server.prototype.respondNoGzip = function (pathname, status, contentType, _heade Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var contentType = _headers['Content-Type'] || - mime.lookup[path.extname(files[0]).slice(1)] || + mime.lookup(files[0]) || 'application/octet-stream'; if(this.options.gzip) { this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); From 00d14759dfb24e85ee44b6067bcda7977d9c278d Mon Sep 17 00:00:00 2001 From: thbaja Date: Wed, 19 Jun 2013 16:42:31 +0200 Subject: [PATCH 034/235] Reverted header check back to original, so we handle one or both of 'If-None-Match' and 'If-Modified-Since' correctly. --- lib/node-static.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index f23faa6..c31c6e3 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -233,9 +233,12 @@ Server.prototype.respondGzip = function(pathname, status, contentType, _headers, } Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { - var mtime = Date.parse(stat.mtime), - key = pathname || files[0], - headers = {}; + var mtime = Date.parse(stat.mtime), + key = pathname || files[0], + headers = {}, + clientETag = req.headers['if-none-match'], + clientMTime = Date.parse(req.headers['if-modified-since']); + // Copy default headers for (var k in this.options.headers) { headers[k] = this.options.headers[k] } @@ -253,8 +256,9 @@ Server.prototype.respondNoGzip = function (pathname, status, contentType, _heade // Conditional GET // If the "If-Modified-Since" or "If-None-Match" headers // match the conditions, send a 304 Not Modified. - if (req.headers['if-none-match'] === headers['Etag'] || - Date.parse(req.headers['if-modified-since']) >= mtime) { + if ((clientMTime || clientETag) && + (!clientETag || clientETag === headers['Etag']) && + (!clientMTime || clientMTime >= mtime)) { // 304 response should not contain entity headers ['Content-Encoding', 'Content-Language', From d33d853a00ce948cf46556eaebbd010fa0a5074d Mon Sep 17 00:00:00 2001 From: thbaja Date: Fri, 21 Jun 2013 10:28:13 +0200 Subject: [PATCH 035/235] Changed order in README and removed merge comment --- README.md | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c681804..21ec0d2 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,24 @@ example: `{ 'X-Hello': 'World!' }` > defaults to `{}` -<<<<<<< HEAD +#### `gzip` # + +Enable support for sending compressed responses. This will enable a check for a +file with the same name plus '.gz' in the same folder. If the compressed file is +found and the client has indicated support for gzip file transfer, the contents +of the .gz file will be sent in place of the uncompressed file along with a +Content-Encoding: gzip header to inform the client the data has been compressed. + +example: `{ gzip: true }` +example: `{ gzip: /^\/text/ }` + +Passing `true` will enable this check for all files. +Passing a RegExp instance will only enable this check if the content-type of the +respond would match that RegExp using its test() method. + +> Defaults to `false` + + Command Line Interface ---------------------- @@ -182,24 +199,3 @@ Command Line Interface # show help message, including all options $ static -h -======= -#### `gzip` # - -Enable support for sending compressed responses. This will enable a check for a -file with the same name plus '.gz' in the same folder. If the compressed file is -found and the client has indicated support for gzip file transfer, the contents -of the .gz file will be sent in place of the uncompressed file along with a -Content-Encoding: gzip header to inform the client the data has been compressed. - -example: `{ gzip: true }` -example: `{ gzip: /^\/text/ }` - -Passing `true` will enable this check for all files. -Passing a RegExp instance will only enable this check if the content-type of the -respond would match that RegExp using its test() method. - -> Defaults to `false` - - - ->>>>>>> Updated the README to describe gzip configuration From 131b6677485386754d5b683869cad56b07ee59f0 Mon Sep 17 00:00:00 2001 From: thbaja Date: Fri, 21 Jun 2013 10:31:03 +0200 Subject: [PATCH 036/235] Corrected comma to semicolon --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index c31c6e3..daadee7 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -11,7 +11,7 @@ var fs = require('fs') var version = [0, 6, 9]; Server = function (root, options) { - if (root && (typeof(root) === 'object')) { options = root, root = null } + if (root && (typeof(root) === 'object')) { options = root; root = null } this.root = path.resolve(root || '.'); this.options = options || {}; From cfc9b41eb9eb7cd3e87076118ab4e132d1bcf2b0 Mon Sep 17 00:00:00 2001 From: Sean Massa Date: Mon, 5 Aug 2013 12:41:16 -0500 Subject: [PATCH 037/235] Fix example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21ec0d2..ccf2574 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Synopsis // // Create a node-static server instance to serve the './public' folder // - var file = new(static.Server)('./public'); + var file = new static.Server('./public'); require('http').createServer(function (request, response) { request.addListener('end', function () { From b8996094b67a232c6088a60af77dfa6718cec7ee Mon Sep 17 00:00:00 2001 From: valentinvieriu Date: Fri, 23 Aug 2013 20:23:23 +0200 Subject: [PATCH 038/235] Update node-static.js using regexp as a gzip option was not working. The content type parameter was missing --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index daadee7..380bbcd 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -211,7 +211,7 @@ Server.prototype.gzipOk = function(req, contentType) { */ Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { var that = this; - if(files.length == 1 && this.gzipOk(req)) { + if(files.length == 1 && this.gzipOk(req,contentType)) { var gzFile = files[0] + ".gz"; fs.stat(gzFile, function(e, gzStat) { if(!e && gzStat.isFile()) { From 2771db5c5b6a4fcc2cc9070ea4a7528eaaf68d4d Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Fri, 28 Jun 2013 12:26:51 -0300 Subject: [PATCH 039/235] Bump version to 0.7.0 --- lib/node-static.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index daadee7..4b102f2 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -8,7 +8,7 @@ var fs = require('fs') , util = require('./node-static/util'); // Current version -var version = [0, 6, 9]; +var version = [0, 7, 0]; Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } diff --git a/package.json b/package.json index 5091077..b282f25 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "request": "latest", "vows": "latest" }, - "version" : "0.6.9", + "version" : "0.7.0", "engines" : { "node": ">= 0.4.1" } } From 3327cc59126798b6f8fb3ffa19bfeddb8ef55e57 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Mon, 26 Aug 2013 17:25:24 -0300 Subject: [PATCH 040/235] Closes #104 --- lib/node-static.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index a7cd574..011c458 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -197,8 +197,8 @@ Server.prototype.serve = function (req, res, callback) { */ Server.prototype.gzipOk = function(req, contentType) { var enable = this.options.gzip; - if(enable && - (typeof enable === 'boolean' || + if(enable && + (typeof enable === 'boolean' || (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { var acceptEncoding = req.headers['accept-encoding']; return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0; @@ -207,11 +207,11 @@ Server.prototype.gzipOk = function(req, contentType) { } /* Send a gzipped version of the file if the options and the client indicate gzip is enabled and - * we find a .gz file mathing the static resource requested. + * we find a .gz file mathing the static resource requested. */ Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { var that = this; - if(files.length == 1 && this.gzipOk(req,contentType)) { + if(files.length == 1 && this.gzipOk(req, contentType)) { var gzFile = files[0] + ".gz"; fs.stat(gzFile, function(e, gzStat) { if(!e && gzStat.isFile()) { @@ -250,7 +250,7 @@ Server.prototype.respondNoGzip = function (pathname, status, contentType, _heade headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString(); headers['Content-Type'] = contentType; headers['Content-Length'] = stat.size; - + for (var k in _headers) { headers[k] = _headers[k] } // Conditional GET @@ -261,12 +261,12 @@ Server.prototype.respondNoGzip = function (pathname, status, contentType, _heade (!clientMTime || clientMTime >= mtime)) { // 304 response should not contain entity headers ['Content-Encoding', - 'Content-Language', - 'Content-Length', - 'Content-Location', - 'Content-MD5', - 'Content-Range', - 'Content-Type', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-MD5', + 'Content-Range', + 'Content-Type', 'Expires', 'Last-Modified'].forEach(function(entityHeader) { delete headers[entityHeader]; @@ -283,7 +283,7 @@ Server.prototype.respondNoGzip = function (pathname, status, contentType, _heade }; Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { - var contentType = _headers['Content-Type'] || + var contentType = _headers['Content-Type'] || mime.lookup(files[0]) || 'application/octet-stream'; if(this.options.gzip) { From b51b8aeffb7cfd1d87d5b730226931a73e84b9a3 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Mon, 26 Aug 2013 17:26:22 -0300 Subject: [PATCH 041/235] Bumps version to 0.7.1 --- lib/node-static.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 011c458..5e0cc3c 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -8,7 +8,7 @@ var fs = require('fs') , util = require('./node-static/util'); // Current version -var version = [0, 7, 0]; +var version = [0, 7, 1]; Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } diff --git a/package.json b/package.json index b282f25..5a094bd 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "request": "latest", "vows": "latest" }, - "version" : "0.7.0", + "version" : "0.7.1", "engines" : { "node": ">= 0.4.1" } } From 716718da41f71edddf413cae2700f47c6320999c Mon Sep 17 00:00:00 2001 From: valentinvieriu Date: Fri, 11 Oct 2013 09:16:28 +0200 Subject: [PATCH 042/235] We accept POST too Needed this to be able to use node-static as a webserver for a Facebook Application --- lib/node-static.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-static.js b/lib/node-static.js index 5e0cc3c..5f8bd3b 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -134,7 +134,7 @@ Server.prototype.servePath = function (pathname, status, headers, req, res, fini pathname = this.resolve(pathname); // Only allow GET and HEAD requests - if (req.method !== 'GET' && req.method !== 'HEAD') { + if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'POST') { finish(405, { 'Allow': 'GET, HEAD' }); return promise; } From f9717e54effb9b8fe4f829fe5b9c0b49cb04b7a4 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Sun, 20 Oct 2013 22:23:31 -0200 Subject: [PATCH 043/235] Accepts all kind of HTTP methods. Fixes #106 and #109 --- lib/node-static.js | 6 ------ test/integration/node-static-test.js | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 5f8bd3b..4b19118 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -133,12 +133,6 @@ Server.prototype.servePath = function (pathname, status, headers, req, res, fini pathname = this.resolve(pathname); - // Only allow GET and HEAD requests - if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'POST') { - finish(405, { 'Allow': 'GET, HEAD' }); - return promise; - } - // Make sure we're not trying to access a // file outside of the root. if (pathname.indexOf(that.root) === 0) { diff --git a/test/integration/node-static-test.js b/test/integration/node-static-test.js index c1e5d39..31ef8c5 100644 --- a/test/integration/node-static-test.js +++ b/test/integration/node-static-test.js @@ -173,7 +173,21 @@ suite.addBatch({ assert.equal(response.statusCode, 200); } } -}).addBatch({ +}) +.addBatch({ + 'requesting POST': { + topic : function(){ + request.post(TEST_SERVER + '/index.html', this.callback); + }, + 'should respond with 200' : function(error, response, body){ + assert.equal(response.statusCode, 200); + }, + 'should not be empty' : function(error, response, body){ + assert.isNotEmpty(body); + } + } +}) +.addBatch({ 'requesting HEAD': { topic : function(){ request.head(TEST_SERVER + '/index.html', this.callback); @@ -182,7 +196,7 @@ suite.addBatch({ assert.equal(response.statusCode, 200); }, 'head must has no body' : function(error, response, body){ - assert.isUndefined(body); + assert.isEmpty(body); } } }) @@ -247,3 +261,4 @@ suite.addBatch({ } } }).export(module); + From 89048dc9040ed6f0341a3a5e1caaafda8c3cfc37 Mon Sep 17 00:00:00 2001 From: Pablo Cantero Date: Sun, 20 Oct 2013 22:25:03 -0200 Subject: [PATCH 044/235] Bump version to 0.7.2 --- lib/node-static.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 4b19118..9030c68 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -8,7 +8,7 @@ var fs = require('fs') , util = require('./node-static/util'); // Current version -var version = [0, 7, 1]; +var version = [0, 7, 2]; Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } diff --git a/package.json b/package.json index 5a094bd..26fd1e3 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "request": "latest", "vows": "latest" }, - "version" : "0.7.1", + "version" : "0.7.2", "engines" : { "node": ">= 0.4.1" } } From 4962c77d87b592266f6a206bf3a06ecbeade48c8 Mon Sep 17 00:00:00 2001 From: timthornton Date: Wed, 6 Nov 2013 15:16:08 -0500 Subject: [PATCH 045/235] Adding bounds checking to buffer copy in Server.stream --- lib/node-static.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/node-static.js b/lib/node-static.js index 9030c68..5c7cf91 100644 --- a/lib/node-static.js +++ b/lib/node-static.js @@ -299,8 +299,12 @@ Server.prototype.stream = function (pathname, files, buffer, res, callback) { flags: 'r', mode: 0666 }).on('data', function (chunk) { - chunk.copy(buffer, offset); - offset += chunk.length; + // Bounds check the incoming chunk and offset, as copying + // a buffer from an invalid offset will throw an error and crash + if (chunk.length && offset < buffer.length && offset >= 0) { + chunk.copy(buffer, offset); + offset += chunk.length; + } }).on('close', function () { streamFile(files, offset); }).on('error', function (err) { From d5e97aadd60e5e882d4e415177fb1249abeea6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Lal?= Date: Wed, 8 Jan 2014 01:12:38 +0100 Subject: [PATCH 046/235] Sober 404 error message without the 500kB image --- bin/cli.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 632b112..22bea9f 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -39,11 +39,6 @@ var fs = require('fs'), var dir = argv._[0] || '.'; - var trainwreck = fs.readFileSync(path.join(__dirname, '../etc/trainwreck.jpg')), - notFound = fs.readFileSync(path.join(__dirname, '../etc/404.html')) - .toString() - .replace('{{trainwreck}}', trainwreck.toString('base64')); - var colors = require('colors'); var log = function(request, response, statusCode) { @@ -91,7 +86,7 @@ require('http').createServer(function (request, response) { file.serve(request, response, function(e, rsp) { if (e && e.status === 404) { response.writeHead(e.status, e.headers); - response.end(notFound); + response.end("Not Found"); log(request, response); } else { log(request, response); From e3a6cd003bc2d9fe5e419e90a23752726b7d5074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Lal?= Date: Wed, 8 Jan 2014 01:13:59 +0100 Subject: [PATCH 047/235] Remove unused files --- etc/404.html | 23 ----------------------- etc/trainwreck.jpg | Bin 543733 -> 0 bytes 2 files changed, 23 deletions(-) delete mode 100644 etc/404.html delete mode 100644 etc/trainwreck.jpg diff --git a/etc/404.html b/etc/404.html deleted file mode 100644 index 147aeb1..0000000 --- a/etc/404.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - -

not found

-

don't worry though, it could be worse.

- - - diff --git a/etc/trainwreck.jpg b/etc/trainwreck.jpg deleted file mode 100644 index ac85b6357eaa21c26ff81fb101a927c6dfb89e87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 543733 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