pax_global_header00006660000000000000000000000064122015322620014505gustar00rootroot0000000000000052 comment=a5e5e05a40bd44aaa7dba3db71540c56dfcd7ac7 send-0.1.4/000077500000000000000000000000001220153226200124405ustar00rootroot00000000000000send-0.1.4/.gitignore000066400000000000000000000000151220153226200144240ustar00rootroot00000000000000node_modules send-0.1.4/.npmignore000066400000000000000000000000351220153226200144350ustar00rootroot00000000000000support test examples *.sock send-0.1.4/History.md000066400000000000000000000011751220153226200144270ustar00rootroot00000000000000 0.1.4 / 2013-08-11 ================== * update fresh 0.1.3 / 2013-07-08 ================== * Revert "Fix fd leak" 0.1.2 / 2013-07-03 ================== * Fix fd leak 0.1.0 / 2012-08-25 ================== * add options parameter to send() that is passed to fs.createReadStream() [kanongil] 0.0.4 / 2012-08-16 ================== * allow custom "Accept-Ranges" definition 0.0.3 / 2012-07-16 ================== * fix normalization of the root directory. Closes #3 0.0.2 / 2012-07-09 ================== * add passing of req explicitly for now (YUCK) 0.0.1 / 2010-01-03 ================== * Initial release send-0.1.4/Makefile000066400000000000000000000001441220153226200140770ustar00rootroot00000000000000 test: @./node_modules/.bin/mocha \ --require should \ --reporter spec \ --bail .PHONY: testsend-0.1.4/Readme.md000066400000000000000000000071501220153226200141620ustar00rootroot00000000000000# send Send is Connect's `static()` extracted for generalized use, a streaming static file server supporting partial responses (Ranges), conditional-GET negotiation, high test coverage, and granular events which may be leveraged to take appropriate actions in your application or framework. ## Installation $ npm install send ## Examples Small: ```js var http = require('http'); var send = require('send'); var app = http.createServer(function(req, res){ send(req, req.url).pipe(res); }).listen(3000); ``` Serving from a root directory with custom error-handling: ```js var http = require('http'); var send = require('send'); var url = require('url'); var app = http.createServer(function(req, res){ // your custom error-handling logic: function error(err) { res.statusCode = err.status || 500; res.end(err.message); } // your custom directory handling logic: function redirect() { res.statusCode = 301; res.setHeader('Location', req.url + '/'); res.end('Redirecting to ' + req.url + '/'); } // transfer arbitrary files from within // /www/example.com/public/* send(req, url.parse(req.url).pathname) .root('/www/example.com/public') .on('error', error) .on('directory', redirect) .pipe(res); }).listen(3000); ``` ## API ### Events - `error` an error occurred `(err)` - `directory` a directory was requested - `file` a file was requested `(path, stat)` - `stream` file streaming has started `(stream)` - `end` streaming has completed ### .root(dir) Serve files relative to `path`. Aliased as `.from(dir)`. ### .index(path) By default send supports "index.html" files, to disable this invoke `.index(false)` or to supply a new index pass a string. ### .maxage(ms) Provide a max-age in milliseconds for http caching, defaults to 0. ### .hidden(bool) Enable or disable transfer of hidden files, defaults to false. ## Error-handling By default when no `error` listeners are present an automatic response will be made, otherwise you have full control over the response, aka you may show a 5xx page etc. ## Caching It does _not_ perform internal caching, you should use a reverse proxy cache such as Varnish for this, or those fancy things called CDNs. If your application is small enough that it would benefit from single-node memory caching, it's small enough that it does not need caching at all ;). ## Debugging To enable `debug()` instrumentation output export __DEBUG__: ``` $ DEBUG=send node app ``` ## Running tests ``` $ npm install $ make test ``` ## License (The MIT License) Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca> 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. send-0.1.4/examples/000077500000000000000000000000001220153226200142565ustar00rootroot00000000000000send-0.1.4/examples/connect.js000066400000000000000000000002161220153226200162440ustar00rootroot00000000000000 /** * Module dependencies. */ var connect = require('connect'); connect() .use(connect.static(__dirname + '/public')) .listen(3000); send-0.1.4/examples/public/000077500000000000000000000000001220153226200155345ustar00rootroot00000000000000send-0.1.4/examples/public/app.js000066400000000000000000000000251220153226200166470ustar00rootroot00000000000000console.log('whoop');send-0.1.4/examples/public/index.html000066400000000000000000000002151220153226200175270ustar00rootroot00000000000000 Send example

I'm HTML

send-0.1.4/examples/simple.js000066400000000000000000000003341220153226200161050ustar00rootroot00000000000000 /** * Module dependencies. */ var send = require('..') , http = require('http'); http.createServer(function(req, res){ send(req.url) .from(__dirname + '/public') .maxage(60000) .pipe(res); }).listen(3000);send-0.1.4/index.js000066400000000000000000000000501220153226200141000ustar00rootroot00000000000000 module.exports = require('./lib/send');send-0.1.4/lib/000077500000000000000000000000001220153226200132065ustar00rootroot00000000000000send-0.1.4/lib/send.js000066400000000000000000000230051220153226200144750ustar00rootroot00000000000000 /** * Module dependencies. */ var debug = require('debug')('send') , parseRange = require('range-parser') , Stream = require('stream') , mime = require('mime') , fresh = require('fresh') , path = require('path') , http = require('http') , fs = require('fs') , basename = path.basename , normalize = path.normalize , join = path.join , utils = require('./utils'); /** * Expose `send`. */ exports = module.exports = send; /** * Expose mime module. */ exports.mime = mime; /** * Return a `SendStream` for `req` and `path`. * * @param {Request} req * @param {String} path * @param {Object} options * @return {SendStream} * @api public */ function send(req, path, options) { return new SendStream(req, path, options); } /** * Initialize a `SendStream` with the given `path`. * * Events: * * - `error` an error occurred * - `stream` file streaming has started * - `end` streaming has completed * - `directory` a directory was requested * * @param {Request} req * @param {String} path * @param {Object} options * @api private */ function SendStream(req, path, options) { var self = this; this.req = req; this.path = path; this.options = options || {}; this.maxage(0); this.hidden(false); this.index('index.html'); } /** * Inherits from `Stream.prototype`. */ SendStream.prototype.__proto__ = Stream.prototype; /** * Enable or disable "hidden" (dot) files. * * @param {Boolean} path * @return {SendStream} * @api public */ SendStream.prototype.hidden = function(val){ debug('hidden %s', val); this._hidden = val; return this; }; /** * Set index `path`, set to a falsy * value to disable index support. * * @param {String|Boolean} path * @return {SendStream} * @api public */ SendStream.prototype.index = function(path){ debug('index %s', path); this._index = path; return this; }; /** * Set root `path`. * * @param {String} path * @return {SendStream} * @api public */ SendStream.prototype.root = SendStream.prototype.from = function(path){ this._root = normalize(path); return this; }; /** * Set max-age to `ms`. * * @param {Number} ms * @return {SendStream} * @api public */ SendStream.prototype.maxage = function(ms){ if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000; debug('max-age %d', ms); this._maxage = ms; return this; }; /** * Emit error with `status`. * * @param {Number} status * @api private */ SendStream.prototype.error = function(status, err){ var res = this.res; var msg = http.STATUS_CODES[status]; err = err || new Error(msg); err.status = status; if (this.listeners('error').length) return this.emit('error', err); res.statusCode = err.status; res.end(msg); }; /** * Check if the pathname is potentially malicious. * * @return {Boolean} * @api private */ SendStream.prototype.isMalicious = function(){ return !this._root && ~this.path.indexOf('..'); }; /** * Check if the pathname ends with "/". * * @return {Boolean} * @api private */ SendStream.prototype.hasTrailingSlash = function(){ return '/' == this.path[this.path.length - 1]; }; /** * Check if the basename leads with ".". * * @return {Boolean} * @api private */ SendStream.prototype.hasLeadingDot = function(){ return '.' == basename(this.path)[0]; }; /** * Check if this is a conditional GET request. * * @return {Boolean} * @api private */ SendStream.prototype.isConditionalGET = function(){ return this.req.headers['if-none-match'] || this.req.headers['if-modified-since']; }; /** * Strip content-* header fields. * * @api private */ SendStream.prototype.removeContentHeaderFields = function(){ var res = this.res; Object.keys(res._headers).forEach(function(field){ if (0 == field.indexOf('content')) { res.removeHeader(field); } }); }; /** * Respond with 304 not modified. * * @api private */ SendStream.prototype.notModified = function(){ var res = this.res; debug('not modified'); this.removeContentHeaderFields(); res.statusCode = 304; res.end(); }; /** * Check if the request is cacheable, aka * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). * * @return {Boolean} * @api private */ SendStream.prototype.isCachable = function(){ var res = this.res; return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode; }; /** * Handle stat() error. * * @param {Error} err * @api private */ SendStream.prototype.onStatError = function(err){ var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; if (~notfound.indexOf(err.code)) return this.error(404, err); this.error(500, err); }; /** * Check if the cache is fresh. * * @return {Boolean} * @api private */ SendStream.prototype.isFresh = function(){ return fresh(this.req.headers, this.res._headers); }; /** * Redirect to `path`. * * @param {String} path * @api private */ SendStream.prototype.redirect = function(path){ if (this.listeners('directory').length) return this.emit('directory'); var res = this.res; path += '/'; res.statusCode = 301; res.setHeader('Location', path); res.end('Redirecting to ' + utils.escape(path)); }; /** * Pipe to `res. * * @param {Stream} res * @return {Stream} res * @api public */ SendStream.prototype.pipe = function(res){ var self = this , args = arguments , path = this.path , root = this._root; // references this.res = res; // invalid request uri path = utils.decode(path); if (-1 == path) return this.error(400); // null byte(s) if (~path.indexOf('\0')) return this.error(400); // join / normalize from optional root dir if (root) path = normalize(join(this._root, path)); // ".." is malicious without "root" if (this.isMalicious()) return this.error(403); // malicious path if (root && 0 != path.indexOf(root)) return this.error(403); // hidden file support if (!this._hidden && this.hasLeadingDot()) return this.error(404); // index file support if (this._index && this.hasTrailingSlash()) path += this._index; debug('stat "%s"', path); fs.stat(path, function(err, stat){ if (err) return self.onStatError(err); if (stat.isDirectory()) return self.redirect(self.path); self.emit('file', path, stat); self.send(path, stat); }); return res; }; /** * Transfer `path`. * * @param {String} path * @api public */ SendStream.prototype.send = function(path, stat){ var options = this.options; var len = stat.size; var res = this.res; var req = this.req; var ranges = req.headers.range; var offset = options.start || 0; // set header fields this.setHeader(stat); // set content-type this.type(path); // conditional GET support if (this.isConditionalGET() && this.isCachable() && this.isFresh()) { return this.notModified(); } // adjust len to start/end options len = Math.max(0, len - offset); if (options.end !== undefined) { var bytes = options.end - offset + 1; if (len > bytes) len = bytes; } // Range support if (ranges) { ranges = parseRange(len, ranges); // unsatisfiable if (-1 == ranges) { res.setHeader('Content-Range', 'bytes */' + stat.size); return this.error(416); } // valid (syntactically invalid ranges are treated as a regular response) if (-2 != ranges) { options.start = offset + ranges[0].start; options.end = offset + ranges[0].end; // Content-Range res.statusCode = 206; res.setHeader('Content-Range', 'bytes ' + ranges[0].start + '-' + ranges[0].end + '/' + len); len = options.end - options.start + 1; } } // content-length res.setHeader('Content-Length', len); // HEAD support if ('HEAD' == req.method) return res.end(); this.stream(path, options); }; /** * Stream `path` to the response. * * @param {String} path * @param {Object} options * @api private */ SendStream.prototype.stream = function(path, options){ // TODO: this is all lame, refactor meeee var self = this; var res = this.res; var req = this.req; // pipe var stream = fs.createReadStream(path, options); this.emit('stream', stream); stream.pipe(res); // socket closed, done with the fd req.on('close', stream.destroy.bind(stream)); // error handling code-smell stream.on('error', function(err){ // no hope in responding if (res._header) { console.error(err.stack); req.destroy(); return; } // 500 err.status = 500; self.emit('error', err); }); // end stream.on('end', function(){ self.emit('end'); }); }; /** * Set content-type based on `path` * if it hasn't been explicitly set. * * @param {String} path * @api private */ SendStream.prototype.type = function(path){ var res = this.res; if (res.getHeader('Content-Type')) return; var type = mime.lookup(path); var charset = mime.charsets.lookup(type); debug('content-type %s', type); res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); }; /** * Set reaponse header fields, most * fields may be pre-defined. * * @param {Object} stat * @api private */ SendStream.prototype.setHeader = function(stat){ var res = this.res; if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes'); if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat)); if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000)); if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); }; send-0.1.4/lib/utils.js000066400000000000000000000014371220153226200147110ustar00rootroot00000000000000 /** * Return an ETag in the form of `"-"` * from the given `stat`. * * @param {Object} stat * @return {String} * @api private */ exports.etag = function(stat) { return '"' + stat.size + '-' + Number(stat.mtime) + '"'; }; /** * decodeURIComponent. * * Allows V8 to only deoptimize this fn instead of all * of send(). * * @param {String} path * @api private */ exports.decode = function(path){ try { return decodeURIComponent(path); } catch (err) { return -1; } }; /** * Escape the given string of `html`. * * @param {String} html * @return {String} * @api private */ exports.escape = function(html){ return String(html) .replace(/&(?!\w+;)/g, '&') .replace(//g, '>') .replace(/"/g, '"'); };send-0.1.4/package.json000066400000000000000000000011501220153226200147230ustar00rootroot00000000000000{ "name": "send", "version": "0.1.4", "description": "Better streaming static file server with Range and conditional-GET support", "keywords": ["static", "file", "server"], "author": "TJ Holowaychuk ", "dependencies": { "debug": "*", "mime": "~1.2.9", "fresh": "0.2.0", "range-parser": "0.0.4" }, "devDependencies": { "mocha": "*", "should": "*", "supertest": "0.0.1", "connect": "2.x" }, "scripts": { "test": "make test" }, "repository" : { "type" : "git" , "url" : "git://github.com/visionmedia/send.git" }, "main": "index" } send-0.1.4/test/000077500000000000000000000000001220153226200134175ustar00rootroot00000000000000send-0.1.4/test/fixtures/000077500000000000000000000000001220153226200152705ustar00rootroot00000000000000send-0.1.4/test/fixtures/.hidden000066400000000000000000000000071220153226200165210ustar00rootroot00000000000000secret send-0.1.4/test/fixtures/name.txt000066400000000000000000000000041220153226200167430ustar00rootroot00000000000000tobisend-0.1.4/test/fixtures/nums000066400000000000000000000000111220153226200161650ustar00rootroot00000000000000123456789send-0.1.4/test/fixtures/pets/000077500000000000000000000000001220153226200162435ustar00rootroot00000000000000send-0.1.4/test/fixtures/pets/index.html000066400000000000000000000000161220153226200202350ustar00rootroot00000000000000tobi loki janesend-0.1.4/test/fixtures/some thing.txt000066400000000000000000000000031220153226200200570ustar00rootroot00000000000000heysend-0.1.4/test/fixtures/tobi.html000066400000000000000000000000131220153226200171050ustar00rootroot00000000000000

tobi

send-0.1.4/test/send.js000066400000000000000000000233541220153226200147150ustar00rootroot00000000000000 var send = require('..') , http = require('http') , Stream = require('stream') , request = require('supertest') , assert = require('assert'); // test server var app = http.createServer(function(req, res){ function error(err) { res.statusCode = err.status; res.end(http.STATUS_CODES[err.status]); } function redirect() { res.statusCode = 301; res.setHeader('Location', req.url + '/'); res.end('Redirecting to ' + req.url + '/'); } send(req, 'test/fixtures' + req.url) .on('error', error) .on('directory', redirect) .pipe(res); }); describe('send.mime', function(){ it('should be exposed', function(){ assert(send.mime); }) }) describe('send(file).pipe(res)', function(){ it('should stream the file contents', function(done){ request(app) .get('/name.txt') .expect('Content-Length', '4') .expect('tobi', done); }) it('should decode the given path as a URI', function(done){ request(app) .get('/some%20thing.txt') .expect('hey', done); }) it('should treat a malformed URI as a bad request', function(done){ request(app) .get('/some%99thing.txt') .expect('Bad Request', done); }) it('should treat an ENAMETOOLONG as a 404', function(done){ var path = Array(100).join('foobar'); request(app) .get('/' + path) .expect(404, done); }) it('should support HEAD', function(done){ request(app) .head('/name.txt') .expect('Content-Length', '4') .expect(200) .end(function(err, res){ res.text.should.equal(''); done(); }); }) it('should add an ETag header field', function(done){ request(app) .get('/name.txt') .end(function(err, res){ if (err) return done(err); res.headers.should.have.property('etag'); done(); }); }) it('should add a Date header field', function(done){ request(app) .get('/name.txt') .end(function(err, res){ if (err) return done(err); res.headers.should.have.property('date'); done(); }); }) it('should add a Last-Modified header field', function(done){ request(app) .get('/name.txt') .end(function(err, res){ if (err) return done(err); res.headers.should.have.property('last-modified'); done(); }); }) it('should add a Accept-Ranges header field', function(done){ request(app) .get('/name.txt') .expect('Accept-Ranges', 'bytes') .end(done); }) it('should 404 if the file does not exist', function(done){ request(app) .get('/meow') .expect(404) .expect('Not Found') .end(done); }) it('should 301 if the directory exists', function(done){ request(app) .get('/pets') .expect(301) .expect('Location', '/pets/') .expect('Redirecting to /pets/') .end(done); }) it('should set Content-Type via mime map', function(done){ request(app) .get('/name.txt') .expect('Content-Type', 'text/plain; charset=UTF-8') .end(function(){ request(app) .get('/tobi.html') .expect('Content-Type', 'text/html; charset=UTF-8') .end(done); }); }) describe('when no "directory" listeners are present', function(){ it('should respond with a redirect', function(done){ var app = http.createServer(function(req, res){ send(req, req.url) .root('test/fixtures') .pipe(res); }); request(app) .get('/pets') .expect(301) .expect('Location', '/pets/') .expect('Redirecting to /pets/') .end(done); }) }) describe('when no "error" listeners are present', function(){ it('should respond to errors directly', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures' + req.url).pipe(res); }); request(app) .get('/foobar') .expect('Not Found') .expect(404) .end(done); }) }) describe('with conditional-GET', function(){ it('should respond with 304 on a match', function(done){ request(app) .get('/name.txt') .end(function(err, res){ var etag = res.headers.etag; request(app) .get('/name.txt') .set('If-None-Match', etag) .expect(304) .end(function(err, res){ res.headers.should.not.have.property('content-type'); res.headers.should.not.have.property('content-length'); done(); }); }) }) it('should respond with 200 otherwise', function(done){ request(app) .get('/name.txt') .end(function(err, res){ var etag = res.headers.etag; request(app) .get('/name.txt') .set('If-None-Match', '123') .expect(200) .expect('tobi') .end(done); }) }) }) describe('with Range request', function(){ it('should support byte ranges', function(done){ request(app) .get('/nums') .set('Range', 'bytes=0-4') .expect('12345', done); }) it('should be inclusive', function(done){ request(app) .get('/nums') .set('Range', 'bytes=0-0') .expect('1', done); }) it('should set Content-Range', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-5') .expect('Content-Range', 'bytes 2-5/9', done); }) it('should support -n', function(done){ request(app) .get('/nums') .set('Range', 'bytes=-3') .expect('789', done); }) it('should support n-', function(done){ request(app) .get('/nums') .set('Range', 'bytes=3-') .expect('456789', done); }) it('should respond with 206 "Partial Content"', function(done){ request(app) .get('/nums') .set('Range', 'bytes=0-4') .expect(206, done); }) it('should set Content-Length to the # of octets transferred', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-3') .expect('34') .expect('Content-Length', '2') .end(done); }) describe('when last-byte-pos of the range is greater the length', function(){ it('is taken to be equal to one less than the length', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-50') .expect('Content-Range', 'bytes 2-8/9') .end(done); }) it('should adapt the Content-Length accordingly', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-50') .expect('Content-Length', '7') .end(done); }) }) describe('when the first- byte-pos of the range is greater length', function(){ it('should respond with 416', function(done){ request(app) .get('/nums') .set('Range', 'bytes=9-50') .expect('Content-Range', 'bytes */9') .expect(416, done); }) }) describe('when syntactically invalid', function(){ it('should respond with 200 and the entire contents', function(done){ request(app) .get('/nums') .set('Range', 'asdf') .expect('123456789', done); }) }) }) describe('when "options" is specified', function(){ it('should support start/end', function(done){ var app = http.createServer(function(req, res){ var i = parseInt(req.url.slice(1)); send(req, 'test/fixtures/nums', { start:i*3, end:i*3+2 }).pipe(res); }); request(app) .get('/1') .expect('456', done); }) it('should support start/end with Range request', function(done){ var app = http.createServer(function(req, res){ var i = parseInt(req.url.slice(1)); send(req, 'test/fixtures/nums', { start:i*3, end:i*3+2 }).pipe(res); }); request(app) .get('/0') .set('Range', 'bytes=-2') .expect('23') .end(done); }) }) }) describe('send(file, options)', function(){ describe('maxAge', function(){ it('should default to 0', function(done){ request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=0') .end(done); }) it('should support Infinity', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt') .maxage(Infinity) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=31536000') .end(done); }) }) describe('index', function(){ it('should default to index.html', function(done){ request(app) .get('/pets/') .expect('tobi\nloki\njane') .end(done); }) }) describe('hidden', function(){ it('should default to false', function(done){ request(app) .get('/.secret') .expect(404) .expect('Not Found') .end(done); }) }) describe('root', function(){ describe('when given', function(){ it('should join root', function(done){ var app = http.createServer(function(req, res){ send(req, req.url) .root(__dirname + '/fixtures') .pipe(res); }); request(app) .get('/pets/../name.txt') .expect('tobi') .end(done); }) it('should restrict paths to within root', function(done){ var app = http.createServer(function(req, res){ send(req, req.url) .root(__dirname + '/fixtures') .on('error', function(err){ res.end(err.message) }) .pipe(res); }); request(app) .get('/pets/../../send.js') .expect('Forbidden') .end(done); }) }) describe('when missing', function(){ it('should consider .. malicious', function(done){ request(app) .get('/../send.js') .expect(403) .expect('Forbidden') .end(done); }) }) }) })