pax_global_header00006660000000000000000000000064122640202260014506gustar00rootroot0000000000000052 comment=4858a8212c580fa831b9614825275f38791df579 node-static-0.7.3/000077500000000000000000000000001226402022600137275ustar00rootroot00000000000000node-static-0.7.3/.gitignore000066400000000000000000000000341226402022600157140ustar00rootroot00000000000000npm-debug.log node_modules node-static-0.7.3/LICENSE000066400000000000000000000020421226402022600147320ustar00rootroot00000000000000Copyright (c) 2010 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. node-static-0.7.3/README.md000066400000000000000000000137421226402022600152150ustar00rootroot00000000000000node-static =========== > a simple, *rfc 2616 compliant* file streaming module for [node](http://nodejs.org) node-static has an in-memory file cache, making it highly efficient. 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 -------- 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: 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: new static.Server('./public'); You can also specify how long the client is supposed to cache the files node-static serves: 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: 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: 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: 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: 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 sys.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*: fileServer.serve(request, response).addListener('error', function (err) { sys.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` Command Line Interface ---------------------- `node-static` also provides a CLI. ### Installation # $ npm install -g node-static ### Example Usage # # 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 # show help message, including all options $ static -h node-static-0.7.3/benchmark/000077500000000000000000000000001226402022600156615ustar00rootroot00000000000000node-static-0.7.3/benchmark/node-static-0.3.0.txt000066400000000000000000000024271226402022600212750ustar00rootroot00000000000000This 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) node-static-0.7.3/bin/000077500000000000000000000000001226402022600144775ustar00rootroot00000000000000node-static-0.7.3/bin/cli.js000077500000000000000000000055141226402022600156140ustar00rootroot00000000000000#!/usr/bin/env node var fs = require('fs'), path = require('path'), 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('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('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'])); } file = new(statik.Server)(dir, options); require('http').createServer(function (request, response) { request.addListener('end', function () { file.serve(request, response, 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); } }); }).resume(); }).listen(+argv.port); console.log('serving "' + dir + '" at http://127.0.0.1:' + argv.port); node-static-0.7.3/examples/000077500000000000000000000000001226402022600155455ustar00rootroot00000000000000node-static-0.7.3/examples/file-server.js000066400000000000000000000013531226402022600203300ustar00rootroot00000000000000var 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"); node-static-0.7.3/lib/000077500000000000000000000000001226402022600144755ustar00rootroot00000000000000node-static-0.7.3/lib/node-static.js000066400000000000000000000260471226402022600172560ustar00rootroot00000000000000var 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, 3]; Server = function (root, options) { if (root && (typeof(root) === 'object')) { options = root; root = null } this.root = path.resolve(root || '.'); this.options = options || {}; this.cache = 3600; this.defaultHeaders = {}; this.options.headers = this.options.headers || {}; 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, 'index.html'), 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()) { //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); } } 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']); // 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'] = stat.size; 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(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) { 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, buffer, res, callback) { (function streamFile(files, offset) { var file = files.shift(); if (file) { file = file[0] === '/' ? file : path.join(pathname || '.', file); // Stream the file to the client fs.createReadStream(file, { flags: 'r', mode: 0666 }).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 < buffer.length && offset >= 0) { chunk.copy(buffer, offset); 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, buffer, offset); } })(files.slice(0), 0); }; // Exports exports.Server = Server; exports.version = version; exports.mime = mime; node-static-0.7.3/lib/node-static/000077500000000000000000000000001226402022600167075ustar00rootroot00000000000000node-static-0.7.3/lib/node-static/util.js000066400000000000000000000016601226402022600202250ustar00rootroot00000000000000var 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), []); }; node-static-0.7.3/package.json000066400000000000000000000015701226402022600162200ustar00rootroot00000000000000{ "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" : [ { "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.7.3", "engines" : { "node": ">= 0.4.1" } } node-static-0.7.3/test/000077500000000000000000000000001226402022600147065ustar00rootroot00000000000000node-static-0.7.3/test/fixtures/000077500000000000000000000000001226402022600165575ustar00rootroot00000000000000node-static-0.7.3/test/fixtures/hello.txt000066400000000000000000000000131226402022600204150ustar00rootroot00000000000000hello worldnode-static-0.7.3/test/fixtures/index.html000066400000000000000000000001421226402022600205510ustar00rootroot00000000000000 Awesome page hello world! node-static-0.7.3/test/fixtures/there/000077500000000000000000000000001226402022600176665ustar00rootroot00000000000000node-static-0.7.3/test/fixtures/there/index.html000066400000000000000000000001401226402022600216560ustar00rootroot00000000000000 Other page hello there! node-static-0.7.3/test/integration/000077500000000000000000000000001226402022600172315ustar00rootroot00000000000000node-static-0.7.3/test/integration/node-static-test.js000066400000000000000000000203761226402022600227660ustar00rootroot00000000000000var 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 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 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); } } }).export(module);