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 |
} |