package/package.json000644 0000002066 3560116604 011552 0ustar00000000 000000 { "name": "node-static", "description": "simple, compliant file streaming module for node", "url": "http://github.com/cloudhead/node-static", "keywords": [ "http", "static", "file", "server" ], "author": "Alexis Sellier ", "contributors": [ "Pablo Cantero ", "Ionică Bizău " ], "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.7.11", "engines": { "node": ">= 0.4.1" }, "bugs": { "url": "https://github.com/cloudhead/node-static/issues" }, "homepage": "https://github.com/cloudhead/node-static", "directories": { "example": "examples", "test": "test" } } package/LICENSE000644 0000002045 3560116604 010266 0ustar00000000 000000 Copyright (c) 2010-14 Alexis Sellier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package/README.md000644 0000014001 3560116604 010533 0ustar00000000 000000 node-static =========== > a simple, *rfc 2616 compliant* file streaming module for [node](http://nodejs.org) node-static understands and supports *conditional GET* and *HEAD* requests. node-static was inspired by some of the other static-file serving modules out there, such as node-paperboy and antinode. Synopsis -------- ```js var static = require('node-static'); // // Create a node-static server instance to serve the './public' folder // var file = new static.Server('./public'); require('http').createServer(function (request, response) { request.addListener('end', function () { // // Serve files! // file.serve(request, response); }).resume(); }).listen(8080); ``` API --- ### Creating a node-static Server # Creating a file server instance is as simple as: ```js new static.Server(); ``` This will serve files in the current directory. If you want to serve files in a specific directory, pass it as the first argument: ```js new static.Server('./public'); ``` You can also specify how long the client is supposed to cache the files node-static serves: ```js new static.Server('./public', { cache: 3600 }); ``` This will set the `Cache-Control` header, telling clients to cache the file for an hour. This is the default setting. ### Serving files under a directory # To serve files under a directory, simply call the `serve` method on a `Server` instance, passing it the HTTP request and response object: ```js var static = require('node-static'); var fileServer = new static.Server('./public'); require('http').createServer(function (request, response) { request.addListener('end', function () { fileServer.serve(request, response); }).resume(); }).listen(8080); ``` ### Serving specific files # If you want to serve a specific file, like an error page for example, use the `serveFile` method: ```js fileServer.serveFile('/error.html', 500, {}, request, response); ``` This will serve the `error.html` file, from under the file root directory, with a `500` status code. For example, you could serve an error page, when the initial request wasn't found: ```js require('http').createServer(function (request, response) { request.addListener('end', function () { fileServer.serve(request, response, function (e, res) { if (e && (e.status === 404)) { // If the file wasn't found fileServer.serveFile('/not-found.html', 404, {}, request, response); } }); }).resume(); }).listen(8080); ``` More on intercepting errors bellow. ### Intercepting errors & Listening # 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: ```js var static = require('node-static'); var fileServer = new static.Server('./public'); require('http').createServer(function (request, response) { request.addListener('end', function () { fileServer.serve(request, response, function (err, result) { if (err) { // There was an error serving the file console.error("Error serving " + request.url + " - " + err.message); // Respond to the client response.writeHead(err.status, err.headers); response.end(); } }); }).resume(); }).listen(8080); ``` Note that if you pass a callback, and there is an error serving the file, node-static *will not* respond to the client. This gives you the opportunity to re-route the request, or handle it differently. For example, you may want to interpret a request as a static request, but if the file isn't found, send it to an application. If you only want to *listen* for errors, you can use *event listeners*: ```js fileServer.serve(request, response).addListener('error', function (err) { console.error("Error serving " + request.url + " - " + err.message); }); ``` With this method, you don't have to explicitly send the response back, in case of an error. ### Options when creating an instance of `Server` # #### `cache` # Sets the `Cache-Control` header. example: `{ cache: 7200 }` Passing a number will set the cache duration to that number of seconds. Passing `false` will disable the `Cache-Control` header. > Defaults to `3600` #### `serverInfo` # Sets the `Server` header. example: `{ serverInfo: "myserver" }` > Defaults to `node-static/{version}` #### `headers` # Sets response headers. example: `{ 'X-Hello': 'World!' }` > defaults to `{}` #### `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` #### `indexFile` # Choose a custom index file when serving up directories. example: `{ indexFile: "index.htm" }` > Defaults to `index.html` Command Line Interface ---------------------- `node-static` also provides a CLI. ### Installation # ```sh $ npm install -g node-static ``` ### Example Usage # ```sh # serve up the current directory $ static serving "." at http://127.0.0.1:8080 # serve up a different directory $ static public 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-revalidate"}' serving "." at http://127.0.0.1:8080 # set cache control max age $ static -c 7200 serving "." at http://127.0.0.1:8080 # expose the server to your local network $ static -a 0.0.0.0 serving "." at http://0.0.0.0:8080 # show help message, including all options $ static -h ``` package/benchmark/node-static-0.3.0.txt000644 0000002427 3560116604 014626 0ustar00000000 000000 This is ApacheBench, Version 2.3 <$Revision: 655654 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Server Software: node-static/0.3.0 Server Hostname: 127.0.0.1 Server Port: 8080 Document Path: /lib/node-static.js Document Length: 6038 bytes Concurrency Level: 20 Time taken for tests: 2.323 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 63190000 bytes HTML transferred: 60380000 bytes Requests per second: 4304.67 [#/sec] (mean) Time per request: 4.646 [ms] (mean) Time per request: 0.232 [ms] (mean, across all concurrent requests) Transfer rate: 26563.66 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.2 0 3 Processing: 1 4 1.4 4 28 Waiting: 1 4 1.3 4 18 Total: 2 5 1.5 4 28 Percentage of the requests served within a certain time (ms) 50% 4 66% 5 75% 5 80% 5 90% 5 95% 6 98% 8 99% 9 100% 28 (longest request) package/bin/cli.js000755 0000007725 3560116604 011153 0ustar00000000 000000 #!/usr/bin/env node var fs = require('fs'), tty = require('tty'), statik = require('./../lib/node-static'); var argv = require('optimist') .usage([ 'USAGE: $0 [-p ] []', 'simple, rfc 2616 compliant file streaming module for node'] .join('\n\n')) .option('port', { alias: 'p', 'default': 8080, description: 'TCP port at which the files will be served' }) .option('host-address', { alias: 'a', 'default': '127.0.0.1', description: 'the local network interface at which to listen' }) .option('cache', { alias: 'c', description: '"Cache-Control" header setting, defaults to 3600' }) .option('version', { alias: 'v', description: 'node-static version' }) .option('headers', { alias: 'H', description: 'additional headers (in JSON format)' }) .option('header-file', { alias: 'f', description: 'JSON file of additional headers' }) .option('gzip', { alias: 'z', description: 'enable compression (tries to serve file of same name plus \'.gz\')' }) .option('spa', { description: 'serve the content as a single page app by redirecting all non-file requests to the index html file' }) .option('indexFile', { alias: 'i', 'default': 'index.html', description: 'specify a custom index file when serving up directories' }) .option('help', { alias: 'h', description: 'display this help message' }) .argv; var dir = argv._[0] || '.'; var colors = require('colors'); var log = function(request, response, statusCode) { var d = new Date(); var seconds = d.getSeconds() < 10? '0'+d.getSeconds() : d.getSeconds(), datestr = d.getHours() + ':' + d.getMinutes() + ':' + seconds, line = datestr + ' [' + response.statusCode + ']: ' + request.url, colorized = line; if (tty.isatty(process.stdout.fd)) colorized = (response.statusCode >= 500) ? line.red.bold : (response.statusCode >= 400) ? line.red : line; console.log(colorized); }; var file, options; if (argv.help) { require('optimist').showHelp(console.log); process.exit(0); } if (argv.version) { console.log('node-static', statik.version.join('.')); process.exit(0); } if (argv.cache) { (options = options || {}).cache = argv.cache; } if (argv.headers) { (options = options || {}).headers = JSON.parse(argv.headers); } if (argv['header-file']) { (options = options || {}).headers = JSON.parse(fs.readFileSync(argv['header-file'])); } if (argv.gzip) { (options = options || {}).gzip = true; } if (argv.indexFile) { (options = options || {}).indexFile = argv['indexFile']; } file = new(statik.Server)(dir, options); require('http').createServer(function (request, response) { request.addListener('end', function () { var callback = function(e, rsp) { if (e && e.status === 404) { response.writeHead(e.status, e.headers); response.end("Not Found"); log(request, response); } else { log(request, response); } }; if (argv['spa'] && request.url.indexOf(".") == -1) { file.serveFile(argv['indexFile'], 200, {}, request, response); } else { file.serve(request, response, callback); } }).resume(); }).listen(+argv.port, argv['host-address']); console.log('serving "' + dir + '" at http://' + argv['host-address'] + ':' + argv.port); if (argv.spa) { console.log('serving as a single page app (all non-file requests redirect to ' + argv['indexFile'] +')'); } package/examples/file-server.js000644 0000001353 3560116604 013661 0ustar00000000 000000 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!'} }); require('http').createServer(function (request, response) { 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); console.log("> node-static is listening on http://127.0.0.1:8080"); package/lib/node-static.js000644 0000032631 3560116604 012603 0ustar00000000 000000 var fs = require('fs') , events = require('events') , buffer = require('buffer') , http = require('http') , url = require('url') , path = require('path') , mime = require('mime') , util = require('./node-static/util'); // Current version var version = [0, 7, 9]; var Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } // resolve() doesn't normalize (to lowercase) drive letters on Windows this.root = path.normalize(path.resolve(root || '.')); this.options = options || {}; this.cache = 3600; this.defaultHeaders = {}; this.options.headers = this.options.headers || {}; this.options.indexFile = this.options.indexFile || "index.html"; if ('cache' in this.options) { if (typeof(this.options.cache) === 'number') { this.cache = this.options.cache; } else if (! this.options.cache) { this.cache = false; } } if ('serverInfo' in this.options) { this.serverInfo = this.options.serverInfo.toString(); } else { this.serverInfo = 'node-static/' + version.join('.'); } this.defaultHeaders['server'] = this.serverInfo; 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]; } }; Server.prototype.serveDir = function (pathname, req, res, finish) { var htmlIndex = path.join(pathname, this.options.indexFile), that = this; fs.stat(htmlIndex, function (e, stat) { if (!e) { var status = 200; var headers = {}; var originalPathname = decodeURI(url.parse(req.url).pathname); if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') { return finish(301, { 'Location': originalPathname + '/' }); } else { that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); } } 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); streamFiles(index.files); }); } }); function streamFiles(files) { util.mstat(pathname, files, function (e, stat) { if (e) { return finish(404, {}) } that.respond(pathname, 200, {}, files, stat, 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) { if (e) { return promise.emit('error', e); } that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) { that.finish(status, headers, req, res, promise); }); }); return promise; }; Server.prototype.finish = function (status, headers, req, res, promise, callback) { var result = { status: status, headers: headers, message: http.STATUS_CODES[status] }; headers['server'] = this.serverInfo; if (!status || status >= 400) { if (callback) { callback(result); } else { if (promise.listeners('error').length > 0) { promise.emit('error', result); } else { res.writeHead(status, headers); res.end(); } } } else { // Don't end the request here, if we're streaming; // it's taken care of in `prototype.stream`. if (status !== 200 || req.method !== 'GET') { res.writeHead(status, headers); res.end(); } callback && callback(null, result); promise.emit('success', result); } }; Server.prototype.servePath = function (pathname, status, headers, req, res, finish) { var that = this, promise = new(events.EventEmitter); pathname = this.resolve(pathname); // Make sure we're not trying to access a // file outside of the root. if (pathname.indexOf(that.root) === 0) { fs.stat(pathname, function (e, stat) { if (e) { finish(404, {}); } else if (stat.isFile()) { // Stream a single file. that.respond(null, status, headers, [pathname], stat, req, res, finish); } else if (stat.isDirectory()) { // Stream a directory of files. that.serveDir(pathname, req, res, finish); } else { finish(400, {}); } }); } else { // Forbidden finish(403, {}); } return promise; }; Server.prototype.resolve = function (pathname) { return path.resolve(path.join(this.root, pathname)); }; 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); } 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); }).on('error', function (err) { promise.emit('error'); }); }); if (! callback) { return promise } }; /* 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. */ 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 && 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 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)) { var gzFile = files[0] + ".gz"; fs.stat(gzFile, function (e, gzStat) { if (!e && gzStat.isFile()) { var vary = _headers['Vary']; _headers['Vary'] = (vary && vary != 'Accept-Encoding' ? vary + ', ' : '') + 'Accept-Encoding'; _headers['Content-Encoding'] = 'gzip'; stat.size = gzStat.size; files = [gzFile]; } 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); } } Server.prototype.parseByteRange = function (req, stat) { var byteRange = { from: 0, to: 0, valid: false } var rangeHeader = req.headers['range']; var flavor = 'bytes='; if (rangeHeader) { if (rangeHeader.indexOf(flavor) == 0 && rangeHeader.indexOf(',') == -1) { /* Parse */ rangeHeader = rangeHeader.substr(flavor.length).split('-'); byteRange.from = parseInt(rangeHeader[0]); byteRange.to = parseInt(rangeHeader[1]); /* Replace empty fields of differential requests by absolute values */ if (isNaN(byteRange.from) && !isNaN(byteRange.to)) { byteRange.from = stat.size - byteRange.to; byteRange.to = stat.size ? stat.size - 1 : 0; } else if (!isNaN(byteRange.from) && isNaN(byteRange.to)) { byteRange.to = stat.size ? stat.size - 1 : 0; } /* General byte range validation */ if (!isNaN(byteRange.from) && !!byteRange.to && 0 <= byteRange.from && byteRange.from < byteRange.to) { byteRange.valid = true; } else { console.warn("Request contains invalid range header: ", rangeHeader); } } else { console.warn("Request contains unsupported range header: ", rangeHeader); } } return byteRange; } Server.prototype.respondNoGzip = function (pathname, status, contentType, _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']), startByte = 0, length = stat.size, byteRange = this.parseByteRange(req, stat); /* Handle byte ranges */ if (files.length == 1 && byteRange.valid) { if (byteRange.to < length) { // Note: HTTP Range param is inclusive startByte = byteRange.from; length = byteRange.to - byteRange.from + 1; status = 206; // Set Content-Range response header (we advertise initial resource size on server here (stat.size)) headers['Content-Range'] = 'bytes ' + byteRange.from + '-' + byteRange.to + '/' + stat.size; } else { byteRange.valid = false; console.warn("Range request exceeds file boundaries, goes until byte no", byteRange.to, "against file size of", length, "bytes"); } } /* In any case, check for unhandled byte range headers */ if (!byteRange.valid && req.headers['range']) { console.error(new Error("Range request present but invalid, might serve whole file instead")); } // 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(); headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString(); headers['Content-Type'] = contentType; headers['Content-Length'] = length; for (var k in _headers) { headers[k] = _headers[k] } // Conditional GET // If the "If-Modified-Since" or "If-None-Match" headers // match the conditions, send a 304 Not Modified. if ((clientMTime || clientETag) && (!clientETag || clientETag === headers['Etag']) && (!clientMTime || clientMTime >= 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]; }); finish(304, headers); } else { res.writeHead(status, headers); this.stream(key, files, length, startByte, res, function (e) { if (e) { return finish(500, {}) } finish(status, headers); }); } }; Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { var contentType = _headers['Content-Type'] || mime.lookup(files[0]) || '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); } } Server.prototype.stream = function (pathname, files, length, startByte, res, callback) { (function streamFile(files, offset) { var file = files.shift(); if (file) { file = path.resolve(file) === path.normalize(file) ? file : path.join(pathname || '.', file); // Stream the file to the client fs.createReadStream(file, { flags: 'r', mode: 0666, start: startByte, end: startByte + (length ? length - 1 : 0) }).on('data', function (chunk) { // 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 < length && offset >= 0) { offset += chunk.length; } }).on('close', function () { streamFile(files, offset); }).on('error', function (err) { callback(err); console.error(err); }).pipe(res, { end: false }); } else { res.end(); callback(null, offset); } })(files.slice(0), 0); }; // Exports exports.Server = Server; exports.version = version; exports.mime = mime; package/lib/node-static/util.js000644 0000001660 3560116604 013556 0ustar00000000 000000 var fs = require('fs') , path = require('path'); exports.mstat = function (dir, files, callback) { (function mstat(files, stats) { var file = files.shift(); if (file) { fs.stat(path.join(dir, file), function (e, stat) { if (e) { callback(e); } else { mstat(files, stats.concat([stat])); } }); } else { callback(null, { size: stats.reduce(function (total, stat) { return total + stat.size; }, 0), mtime: stats.reduce(function (latest, stat) { return latest > stat.mtime ? latest : stat.mtime; }, 0), ino: stats.reduce(function (total, stat) { return total + stat.ino; }, 0) }); } })(files.slice(0), []); }; package/test/fixtures/empty.css000644 0000000000 3560116604 013746 0ustar00000000 000000 package/test/fixtures/hello.txt000644 0000000013 3560116604 013746 0ustar00000000 000000 hello worldpackage/test/fixtures/index.html000644 0000000142 3560116604 014102 0ustar00000000 000000 Awesome page hello world! package/test/fixtures/there/index.html000644 0000000140 3560116604 015207 0ustar00000000 000000 Other page hello there! package/test/integration/node-static-test.js000644 0000030775 3560116604 016323 0ustar00000000 000000 var vows = require('vows') , request = require('request') , assert = require('assert') , static = require('../../lib/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; 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': { topic: function () { server = require('http').createServer(function (request, response) { fileServer.serve(request, response, function(err, result) { if (callback) callback(request, response, err, result); else request.end(); }); }).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({ '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 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) { 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({ 'requesting a malformed URI': { topic: function(){ request.get(TEST_SERVER + '/a%AFc', this.callback); }, 'should respond with 400': function(error, response, body){ assert.equal(response.statusCode, 400); } } }) .addBatch({ 'serving empty.css': { topic : function(){ request.get(TEST_SERVER + '/empty.css', this.callback); }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); }, 'should respond with text/css': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/css'); }, 'should respond with empty string': function(error, response, body){ assert.equal(body, ''); } } }) .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'); } } }).addBatch({ 'serving first 5 bytes of hello.txt': { topic : function(){ var options = { url: TEST_SERVER + '/hello.txt', headers: { 'Range': 'bytes=0-4' } }; request.get(options, this.callback); }, 'should respond with 206' : function(error, response, body){ assert.equal(response.statusCode, 206); }, 'should respond with text/plain': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/plain'); }, 'should have content-length of 5 bytes': function(error, response, body){ assert.equal(response.headers['content-length'], 5); }, 'should have a valid Content-Range header in response': function(error, response, body){ assert.equal(response.headers['content-range'], 'bytes 0-4/11'); }, 'should respond with hello': function(error, response, body){ assert.equal(body, 'hello'); } } }).addBatch({ 'serving last 5 bytes of hello.txt': { topic : function(){ var options = { url: TEST_SERVER + '/hello.txt', headers: { 'Range': 'bytes=6-10' } }; request.get(options, this.callback); }, 'should respond with 206' : function(error, response, body){ assert.equal(response.statusCode, 206); }, 'should respond with text/plain': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/plain'); }, 'should have content-length of 5 bytes': function(error, response, body){ assert.equal(response.headers['content-length'], 5); }, 'should have a valid Content-Range header in response': function(error, response, body){ assert.equal(response.headers['content-range'], 'bytes 6-10/11'); }, 'should respond with world': function(error, response, body){ assert.equal(body, 'world'); } } }).addBatch({ 'serving all from the start of hello.txt': { topic : function(){ var options = { url: TEST_SERVER + '/hello.txt', headers: { 'Range': 'bytes=0-' } }; request.get(options, this.callback); }, 'should respond with 206' : function(error, response, body){ assert.equal(response.statusCode, 206); }, 'should respond with text/plain': function(error, response, body){ assert.equal(response.headers['content-type'], 'text/plain'); }, 'should have content-length of 11 bytes': function(error, response, body){ assert.equal(response.headers['content-length'], 11); }, 'should have a valid Content-Range header in response': function(error, response, body){ assert.equal(response.headers['content-range'], 'bytes 0-10/11'); }, 'should respond with "hello world"': function(error, response, body){ assert.equal(body, 'hello world'); } } }).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'); } } }).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'); } } }).addBatch({ 'requesting with If-None-Match': { topic : function(){ var _this = this; request.get(TEST_SERVER + '/index.html', function(error, response, body){ request({ method: 'GET', uri: TEST_SERVER + '/index.html', headers: {'if-none-match': response.headers['etag']} }, _this.callback); }); }, 'should respond with 304' : function(error, response, body){ assert.equal(response.statusCode, 304); } }, 'requesting with If-None-Match and If-Modified-Since': { topic : function(){ var _this = this; request.get(TEST_SERVER + '/index.html', function(error, response, body){ var modified = Date.parse(response.headers['last-modified']); var oneDayLater = new Date(modified + (24 * 60 * 60 * 1000)).toUTCString(); var nonMatchingEtag = '1111222233334444'; request({ method: 'GET', uri: TEST_SERVER + '/index.html', headers: { 'if-none-match': nonMatchingEtag, 'if-modified-since': oneDayLater } }, _this.callback); }); }, 'should respond with a 200': function(error, response, body){ assert.equal(response.statusCode, 200); } } }) .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); }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); }, 'head must has no body' : function(error, response, body){ assert.isEmpty(body); } } }) .addBatch(headers) .addBatch({ 'addings custom mime types': { topic : function(){ static.mime.define({'application/font-woff': ['woff']}); this.callback(); }, 'should add woff' : function(error, response, body){ 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); } } }).addBatch({ 'once an http server is listening with custom index configuration': { topic: function () { server.close(); fileServer = new static.Server(__dirname + '/../fixtures', { indexFile: "hello.txt" }); server = require('http').createServer(function (request, response) { 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({ 'serving custom index file': { topic : function(){ request.get(TEST_SERVER + '/', this.callback); }, 'should respond with 200' : function(error, response, body){ assert.equal(response.statusCode, 200); }, 'should respond with empty string': function(error, response, body){ assert.equal(body, 'hello world'); } } }).export(module);