root / HServer / 00.Server / 00.Program / node_modules / send / index.js
이력 | 보기 | 이력해설 | 다운로드 (22.8 KB)
| 1 |
/*!
|
|---|---|
| 2 |
* send
|
| 3 |
* Copyright(c) 2012 TJ Holowaychuk
|
| 4 |
* Copyright(c) 2014-2016 Douglas Christopher Wilson
|
| 5 |
* MIT Licensed
|
| 6 |
*/
|
| 7 |
|
| 8 |
'use strict'
|
| 9 |
|
| 10 |
/**
|
| 11 |
* Module dependencies.
|
| 12 |
* @private
|
| 13 |
*/
|
| 14 |
|
| 15 |
var createError = require('http-errors') |
| 16 |
var debug = require('debug')('send') |
| 17 |
var deprecate = require('depd')('send') |
| 18 |
var destroy = require('destroy') |
| 19 |
var encodeUrl = require('encodeurl') |
| 20 |
var escapeHtml = require('escape-html') |
| 21 |
var etag = require('etag') |
| 22 |
var fresh = require('fresh') |
| 23 |
var fs = require('fs') |
| 24 |
var mime = require('mime') |
| 25 |
var ms = require('ms') |
| 26 |
var onFinished = require('on-finished') |
| 27 |
var parseRange = require('range-parser') |
| 28 |
var path = require('path') |
| 29 |
var statuses = require('statuses') |
| 30 |
var Stream = require('stream') |
| 31 |
var util = require('util') |
| 32 |
|
| 33 |
/**
|
| 34 |
* Path function references.
|
| 35 |
* @private
|
| 36 |
*/
|
| 37 |
|
| 38 |
var extname = path.extname
|
| 39 |
var join = path.join
|
| 40 |
var normalize = path.normalize
|
| 41 |
var resolve = path.resolve
|
| 42 |
var sep = path.sep
|
| 43 |
|
| 44 |
/**
|
| 45 |
* Regular expression for identifying a bytes Range header.
|
| 46 |
* @private
|
| 47 |
*/
|
| 48 |
|
| 49 |
var BYTES_RANGE_REGEXP = /^ *bytes=/ |
| 50 |
|
| 51 |
/**
|
| 52 |
* Maximum value allowed for the max age.
|
| 53 |
* @private
|
| 54 |
*/
|
| 55 |
|
| 56 |
var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year |
| 57 |
|
| 58 |
/**
|
| 59 |
* Regular expression to match a path with a directory up component.
|
| 60 |
* @private
|
| 61 |
*/
|
| 62 |
|
| 63 |
var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ |
| 64 |
|
| 65 |
/**
|
| 66 |
* Module exports.
|
| 67 |
* @public
|
| 68 |
*/
|
| 69 |
|
| 70 |
module.exports = send |
| 71 |
module.exports.mime = mime |
| 72 |
|
| 73 |
/**
|
| 74 |
* Return a `SendStream` for `req` and `path`.
|
| 75 |
*
|
| 76 |
* @param {object} req
|
| 77 |
* @param {string} path
|
| 78 |
* @param {object} [options]
|
| 79 |
* @return {SendStream}
|
| 80 |
* @public
|
| 81 |
*/
|
| 82 |
|
| 83 |
function send (req, path, options) { |
| 84 |
return new SendStream(req, path, options) |
| 85 |
} |
| 86 |
|
| 87 |
/**
|
| 88 |
* Initialize a `SendStream` with the given `path`.
|
| 89 |
*
|
| 90 |
* @param {Request} req
|
| 91 |
* @param {String} path
|
| 92 |
* @param {object} [options]
|
| 93 |
* @private
|
| 94 |
*/
|
| 95 |
|
| 96 |
function SendStream (req, path, options) { |
| 97 |
Stream.call(this)
|
| 98 |
|
| 99 |
var opts = options || {}
|
| 100 |
|
| 101 |
this.options = opts
|
| 102 |
this.path = path
|
| 103 |
this.req = req
|
| 104 |
|
| 105 |
this._acceptRanges = opts.acceptRanges !== undefined |
| 106 |
? Boolean(opts.acceptRanges) |
| 107 |
: true
|
| 108 |
|
| 109 |
this._cacheControl = opts.cacheControl !== undefined |
| 110 |
? Boolean(opts.cacheControl) |
| 111 |
: true
|
| 112 |
|
| 113 |
this._etag = opts.etag !== undefined |
| 114 |
? Boolean(opts.etag) |
| 115 |
: true
|
| 116 |
|
| 117 |
this._dotfiles = opts.dotfiles !== undefined |
| 118 |
? opts.dotfiles |
| 119 |
: 'ignore'
|
| 120 |
|
| 121 |
if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') { |
| 122 |
throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') |
| 123 |
} |
| 124 |
|
| 125 |
this._hidden = Boolean(opts.hidden)
|
| 126 |
|
| 127 |
if (opts.hidden !== undefined) { |
| 128 |
deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead') |
| 129 |
} |
| 130 |
|
| 131 |
// legacy support
|
| 132 |
if (opts.dotfiles === undefined) { |
| 133 |
this._dotfiles = undefined |
| 134 |
} |
| 135 |
|
| 136 |
this._extensions = opts.extensions !== undefined |
| 137 |
? normalizeList(opts.extensions, 'extensions option')
|
| 138 |
: [] |
| 139 |
|
| 140 |
this._immutable = opts.immutable !== undefined |
| 141 |
? Boolean(opts.immutable) |
| 142 |
: false
|
| 143 |
|
| 144 |
this._index = opts.index !== undefined |
| 145 |
? normalizeList(opts.index, 'index option')
|
| 146 |
: ['index.html']
|
| 147 |
|
| 148 |
this._lastModified = opts.lastModified !== undefined |
| 149 |
? Boolean(opts.lastModified) |
| 150 |
: true
|
| 151 |
|
| 152 |
this._maxage = opts.maxAge || opts.maxage
|
| 153 |
this._maxage = typeof this._maxage === 'string' |
| 154 |
? ms(this._maxage)
|
| 155 |
: Number(this._maxage)
|
| 156 |
this._maxage = !isNaN(this._maxage) |
| 157 |
? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) |
| 158 |
: 0
|
| 159 |
|
| 160 |
this._root = opts.root
|
| 161 |
? resolve(opts.root) |
| 162 |
: null
|
| 163 |
|
| 164 |
if (!this._root && opts.from) { |
| 165 |
this.from(opts.from)
|
| 166 |
} |
| 167 |
} |
| 168 |
|
| 169 |
/**
|
| 170 |
* Inherits from `Stream`.
|
| 171 |
*/
|
| 172 |
|
| 173 |
util.inherits(SendStream, Stream) |
| 174 |
|
| 175 |
/**
|
| 176 |
* Enable or disable etag generation.
|
| 177 |
*
|
| 178 |
* @param {Boolean} val
|
| 179 |
* @return {SendStream}
|
| 180 |
* @api public
|
| 181 |
*/
|
| 182 |
|
| 183 |
SendStream.prototype.etag = deprecate.function(function etag (val) { |
| 184 |
this._etag = Boolean(val)
|
| 185 |
debug('etag %s', this._etag) |
| 186 |
return this |
| 187 |
}, 'send.etag: pass etag as option')
|
| 188 |
|
| 189 |
/**
|
| 190 |
* Enable or disable "hidden" (dot) files.
|
| 191 |
*
|
| 192 |
* @param {Boolean} path
|
| 193 |
* @return {SendStream}
|
| 194 |
* @api public
|
| 195 |
*/
|
| 196 |
|
| 197 |
SendStream.prototype.hidden = deprecate.function(function hidden (val) { |
| 198 |
this._hidden = Boolean(val)
|
| 199 |
this._dotfiles = undefined |
| 200 |
debug('hidden %s', this._hidden) |
| 201 |
return this |
| 202 |
}, 'send.hidden: use dotfiles option')
|
| 203 |
|
| 204 |
/**
|
| 205 |
* Set index `paths`, set to a falsy
|
| 206 |
* value to disable index support.
|
| 207 |
*
|
| 208 |
* @param {String|Boolean|Array} paths
|
| 209 |
* @return {SendStream}
|
| 210 |
* @api public
|
| 211 |
*/
|
| 212 |
|
| 213 |
SendStream.prototype.index = deprecate.function(function index (paths) { |
| 214 |
var index = !paths ? [] : normalizeList(paths, 'paths argument') |
| 215 |
debug('index %o', paths)
|
| 216 |
this._index = index
|
| 217 |
return this |
| 218 |
}, 'send.index: pass index as option')
|
| 219 |
|
| 220 |
/**
|
| 221 |
* Set root `path`.
|
| 222 |
*
|
| 223 |
* @param {String} path
|
| 224 |
* @return {SendStream}
|
| 225 |
* @api public
|
| 226 |
*/
|
| 227 |
|
| 228 |
SendStream.prototype.root = function root (path) { |
| 229 |
this._root = resolve(String(path))
|
| 230 |
debug('root %s', this._root) |
| 231 |
return this |
| 232 |
} |
| 233 |
|
| 234 |
SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
|
| 235 |
'send.from: pass root as option')
|
| 236 |
|
| 237 |
SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
|
| 238 |
'send.root: pass root as option')
|
| 239 |
|
| 240 |
/**
|
| 241 |
* Set max-age to `maxAge`.
|
| 242 |
*
|
| 243 |
* @param {Number} maxAge
|
| 244 |
* @return {SendStream}
|
| 245 |
* @api public
|
| 246 |
*/
|
| 247 |
|
| 248 |
SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) { |
| 249 |
this._maxage = typeof maxAge === 'string' |
| 250 |
? ms(maxAge) |
| 251 |
: Number(maxAge) |
| 252 |
this._maxage = !isNaN(this._maxage) |
| 253 |
? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) |
| 254 |
: 0
|
| 255 |
debug('max-age %d', this._maxage) |
| 256 |
return this |
| 257 |
}, 'send.maxage: pass maxAge as option')
|
| 258 |
|
| 259 |
/**
|
| 260 |
* Emit error with `status`.
|
| 261 |
*
|
| 262 |
* @param {number} status
|
| 263 |
* @param {Error} [err]
|
| 264 |
* @private
|
| 265 |
*/
|
| 266 |
|
| 267 |
SendStream.prototype.error = function error (status, err) { |
| 268 |
// emit if listeners instead of responding
|
| 269 |
if (hasListeners(this, 'error')) { |
| 270 |
return this.emit('error', createError(status, err, { |
| 271 |
expose: false |
| 272 |
})) |
| 273 |
} |
| 274 |
|
| 275 |
var res = this.res |
| 276 |
var msg = statuses[status] || String(status)
|
| 277 |
var doc = createHtmlDocument('Error', escapeHtml(msg)) |
| 278 |
|
| 279 |
// clear existing headers
|
| 280 |
clearHeaders(res) |
| 281 |
|
| 282 |
// add error headers
|
| 283 |
if (err && err.headers) {
|
| 284 |
setHeaders(res, err.headers) |
| 285 |
} |
| 286 |
|
| 287 |
// send basic response
|
| 288 |
res.statusCode = status |
| 289 |
res.setHeader('Content-Type', 'text/html; charset=UTF-8') |
| 290 |
res.setHeader('Content-Length', Buffer.byteLength(doc))
|
| 291 |
res.setHeader('Content-Security-Policy', "default-src 'self'") |
| 292 |
res.setHeader('X-Content-Type-Options', 'nosniff') |
| 293 |
res.end(doc) |
| 294 |
} |
| 295 |
|
| 296 |
/**
|
| 297 |
* Check if the pathname ends with "/".
|
| 298 |
*
|
| 299 |
* @return {boolean}
|
| 300 |
* @private
|
| 301 |
*/
|
| 302 |
|
| 303 |
SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () { |
| 304 |
return this.path[this.path.length - 1] === '/' |
| 305 |
} |
| 306 |
|
| 307 |
/**
|
| 308 |
* Check if this is a conditional GET request.
|
| 309 |
*
|
| 310 |
* @return {Boolean}
|
| 311 |
* @api private
|
| 312 |
*/
|
| 313 |
|
| 314 |
SendStream.prototype.isConditionalGET = function isConditionalGET () { |
| 315 |
return this.req.headers['if-match'] || |
| 316 |
this.req.headers['if-unmodified-since'] || |
| 317 |
this.req.headers['if-none-match'] || |
| 318 |
this.req.headers['if-modified-since'] |
| 319 |
} |
| 320 |
|
| 321 |
/**
|
| 322 |
* Check if the request preconditions failed.
|
| 323 |
*
|
| 324 |
* @return {boolean}
|
| 325 |
* @private
|
| 326 |
*/
|
| 327 |
|
| 328 |
SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () { |
| 329 |
var req = this.req |
| 330 |
var res = this.res |
| 331 |
|
| 332 |
// if-match
|
| 333 |
var match = req.headers['if-match'] |
| 334 |
if (match) {
|
| 335 |
var etag = res.getHeader('ETag') |
| 336 |
return !etag || (match !== '*' && parseTokenList(match).every(function (match) { |
| 337 |
return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag |
| 338 |
})) |
| 339 |
} |
| 340 |
|
| 341 |
// if-unmodified-since
|
| 342 |
var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) |
| 343 |
if (!isNaN(unmodifiedSince)) {
|
| 344 |
var lastModified = parseHttpDate(res.getHeader('Last-Modified')) |
| 345 |
return isNaN(lastModified) || lastModified > unmodifiedSince
|
| 346 |
} |
| 347 |
|
| 348 |
return false |
| 349 |
} |
| 350 |
|
| 351 |
/**
|
| 352 |
* Strip content-* header fields.
|
| 353 |
*
|
| 354 |
* @private
|
| 355 |
*/
|
| 356 |
|
| 357 |
SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () { |
| 358 |
var res = this.res |
| 359 |
var headers = getHeaderNames(res)
|
| 360 |
|
| 361 |
for (var i = 0; i < headers.length; i++) { |
| 362 |
var header = headers[i]
|
| 363 |
if (header.substr(0, 8) === 'content-' && header !== 'content-location') { |
| 364 |
res.removeHeader(header) |
| 365 |
} |
| 366 |
} |
| 367 |
} |
| 368 |
|
| 369 |
/**
|
| 370 |
* Respond with 304 not modified.
|
| 371 |
*
|
| 372 |
* @api private
|
| 373 |
*/
|
| 374 |
|
| 375 |
SendStream.prototype.notModified = function notModified () { |
| 376 |
var res = this.res |
| 377 |
debug('not modified')
|
| 378 |
this.removeContentHeaderFields()
|
| 379 |
res.statusCode = 304
|
| 380 |
res.end() |
| 381 |
} |
| 382 |
|
| 383 |
/**
|
| 384 |
* Raise error that headers already sent.
|
| 385 |
*
|
| 386 |
* @api private
|
| 387 |
*/
|
| 388 |
|
| 389 |
SendStream.prototype.headersAlreadySent = function headersAlreadySent () { |
| 390 |
var err = new Error('Can\'t set headers after they are sent.') |
| 391 |
debug('headers already sent')
|
| 392 |
this.error(500, err) |
| 393 |
} |
| 394 |
|
| 395 |
/**
|
| 396 |
* Check if the request is cacheable, aka
|
| 397 |
* responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
|
| 398 |
*
|
| 399 |
* @return {Boolean}
|
| 400 |
* @api private
|
| 401 |
*/
|
| 402 |
|
| 403 |
SendStream.prototype.isCachable = function isCachable () { |
| 404 |
var statusCode = this.res.statusCode |
| 405 |
return (statusCode >= 200 && statusCode < 300) || |
| 406 |
statusCode === 304
|
| 407 |
} |
| 408 |
|
| 409 |
/**
|
| 410 |
* Handle stat() error.
|
| 411 |
*
|
| 412 |
* @param {Error} error
|
| 413 |
* @private
|
| 414 |
*/
|
| 415 |
|
| 416 |
SendStream.prototype.onStatError = function onStatError (error) { |
| 417 |
switch (error.code) {
|
| 418 |
case 'ENAMETOOLONG': |
| 419 |
case 'ENOENT': |
| 420 |
case 'ENOTDIR': |
| 421 |
this.error(404, error) |
| 422 |
break
|
| 423 |
default:
|
| 424 |
this.error(500, error) |
| 425 |
break
|
| 426 |
} |
| 427 |
} |
| 428 |
|
| 429 |
/**
|
| 430 |
* Check if the cache is fresh.
|
| 431 |
*
|
| 432 |
* @return {Boolean}
|
| 433 |
* @api private
|
| 434 |
*/
|
| 435 |
|
| 436 |
SendStream.prototype.isFresh = function isFresh () { |
| 437 |
return fresh(this.req.headers, { |
| 438 |
'etag': this.res.getHeader('ETag'), |
| 439 |
'last-modified': this.res.getHeader('Last-Modified') |
| 440 |
}) |
| 441 |
} |
| 442 |
|
| 443 |
/**
|
| 444 |
* Check if the range is fresh.
|
| 445 |
*
|
| 446 |
* @return {Boolean}
|
| 447 |
* @api private
|
| 448 |
*/
|
| 449 |
|
| 450 |
SendStream.prototype.isRangeFresh = function isRangeFresh () { |
| 451 |
var ifRange = this.req.headers['if-range'] |
| 452 |
|
| 453 |
if (!ifRange) {
|
| 454 |
return true |
| 455 |
} |
| 456 |
|
| 457 |
// if-range as etag
|
| 458 |
if (ifRange.indexOf('"') !== -1) { |
| 459 |
var etag = this.res.getHeader('ETag') |
| 460 |
return Boolean(etag && ifRange.indexOf(etag) !== -1) |
| 461 |
} |
| 462 |
|
| 463 |
// if-range as modified date
|
| 464 |
var lastModified = this.res.getHeader('Last-Modified') |
| 465 |
return parseHttpDate(lastModified) <= parseHttpDate(ifRange)
|
| 466 |
} |
| 467 |
|
| 468 |
/**
|
| 469 |
* Redirect to path.
|
| 470 |
*
|
| 471 |
* @param {string} path
|
| 472 |
* @private
|
| 473 |
*/
|
| 474 |
|
| 475 |
SendStream.prototype.redirect = function redirect (path) { |
| 476 |
var res = this.res |
| 477 |
|
| 478 |
if (hasListeners(this, 'directory')) { |
| 479 |
this.emit('directory', res, path) |
| 480 |
return
|
| 481 |
} |
| 482 |
|
| 483 |
if (this.hasTrailingSlash()) { |
| 484 |
this.error(403) |
| 485 |
return
|
| 486 |
} |
| 487 |
|
| 488 |
var loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) |
| 489 |
var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' + |
| 490 |
escapeHtml(loc) + '</a>')
|
| 491 |
|
| 492 |
// redirect
|
| 493 |
res.statusCode = 301
|
| 494 |
res.setHeader('Content-Type', 'text/html; charset=UTF-8') |
| 495 |
res.setHeader('Content-Length', Buffer.byteLength(doc))
|
| 496 |
res.setHeader('Content-Security-Policy', "default-src 'self'") |
| 497 |
res.setHeader('X-Content-Type-Options', 'nosniff') |
| 498 |
res.setHeader('Location', loc)
|
| 499 |
res.end(doc) |
| 500 |
} |
| 501 |
|
| 502 |
/**
|
| 503 |
* Pipe to `res.
|
| 504 |
*
|
| 505 |
* @param {Stream} res
|
| 506 |
* @return {Stream} res
|
| 507 |
* @api public
|
| 508 |
*/
|
| 509 |
|
| 510 |
SendStream.prototype.pipe = function pipe (res) { |
| 511 |
// root path
|
| 512 |
var root = this._root |
| 513 |
|
| 514 |
// references
|
| 515 |
this.res = res
|
| 516 |
|
| 517 |
// decode the path
|
| 518 |
var path = decode(this.path) |
| 519 |
if (path === -1) { |
| 520 |
this.error(400) |
| 521 |
return res
|
| 522 |
} |
| 523 |
|
| 524 |
// null byte(s)
|
| 525 |
if (~path.indexOf('\0')) { |
| 526 |
this.error(400) |
| 527 |
return res
|
| 528 |
} |
| 529 |
|
| 530 |
var parts
|
| 531 |
if (root !== null) { |
| 532 |
// normalize
|
| 533 |
if (path) {
|
| 534 |
path = normalize('.' + sep + path)
|
| 535 |
} |
| 536 |
|
| 537 |
// malicious path
|
| 538 |
if (UP_PATH_REGEXP.test(path)) {
|
| 539 |
debug('malicious path "%s"', path)
|
| 540 |
this.error(403) |
| 541 |
return res
|
| 542 |
} |
| 543 |
|
| 544 |
// explode path parts
|
| 545 |
parts = path.split(sep) |
| 546 |
|
| 547 |
// join / normalize from optional root dir
|
| 548 |
path = normalize(join(root, path)) |
| 549 |
root = normalize(root + sep) |
| 550 |
} else {
|
| 551 |
// ".." is malicious without "root"
|
| 552 |
if (UP_PATH_REGEXP.test(path)) {
|
| 553 |
debug('malicious path "%s"', path)
|
| 554 |
this.error(403) |
| 555 |
return res
|
| 556 |
} |
| 557 |
|
| 558 |
// explode path parts
|
| 559 |
parts = normalize(path).split(sep) |
| 560 |
|
| 561 |
// resolve the path
|
| 562 |
path = resolve(path) |
| 563 |
} |
| 564 |
|
| 565 |
// dotfile handling
|
| 566 |
if (containsDotFile(parts)) {
|
| 567 |
var access = this._dotfiles |
| 568 |
|
| 569 |
// legacy support
|
| 570 |
if (access === undefined) { |
| 571 |
access = parts[parts.length - 1][0] === '.' |
| 572 |
? (this._hidden ? 'allow' : 'ignore') |
| 573 |
: 'allow'
|
| 574 |
} |
| 575 |
|
| 576 |
debug('%s dotfile "%s"', access, path)
|
| 577 |
switch (access) {
|
| 578 |
case 'allow': |
| 579 |
break
|
| 580 |
case 'deny': |
| 581 |
this.error(403) |
| 582 |
return res
|
| 583 |
case 'ignore': |
| 584 |
default:
|
| 585 |
this.error(404) |
| 586 |
return res
|
| 587 |
} |
| 588 |
} |
| 589 |
|
| 590 |
// index file support
|
| 591 |
if (this._index.length && this.hasTrailingSlash()) { |
| 592 |
this.sendIndex(path)
|
| 593 |
return res
|
| 594 |
} |
| 595 |
|
| 596 |
this.sendFile(path)
|
| 597 |
return res
|
| 598 |
} |
| 599 |
|
| 600 |
/**
|
| 601 |
* Transfer `path`.
|
| 602 |
*
|
| 603 |
* @param {String} path
|
| 604 |
* @api public
|
| 605 |
*/
|
| 606 |
|
| 607 |
SendStream.prototype.send = function send (path, stat) { |
| 608 |
var len = stat.size
|
| 609 |
var options = this.options |
| 610 |
var opts = {}
|
| 611 |
var res = this.res |
| 612 |
var req = this.req |
| 613 |
var ranges = req.headers.range
|
| 614 |
var offset = options.start || 0 |
| 615 |
|
| 616 |
if (headersSent(res)) {
|
| 617 |
// impossible to send now
|
| 618 |
this.headersAlreadySent()
|
| 619 |
return
|
| 620 |
} |
| 621 |
|
| 622 |
debug('pipe "%s"', path)
|
| 623 |
|
| 624 |
// set header fields
|
| 625 |
this.setHeader(path, stat)
|
| 626 |
|
| 627 |
// set content-type
|
| 628 |
this.type(path)
|
| 629 |
|
| 630 |
// conditional GET support
|
| 631 |
if (this.isConditionalGET()) { |
| 632 |
if (this.isPreconditionFailure()) { |
| 633 |
this.error(412) |
| 634 |
return
|
| 635 |
} |
| 636 |
|
| 637 |
if (this.isCachable() && this.isFresh()) { |
| 638 |
this.notModified()
|
| 639 |
return
|
| 640 |
} |
| 641 |
} |
| 642 |
|
| 643 |
// adjust len to start/end options
|
| 644 |
len = Math.max(0, len - offset)
|
| 645 |
if (options.end !== undefined) { |
| 646 |
var bytes = options.end - offset + 1 |
| 647 |
if (len > bytes) len = bytes
|
| 648 |
} |
| 649 |
|
| 650 |
// Range support
|
| 651 |
if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) { |
| 652 |
// parse
|
| 653 |
ranges = parseRange(len, ranges, {
|
| 654 |
combine: true |
| 655 |
}) |
| 656 |
|
| 657 |
// If-Range support
|
| 658 |
if (!this.isRangeFresh()) { |
| 659 |
debug('range stale')
|
| 660 |
ranges = -2
|
| 661 |
} |
| 662 |
|
| 663 |
// unsatisfiable
|
| 664 |
if (ranges === -1) { |
| 665 |
debug('range unsatisfiable')
|
| 666 |
|
| 667 |
// Content-Range
|
| 668 |
res.setHeader('Content-Range', contentRange('bytes', len)) |
| 669 |
|
| 670 |
// 416 Requested Range Not Satisfiable
|
| 671 |
return this.error(416, { |
| 672 |
headers: {'Content-Range': res.getHeader('Content-Range')} |
| 673 |
}) |
| 674 |
} |
| 675 |
|
| 676 |
// valid (syntactically invalid/multiple ranges are treated as a regular response)
|
| 677 |
if (ranges !== -2 && ranges.length === 1) { |
| 678 |
debug('range %j', ranges)
|
| 679 |
|
| 680 |
// Content-Range
|
| 681 |
res.statusCode = 206
|
| 682 |
res.setHeader('Content-Range', contentRange('bytes', len, ranges[0])) |
| 683 |
|
| 684 |
// adjust for requested range
|
| 685 |
offset += ranges[0].start
|
| 686 |
len = ranges[0].end - ranges[0].start + 1 |
| 687 |
} |
| 688 |
} |
| 689 |
|
| 690 |
// clone options
|
| 691 |
for (var prop in options) { |
| 692 |
opts[prop] = options[prop] |
| 693 |
} |
| 694 |
|
| 695 |
// set read options
|
| 696 |
opts.start = offset |
| 697 |
opts.end = Math.max(offset, offset + len - 1)
|
| 698 |
|
| 699 |
// content-length
|
| 700 |
res.setHeader('Content-Length', len)
|
| 701 |
|
| 702 |
// HEAD support
|
| 703 |
if (req.method === 'HEAD') { |
| 704 |
res.end() |
| 705 |
return
|
| 706 |
} |
| 707 |
|
| 708 |
this.stream(path, opts)
|
| 709 |
} |
| 710 |
|
| 711 |
/**
|
| 712 |
* Transfer file for `path`.
|
| 713 |
*
|
| 714 |
* @param {String} path
|
| 715 |
* @api private
|
| 716 |
*/
|
| 717 |
SendStream.prototype.sendFile = function sendFile (path) { |
| 718 |
var i = 0 |
| 719 |
var self = this |
| 720 |
|
| 721 |
debug('stat "%s"', path)
|
| 722 |
fs.stat(path, function onstat (err, stat) { |
| 723 |
if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) { |
| 724 |
// not found, check extensions
|
| 725 |
return next(err)
|
| 726 |
} |
| 727 |
if (err) return self.onStatError(err) |
| 728 |
if (stat.isDirectory()) return self.redirect(path) |
| 729 |
self.emit('file', path, stat)
|
| 730 |
self.send(path, stat) |
| 731 |
}) |
| 732 |
|
| 733 |
function next (err) { |
| 734 |
if (self._extensions.length <= i) {
|
| 735 |
return err
|
| 736 |
? self.onStatError(err) |
| 737 |
: self.error(404)
|
| 738 |
} |
| 739 |
|
| 740 |
var p = path + '.' + self._extensions[i++] |
| 741 |
|
| 742 |
debug('stat "%s"', p)
|
| 743 |
fs.stat(p, function (err, stat) {
|
| 744 |
if (err) return next(err) |
| 745 |
if (stat.isDirectory()) return next() |
| 746 |
self.emit('file', p, stat)
|
| 747 |
self.send(p, stat) |
| 748 |
}) |
| 749 |
} |
| 750 |
} |
| 751 |
|
| 752 |
/**
|
| 753 |
* Transfer index for `path`.
|
| 754 |
*
|
| 755 |
* @param {String} path
|
| 756 |
* @api private
|
| 757 |
*/
|
| 758 |
SendStream.prototype.sendIndex = function sendIndex (path) { |
| 759 |
var i = -1 |
| 760 |
var self = this |
| 761 |
|
| 762 |
function next (err) { |
| 763 |
if (++i >= self._index.length) {
|
| 764 |
if (err) return self.onStatError(err) |
| 765 |
return self.error(404) |
| 766 |
} |
| 767 |
|
| 768 |
var p = join(path, self._index[i])
|
| 769 |
|
| 770 |
debug('stat "%s"', p)
|
| 771 |
fs.stat(p, function (err, stat) {
|
| 772 |
if (err) return next(err) |
| 773 |
if (stat.isDirectory()) return next() |
| 774 |
self.emit('file', p, stat)
|
| 775 |
self.send(p, stat) |
| 776 |
}) |
| 777 |
} |
| 778 |
|
| 779 |
next() |
| 780 |
} |
| 781 |
|
| 782 |
/**
|
| 783 |
* Stream `path` to the response.
|
| 784 |
*
|
| 785 |
* @param {String} path
|
| 786 |
* @param {Object} options
|
| 787 |
* @api private
|
| 788 |
*/
|
| 789 |
|
| 790 |
SendStream.prototype.stream = function stream (path, options) { |
| 791 |
// TODO: this is all lame, refactor meeee
|
| 792 |
var finished = false |
| 793 |
var self = this |
| 794 |
var res = this.res |
| 795 |
|
| 796 |
// pipe
|
| 797 |
var stream = fs.createReadStream(path, options)
|
| 798 |
this.emit('stream', stream) |
| 799 |
stream.pipe(res) |
| 800 |
|
| 801 |
// response finished, done with the fd
|
| 802 |
onFinished(res, function onfinished () { |
| 803 |
finished = true
|
| 804 |
destroy(stream) |
| 805 |
}) |
| 806 |
|
| 807 |
// error handling code-smell
|
| 808 |
stream.on('error', function onerror (err) { |
| 809 |
// request already finished
|
| 810 |
if (finished) return |
| 811 |
|
| 812 |
// clean up stream
|
| 813 |
finished = true
|
| 814 |
destroy(stream) |
| 815 |
|
| 816 |
// error
|
| 817 |
self.onStatError(err) |
| 818 |
}) |
| 819 |
|
| 820 |
// end
|
| 821 |
stream.on('end', function onend () { |
| 822 |
self.emit('end')
|
| 823 |
}) |
| 824 |
} |
| 825 |
|
| 826 |
/**
|
| 827 |
* Set content-type based on `path`
|
| 828 |
* if it hasn't been explicitly set.
|
| 829 |
*
|
| 830 |
* @param {String} path
|
| 831 |
* @api private
|
| 832 |
*/
|
| 833 |
|
| 834 |
SendStream.prototype.type = function type (path) { |
| 835 |
var res = this.res |
| 836 |
|
| 837 |
if (res.getHeader('Content-Type')) return |
| 838 |
|
| 839 |
var type = mime.lookup(path)
|
| 840 |
|
| 841 |
if (!type) {
|
| 842 |
debug('no content-type')
|
| 843 |
return
|
| 844 |
} |
| 845 |
|
| 846 |
var charset = mime.charsets.lookup(type)
|
| 847 |
|
| 848 |
debug('content-type %s', type)
|
| 849 |
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')) |
| 850 |
} |
| 851 |
|
| 852 |
/**
|
| 853 |
* Set response header fields, most
|
| 854 |
* fields may be pre-defined.
|
| 855 |
*
|
| 856 |
* @param {String} path
|
| 857 |
* @param {Object} stat
|
| 858 |
* @api private
|
| 859 |
*/
|
| 860 |
|
| 861 |
SendStream.prototype.setHeader = function setHeader (path, stat) { |
| 862 |
var res = this.res |
| 863 |
|
| 864 |
this.emit('headers', res, path, stat) |
| 865 |
|
| 866 |
if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { |
| 867 |
debug('accept ranges')
|
| 868 |
res.setHeader('Accept-Ranges', 'bytes') |
| 869 |
} |
| 870 |
|
| 871 |
if (this._cacheControl && !res.getHeader('Cache-Control')) { |
| 872 |
var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) |
| 873 |
|
| 874 |
if (this._immutable) { |
| 875 |
cacheControl += ', immutable'
|
| 876 |
} |
| 877 |
|
| 878 |
debug('cache-control %s', cacheControl)
|
| 879 |
res.setHeader('Cache-Control', cacheControl)
|
| 880 |
} |
| 881 |
|
| 882 |
if (this._lastModified && !res.getHeader('Last-Modified')) { |
| 883 |
var modified = stat.mtime.toUTCString()
|
| 884 |
debug('modified %s', modified)
|
| 885 |
res.setHeader('Last-Modified', modified)
|
| 886 |
} |
| 887 |
|
| 888 |
if (this._etag && !res.getHeader('ETag')) { |
| 889 |
var val = etag(stat)
|
| 890 |
debug('etag %s', val)
|
| 891 |
res.setHeader('ETag', val)
|
| 892 |
} |
| 893 |
} |
| 894 |
|
| 895 |
/**
|
| 896 |
* Clear all headers from a response.
|
| 897 |
*
|
| 898 |
* @param {object} res
|
| 899 |
* @private
|
| 900 |
*/
|
| 901 |
|
| 902 |
function clearHeaders (res) { |
| 903 |
var headers = getHeaderNames(res)
|
| 904 |
|
| 905 |
for (var i = 0; i < headers.length; i++) { |
| 906 |
res.removeHeader(headers[i]) |
| 907 |
} |
| 908 |
} |
| 909 |
|
| 910 |
/**
|
| 911 |
* Collapse all leading slashes into a single slash
|
| 912 |
*
|
| 913 |
* @param {string} str
|
| 914 |
* @private
|
| 915 |
*/
|
| 916 |
function collapseLeadingSlashes (str) { |
| 917 |
for (var i = 0; i < str.length; i++) { |
| 918 |
if (str[i] !== '/') { |
| 919 |
break
|
| 920 |
} |
| 921 |
} |
| 922 |
|
| 923 |
return i > 1 |
| 924 |
? '/' + str.substr(i)
|
| 925 |
: str |
| 926 |
} |
| 927 |
|
| 928 |
/**
|
| 929 |
* Determine if path parts contain a dotfile.
|
| 930 |
*
|
| 931 |
* @api private
|
| 932 |
*/
|
| 933 |
|
| 934 |
function containsDotFile (parts) { |
| 935 |
for (var i = 0; i < parts.length; i++) { |
| 936 |
var part = parts[i]
|
| 937 |
if (part.length > 1 && part[0] === '.') { |
| 938 |
return true |
| 939 |
} |
| 940 |
} |
| 941 |
|
| 942 |
return false |
| 943 |
} |
| 944 |
|
| 945 |
/**
|
| 946 |
* Create a Content-Range header.
|
| 947 |
*
|
| 948 |
* @param {string} type
|
| 949 |
* @param {number} size
|
| 950 |
* @param {array} [range]
|
| 951 |
*/
|
| 952 |
|
| 953 |
function contentRange (type, size, range) { |
| 954 |
return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size |
| 955 |
} |
| 956 |
|
| 957 |
/**
|
| 958 |
* Create a minimal HTML document.
|
| 959 |
*
|
| 960 |
* @param {string} title
|
| 961 |
* @param {string} body
|
| 962 |
* @private
|
| 963 |
*/
|
| 964 |
|
| 965 |
function createHtmlDocument (title, body) { |
| 966 |
return '<!DOCTYPE html>\n' + |
| 967 |
'<html lang="en">\n' +
|
| 968 |
'<head>\n' +
|
| 969 |
'<meta charset="utf-8">\n' +
|
| 970 |
'<title>' + title + '</title>\n' + |
| 971 |
'</head>\n' +
|
| 972 |
'<body>\n' +
|
| 973 |
'<pre>' + body + '</pre>\n' + |
| 974 |
'</body>\n' +
|
| 975 |
'</html>\n'
|
| 976 |
} |
| 977 |
|
| 978 |
/**
|
| 979 |
* decodeURIComponent.
|
| 980 |
*
|
| 981 |
* Allows V8 to only deoptimize this fn instead of all
|
| 982 |
* of send().
|
| 983 |
*
|
| 984 |
* @param {String} path
|
| 985 |
* @api private
|
| 986 |
*/
|
| 987 |
|
| 988 |
function decode (path) { |
| 989 |
try {
|
| 990 |
return decodeURIComponent(path)
|
| 991 |
} catch (err) {
|
| 992 |
return -1 |
| 993 |
} |
| 994 |
} |
| 995 |
|
| 996 |
/**
|
| 997 |
* Get the header names on a respnse.
|
| 998 |
*
|
| 999 |
* @param {object} res
|
| 1000 |
* @returns {array[string]}
|
| 1001 |
* @private
|
| 1002 |
*/
|
| 1003 |
|
| 1004 |
function getHeaderNames (res) { |
| 1005 |
return typeof res.getHeaderNames !== 'function' |
| 1006 |
? Object.keys(res._headers || {})
|
| 1007 |
: res.getHeaderNames() |
| 1008 |
} |
| 1009 |
|
| 1010 |
/**
|
| 1011 |
* Determine if emitter has listeners of a given type.
|
| 1012 |
*
|
| 1013 |
* The way to do this check is done three different ways in Node.js >= 0.8
|
| 1014 |
* so this consolidates them into a minimal set using instance methods.
|
| 1015 |
*
|
| 1016 |
* @param {EventEmitter} emitter
|
| 1017 |
* @param {string} type
|
| 1018 |
* @returns {boolean}
|
| 1019 |
* @private
|
| 1020 |
*/
|
| 1021 |
|
| 1022 |
function hasListeners (emitter, type) { |
| 1023 |
var count = typeof emitter.listenerCount !== 'function' |
| 1024 |
? emitter.listeners(type).length |
| 1025 |
: emitter.listenerCount(type) |
| 1026 |
|
| 1027 |
return count > 0 |
| 1028 |
} |
| 1029 |
|
| 1030 |
/**
|
| 1031 |
* Determine if the response headers have been sent.
|
| 1032 |
*
|
| 1033 |
* @param {object} res
|
| 1034 |
* @returns {boolean}
|
| 1035 |
* @private
|
| 1036 |
*/
|
| 1037 |
|
| 1038 |
function headersSent (res) { |
| 1039 |
return typeof res.headersSent !== 'boolean' |
| 1040 |
? Boolean(res._header) |
| 1041 |
: res.headersSent |
| 1042 |
} |
| 1043 |
|
| 1044 |
/**
|
| 1045 |
* Normalize the index option into an array.
|
| 1046 |
*
|
| 1047 |
* @param {boolean|string|array} val
|
| 1048 |
* @param {string} name
|
| 1049 |
* @private
|
| 1050 |
*/
|
| 1051 |
|
| 1052 |
function normalizeList (val, name) { |
| 1053 |
var list = [].concat(val || [])
|
| 1054 |
|
| 1055 |
for (var i = 0; i < list.length; i++) { |
| 1056 |
if (typeof list[i] !== 'string') { |
| 1057 |
throw new TypeError(name + ' must be array of strings or false') |
| 1058 |
} |
| 1059 |
} |
| 1060 |
|
| 1061 |
return list
|
| 1062 |
} |
| 1063 |
|
| 1064 |
/**
|
| 1065 |
* Parse an HTTP Date into a number.
|
| 1066 |
*
|
| 1067 |
* @param {string} date
|
| 1068 |
* @private
|
| 1069 |
*/
|
| 1070 |
|
| 1071 |
function parseHttpDate (date) { |
| 1072 |
var timestamp = date && Date.parse(date)
|
| 1073 |
|
| 1074 |
return typeof timestamp === 'number' |
| 1075 |
? timestamp |
| 1076 |
: NaN
|
| 1077 |
} |
| 1078 |
|
| 1079 |
/**
|
| 1080 |
* Parse a HTTP token list.
|
| 1081 |
*
|
| 1082 |
* @param {string} str
|
| 1083 |
* @private
|
| 1084 |
*/
|
| 1085 |
|
| 1086 |
function parseTokenList (str) { |
| 1087 |
var end = 0 |
| 1088 |
var list = []
|
| 1089 |
var start = 0 |
| 1090 |
|
| 1091 |
// gather tokens
|
| 1092 |
for (var i = 0, len = str.length; i < len; i++) { |
| 1093 |
switch (str.charCodeAt(i)) {
|
| 1094 |
case 0x20: /* */ |
| 1095 |
if (start === end) {
|
| 1096 |
start = end = i + 1
|
| 1097 |
} |
| 1098 |
break
|
| 1099 |
case 0x2c: /* , */ |
| 1100 |
list.push(str.substring(start, end)) |
| 1101 |
start = end = i + 1
|
| 1102 |
break
|
| 1103 |
default:
|
| 1104 |
end = i + 1
|
| 1105 |
break
|
| 1106 |
} |
| 1107 |
} |
| 1108 |
|
| 1109 |
// final token
|
| 1110 |
list.push(str.substring(start, end)) |
| 1111 |
|
| 1112 |
return list
|
| 1113 |
} |
| 1114 |
|
| 1115 |
/**
|
| 1116 |
* Set an object of headers on a response.
|
| 1117 |
*
|
| 1118 |
* @param {object} res
|
| 1119 |
* @param {object} headers
|
| 1120 |
* @private
|
| 1121 |
*/
|
| 1122 |
|
| 1123 |
function setHeaders (res, headers) { |
| 1124 |
var keys = Object.keys(headers)
|
| 1125 |
|
| 1126 |
for (var i = 0; i < keys.length; i++) { |
| 1127 |
var key = keys[i]
|
| 1128 |
res.setHeader(key, headers[key]) |
| 1129 |
} |
| 1130 |
} |