/node_modules/express/node_modules/send/lib/send.js
JavaScript | 462 lines | 199 code | 77 blank | 186 comment | 46 complexity | bc567ca2e6c58fa42147326cd7a3e762 MD5 | raw file
Possible License(s): Apache-2.0, MIT
1 2/** 3 * Module dependencies. 4 */ 5 6var 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') 13 , fs = require('fs') 14 , basename = path.basename 15 , normalize = path.normalize 16 , join = path.join 17 , utils = require('./utils'); 18 19/** 20 * Expose `send`. 21 */ 22 23exports = module.exports = send; 24 25/** 26 * Expose mime module. 27 */ 28 29exports.mime = mime; 30 31/** 32 * Return a `SendStream` for `req` and `path`. 33 * 34 * @param {Request} req 35 * @param {String} path 36 * @return {SendStream} 37 * @api public 38 */ 39 40function send(req, path) { 41 return new SendStream(req, path); 42} 43 44/** 45 * Initialize a `SendStream` with the given `path`. 46 * 47 * Events: 48 * 49 * - `error` an error occurred 50 * - `stream` file streaming has started 51 * - `end` streaming has completed 52 * - `directory` a directory was requested 53 * 54 * @param {Request} req 55 * @param {String} path 56 * @api private 57 */ 58 59function SendStream(req, path) { 60 var self = this; 61 this.req = req; 62 this.path = path; 63 this.maxage(0); 64 this.hidden(false); 65 this.index('index.html'); 66} 67 68/** 69 * Inherits from `Stream.prototype`. 70 */ 71 72SendStream.prototype.__proto__ = Stream.prototype; 73 74/** 75 * Enable or disable "hidden" (dot) files. 76 * 77 * @param {Boolean} path 78 * @return {SendStream} 79 * @api public 80 */ 81 82SendStream.prototype.hidden = function(val){ 83 debug('hidden %s', val); 84 this._hidden = val; 85 return this; 86}; 87 88/** 89 * Set index `path`, set to a falsy 90 * value to disable index support. 91 * 92 * @param {String|Boolean} path 93 * @return {SendStream} 94 * @api public 95 */ 96 97SendStream.prototype.index = function(path){ 98 debug('index %s', path); 99 this._index = path; 100 return this; 101}; 102 103/** 104 * Set root `path`. 105 * 106 * @param {String} path 107 * @return {SendStream} 108 * @api public 109 */ 110 111SendStream.prototype.root = 112SendStream.prototype.from = function(path){ 113 this._root = normalize(path); 114 return this; 115}; 116 117/** 118 * Set max-age to `ms`. 119 * 120 * @param {Number} ms 121 * @return {SendStream} 122 * @api public 123 */ 124 125SendStream.prototype.maxage = function(ms){ 126 if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000; 127 debug('max-age %d', ms); 128 this._maxage = ms; 129 return this; 130}; 131 132/** 133 * Emit error with `status`. 134 * 135 * @param {Number} status 136 * @api private 137 */ 138 139SendStream.prototype.error = function(status, err){ 140 var res = this.res; 141 var msg = http.STATUS_CODES[status]; 142 err = err || new Error(msg); 143 err.status = status; 144 if (this.listeners('error').length) return this.emit('error', err); 145 res.statusCode = err.status; 146 res.end(msg); 147}; 148 149/** 150 * Check if the pathname is potentially malicious. 151 * 152 * @return {Boolean} 153 * @api private 154 */ 155 156SendStream.prototype.isMalicious = function(){ 157 return !this._root && ~this.path.indexOf('..'); 158}; 159 160/** 161 * Check if the pathname ends with "/". 162 * 163 * @return {Boolean} 164 * @api private 165 */ 166 167SendStream.prototype.hasTrailingSlash = function(){ 168 return '/' == this.path[this.path.length - 1]; 169}; 170 171/** 172 * Check if the basename leads with ".". 173 * 174 * @return {Boolean} 175 * @api private 176 */ 177 178SendStream.prototype.hasLeadingDot = function(){ 179 return '.' == basename(this.path)[0]; 180}; 181 182/** 183 * Check if this is a conditional GET request. 184 * 185 * @return {Boolean} 186 * @api private 187 */ 188 189SendStream.prototype.isConditionalGET = function(){ 190 return this.req.headers['if-none-match'] 191 || this.req.headers['if-modified-since']; 192}; 193 194/** 195 * Strip content-* header fields. 196 * 197 * @api private 198 */ 199 200SendStream.prototype.removeContentHeaderFields = function(){ 201 var res = this.res; 202 Object.keys(res._headers).forEach(function(field){ 203 if (0 == field.indexOf('content')) { 204 res.removeHeader(field); 205 } 206 }); 207}; 208 209/** 210 * Respond with 304 not modified. 211 * 212 * @api private 213 */ 214 215SendStream.prototype.notModified = function(){ 216 var res = this.res; 217 debug('not modified'); 218 this.removeContentHeaderFields(); 219 res.statusCode = 304; 220 res.end(); 221}; 222 223/** 224 * Check if the request is cacheable, aka 225 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). 226 * 227 * @return {Boolean} 228 * @api private 229 */ 230 231SendStream.prototype.isCachable = function(){ 232 var res = this.res; 233 return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode; 234}; 235 236/** 237 * Handle stat() error. 238 * 239 * @param {Error} err 240 * @api private 241 */ 242 243SendStream.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); 247}; 248 249/** 250 * Check if the cache is fresh. 251 * 252 * @return {Boolean} 253 * @api private 254 */ 255 256SendStream.prototype.isFresh = function(){ 257 return fresh(this.req.headers, this.res._headers); 258}; 259 260/** 261 * Redirect to `path`. 262 * 263 * @param {String} path 264 * @api private 265 */ 266 267SendStream.prototype.redirect = function(path){ 268 if (this.listeners('directory').length) return this.emit('directory'); 269 var res = this.res; 270 path += '/'; 271 res.statusCode = 301; 272 res.setHeader('Location', path); 273 res.end('Redirecting to ' + utils.escape(path)); 274}; 275 276/** 277 * Pipe to `res. 278 * 279 * @param {Stream} res 280 * @return {Stream} res 281 * @api public 282 */ 283 284SendStream.prototype.pipe = function(res){ 285 var self = this 286 , args = arguments 287 , path = this.path 288 , root = this._root; 289 290 // references 291 this.res = res; 292 293 // invalid request uri 294 path = utils.decode(path); 295 if (-1 == path) return this.error(400); 296 297 // null byte(s) 298 if (~path.indexOf('\0')) return this.error(400); 299 300 // join / normalize from optional root dir 301 if (root) path = normalize(join(this._root, path)); 302 303 // ".." is malicious without "root" 304 if (this.isMalicious()) return this.error(403); 305 306 // malicious path 307 if (root && 0 != path.indexOf(root)) return this.error(403); 308 309 // hidden file support 310 if (!this._hidden && this.hasLeadingDot()) return this.error(404); 311 312 // index file support 313 if (this._index && this.hasTrailingSlash()) path += this._index; 314 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); 320 }); 321 322 return res; 323}; 324 325/** 326 * Transfer `path`. 327 * 328 * @param {String} path 329 * @api public 330 */ 331 332SendStream.prototype.send = function(path, stat){ 333 var options = {}; 334 var len = stat.size; 335 var res = this.res; 336 var req = this.req; 337 var ranges = req.headers.range; 338 339 // set header fields 340 this.setHeader(stat); 341 342 // set content-type 343 this.type(path); 344 345 // conditional GET support 346 if (this.isConditionalGET() 347 && this.isCachable() 348 && this.isFresh()) { 349 return this.notModified(); 350 } 351 352 // Range support 353 if (ranges) { 354 ranges = parseRange(len, ranges); 355 356 // unsatisfiable 357 if (-1 == ranges) { 358 res.setHeader('Content-Range', 'bytes */' + stat.size); 359 return this.error(416); 360 } 361 362 // valid (syntactically invalid ranges are treated as a regular response) 363 if (-2 != ranges) { 364 options.start = ranges[0].start; 365 options.end = ranges[0].end; 366 367 // Content-Range 368 len = options.end - options.start + 1; 369 res.statusCode = 206; 370 res.setHeader('Content-Range', 'bytes ' 371 + options.start 372 + '-' 373 + options.end 374 + '/' 375 + stat.size); 376 } 377 } 378 379 // content-length 380 res.setHeader('Content-Length', len); 381 382 // HEAD support 383 if ('HEAD' == req.method) return res.end(); 384 385 this.stream(path, options); 386}; 387 388/** 389 * Stream `path` to the response. 390 * 391 * @param {String} path 392 * @param {Object} options 393 * @api private 394 */ 395 396SendStream.prototype.stream = function(path, options){ 397 // TODO: this is all lame, refactor meeee 398 var self = this; 399 var res = this.res; 400 var req = this.req; 401 402 // pipe 403 var stream = fs.createReadStream(path, options); 404 this.emit('stream', stream); 405 stream.pipe(res); 406 407 // socket closed, done with the fd 408 req.on('close', stream.destroy.bind(stream)); 409 410 // error handling code-smell 411 stream.on('error', function(err){ 412 // no hope in responding 413 if (res._header) { 414 console.error(err.stack); 415 req.destroy(); 416 return; 417 } 418 419 // 500 420 err.status = 500; 421 self.emit('error', err); 422 }); 423 424 // end 425 stream.on('end', function(){ 426 self.emit('end'); 427 }); 428}; 429 430/** 431 * Set content-type based on `path` 432 * if it hasn't been explicitly set. 433 * 434 * @param {String} path 435 * @api private 436 */ 437 438SendStream.prototype.type = function(path){ 439 var res = this.res; 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 : '')); 445}; 446 447/** 448 * Set reaponse header fields, most 449 * fields may be pre-defined. 450 * 451 * @param {Object} stat 452 * @api private 453 */ 454 455SendStream.prototype.setHeader = function(stat){ 456 var res = this.res; 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()); 462};