6 var debug = require('debug')('send')
7 , parseRange = require('range-parser')
8 , Stream = require('stream')
9 , mime = require('mime')
10 , fresh = require('fresh')
11 , path = require('path')
12 , http = require('http')
14 , basename = path.basename
15 , normalize = path.normalize
17 , utils = require('./utils');
23 exports = module.exports = send;
32 * Return a `SendStream` for `req` and `path`.
34 * @param {Request} req
35 * @param {String} path
36 * @return {SendStream}
40 function send(req, path) {
41 return new SendStream(req, path);
45 * Initialize a `SendStream` with the given `path`.
49 * - `error` an error occurred
50 * - `stream` file streaming has started
51 * - `end` streaming has completed
52 * - `directory` a directory was requested
54 * @param {Request} req
55 * @param {String} path
59 function SendStream(req, path) {
65 this.index('index.html');
69 * Inherits from `Stream.prototype`.
72 SendStream.prototype.__proto__ = Stream.prototype;
75 * Enable or disable "hidden" (dot) files.
77 * @param {Boolean} path
78 * @return {SendStream}
82 SendStream.prototype.hidden = function(val){
83 debug('hidden %s', val);
89 * Set index `path`, set to a falsy
90 * value to disable index support.
92 * @param {String|Boolean} path
93 * @return {SendStream}
97 SendStream.prototype.index = function(path){
98 debug('index %s', path);
106 * @param {String} path
107 * @return {SendStream}
111 SendStream.prototype.root =
112 SendStream.prototype.from = function(path){
113 this._root = normalize(path);
118 * Set max-age to `ms`.
121 * @return {SendStream}
125 SendStream.prototype.maxage = function(ms){
126 if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000;
127 debug('max-age %d', ms);
133 * Emit error with `status`.
135 * @param {Number} status
139 SendStream.prototype.error = function(status, err){
141 var msg = http.STATUS_CODES[status];
142 err = err || new Error(msg);
144 if (this.listeners('error').length) return this.emit('error', err);
145 res.statusCode = err.status;
150 * Check if the pathname is potentially malicious.
156 SendStream.prototype.isMalicious = function(){
157 return !this._root && ~this.path.indexOf('..');
161 * Check if the pathname ends with "/".
167 SendStream.prototype.hasTrailingSlash = function(){
168 return '/' == this.path[this.path.length - 1];
172 * Check if the basename leads with ".".
178 SendStream.prototype.hasLeadingDot = function(){
179 return '.' == basename(this.path)[0];
183 * Check if this is a conditional GET request.
189 SendStream.prototype.isConditionalGET = function(){
190 return this.req.headers['if-none-match']
191 || this.req.headers['if-modified-since'];
195 * Strip content-* header fields.
200 SendStream.prototype.removeContentHeaderFields = function(){
202 Object.keys(res._headers).forEach(function(field){
203 if (0 == field.indexOf('content')) {
204 res.removeHeader(field);
210 * Respond with 304 not modified.
215 SendStream.prototype.notModified = function(){
217 debug('not modified');
218 this.removeContentHeaderFields();
219 res.statusCode = 304;
224 * Check if the request is cacheable, aka
225 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
231 SendStream.prototype.isCachable = function(){
233 return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
237 * Handle stat() error.
243 SendStream.prototype.onStatError = function(err){
244 var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
245 if (~notfound.indexOf(err.code)) return this.error(404, err);
246 this.error(500, err);
250 * Check if the cache is fresh.
256 SendStream.prototype.isFresh = function(){
257 return fresh(this.req.headers, this.res._headers);
261 * Redirect to `path`.
263 * @param {String} path
267 SendStream.prototype.redirect = function(path){
268 if (this.listeners('directory').length) return this.emit('directory');
271 res.statusCode = 301;
272 res.setHeader('Location', path);
273 res.end('Redirecting to ' + utils.escape(path));
279 * @param {Stream} res
280 * @return {Stream} res
284 SendStream.prototype.pipe = function(res){
293 // invalid request uri
294 path = utils.decode(path);
295 if (-1 == path) return this.error(400);
298 if (~path.indexOf('\0')) return this.error(400);
300 // join / normalize from optional root dir
301 if (root) path = normalize(join(this._root, path));
303 // ".." is malicious without "root"
304 if (this.isMalicious()) return this.error(403);
307 if (root && 0 != path.indexOf(root)) return this.error(403);
309 // hidden file support
310 if (!this._hidden && this.hasLeadingDot()) return this.error(404);
312 // index file support
313 if (this._index && this.hasTrailingSlash()) path += this._index;
315 debug('stat "%s"', path);
316 fs.stat(path, function(err, stat){
317 if (err) return self.onStatError(err);
318 if (stat.isDirectory()) return self.redirect(self.path);
319 self.send(path, stat);
328 * @param {String} path
332 SendStream.prototype.send = function(path, stat){
337 var ranges = req.headers.range;
340 this.setHeader(stat);
345 // conditional GET support
346 if (this.isConditionalGET()
349 return this.notModified();
354 ranges = parseRange(len, ranges);
358 res.setHeader('Content-Range', 'bytes */' + stat.size);
359 return this.error(416);
362 // valid (syntactically invalid ranges are treated as a regular response)
364 options.start = ranges[0].start;
365 options.end = ranges[0].end;
368 len = options.end - options.start + 1;
369 res.statusCode = 206;
370 res.setHeader('Content-Range', 'bytes '
380 res.setHeader('Content-Length', len);
383 if ('HEAD' == req.method) return res.end();
385 this.stream(path, options);
389 * Stream `path` to the response.
391 * @param {String} path
392 * @param {Object} options
396 SendStream.prototype.stream = function(path, options){
397 // TODO: this is all lame, refactor meeee
403 var stream = fs.createReadStream(path, options);
404 this.emit('stream', stream);
407 // socket closed, done with the fd
408 req.on('close', stream.destroy.bind(stream));
410 // error handling code-smell
411 stream.on('error', function(err){
412 // no hope in responding
414 console.error(err.stack);
421 self.emit('error', err);
425 stream.on('end', function(){
431 * Set content-type based on `path`
432 * if it hasn't been explicitly set.
434 * @param {String} path
438 SendStream.prototype.type = function(path){
440 if (res.getHeader('Content-Type')) return;
441 var type = mime.lookup(path);
442 var charset = mime.charsets.lookup(type);
443 debug('content-type %s', type);
444 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
448 * Set reaponse header fields, most
449 * fields may be pre-defined.
451 * @param {Object} stat
455 SendStream.prototype.setHeader = function(stat){
457 if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
458 if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
459 if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
460 if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000));
461 if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());