| /*! |
| * send |
| * Copyright(c) 2012 TJ Holowaychuk |
| * Copyright(c) 2014-2016 Douglas Christopher Wilson |
| * MIT Licensed |
| */ |
| |
| 'use strict' |
| |
| /** |
| * Module dependencies. |
| * @private |
| */ |
| |
| var createError = require('http-errors') |
| var debug = require('debug')('send') |
| var deprecate = require('depd')('send') |
| var destroy = require('destroy') |
| var encodeUrl = require('encodeurl') |
| var escapeHtml = require('escape-html') |
| var etag = require('etag') |
| var fresh = require('fresh') |
| var fs = require('fs') |
| var mime = require('mime') |
| var ms = require('ms') |
| var onFinished = require('on-finished') |
| var parseRange = require('range-parser') |
| var path = require('path') |
| var statuses = require('statuses') |
| var Stream = require('stream') |
| var util = require('util') |
| |
| /** |
| * Path function references. |
| * @private |
| */ |
| |
| var extname = path.extname |
| var join = path.join |
| var normalize = path.normalize |
| var resolve = path.resolve |
| var sep = path.sep |
| |
| /** |
| * Regular expression for identifying a bytes Range header. |
| * @private |
| */ |
| |
| var BYTES_RANGE_REGEXP = /^ *bytes=/ |
| |
| /** |
| * Maximum value allowed for the max age. |
| * @private |
| */ |
| |
| var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year |
| |
| /** |
| * Regular expression to match a path with a directory up component. |
| * @private |
| */ |
| |
| var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ |
| |
| /** |
| * Module exports. |
| * @public |
| */ |
| |
| module.exports = send |
| module.exports.mime = mime |
| |
| /** |
| * Return a `SendStream` for `req` and `path`. |
| * |
| * @param {object} req |
| * @param {string} path |
| * @param {object} [options] |
| * @return {SendStream} |
| * @public |
| */ |
| |
| function send (req, path, options) { |
| return new SendStream(req, path, options) |
| } |
| |
| /** |
| * Initialize a `SendStream` with the given `path`. |
| * |
| * @param {Request} req |
| * @param {String} path |
| * @param {object} [options] |
| * @private |
| */ |
| |
| function SendStream (req, path, options) { |
| Stream.call(this) |
| |
| var opts = options || {} |
| |
| this.options = opts |
| this.path = path |
| this.req = req |
| |
| this._acceptRanges = opts.acceptRanges !== undefined |
| ? Boolean(opts.acceptRanges) |
| : true |
| |
| this._cacheControl = opts.cacheControl !== undefined |
| ? Boolean(opts.cacheControl) |
| : true |
| |
| this._etag = opts.etag !== undefined |
| ? Boolean(opts.etag) |
| : true |
| |
| this._dotfiles = opts.dotfiles !== undefined |
| ? opts.dotfiles |
| : 'ignore' |
| |
| if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') { |
| throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') |
| } |
| |
| this._hidden = Boolean(opts.hidden) |
| |
| if (opts.hidden !== undefined) { |
| deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead') |
| } |
| |
| // legacy support |
| if (opts.dotfiles === undefined) { |
| this._dotfiles = undefined |
| } |
| |
| this._extensions = opts.extensions !== undefined |
| ? normalizeList(opts.extensions, 'extensions option') |
| : [] |
| |
| this._immutable = opts.immutable !== undefined |
| ? Boolean(opts.immutable) |
| : false |
| |
| this._index = opts.index !== undefined |
| ? normalizeList(opts.index, 'index option') |
| : ['index.html'] |
| |
| this._lastModified = opts.lastModified !== undefined |
| ? Boolean(opts.lastModified) |
| : true |
| |
| this._maxage = opts.maxAge || opts.maxage |
| this._maxage = typeof this._maxage === 'string' |
| ? ms(this._maxage) |
| : Number(this._maxage) |
| this._maxage = !isNaN(this._maxage) |
| ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) |
| : 0 |
| |
| this._root = opts.root |
| ? resolve(opts.root) |
| : null |
| |
| if (!this._root && opts.from) { |
| this.from(opts.from) |
| } |
| } |
| |
| /** |
| * Inherits from `Stream`. |
| */ |
| |
| util.inherits(SendStream, Stream) |
| |
| /** |
| * Enable or disable etag generation. |
| * |
| * @param {Boolean} val |
| * @return {SendStream} |
| * @api public |
| */ |
| |
| SendStream.prototype.etag = deprecate.function(function etag (val) { |
| this._etag = Boolean(val) |
| debug('etag %s', this._etag) |
| return this |
| }, 'send.etag: pass etag as option') |
| |
| /** |
| * Enable or disable "hidden" (dot) files. |
| * |
| * @param {Boolean} path |
| * @return {SendStream} |
| * @api public |
| */ |
| |
| SendStream.prototype.hidden = deprecate.function(function hidden (val) { |
| this._hidden = Boolean(val) |
| this._dotfiles = undefined |
| debug('hidden %s', this._hidden) |
| return this |
| }, 'send.hidden: use dotfiles option') |
| |
| /** |
| * Set index `paths`, set to a falsy |
| * value to disable index support. |
| * |
| * @param {String|Boolean|Array} paths |
| * @return {SendStream} |
| * @api public |
| */ |
| |
| SendStream.prototype.index = deprecate.function(function index (paths) { |
| var index = !paths ? [] : normalizeList(paths, 'paths argument') |
| debug('index %o', paths) |
| this._index = index |
| return this |
| }, 'send.index: pass index as option') |
| |
| /** |
| * Set root `path`. |
| * |
| * @param {String} path |
| * @return {SendStream} |
| * @api public |
| */ |
| |
| SendStream.prototype.root = function root (path) { |
| this._root = resolve(String(path)) |
| debug('root %s', this._root) |
| return this |
| } |
| |
| SendStream.prototype.from = deprecate.function(SendStream.prototype.root, |
| 'send.from: pass root as option') |
| |
| SendStream.prototype.root = deprecate.function(SendStream.prototype.root, |
| 'send.root: pass root as option') |
| |
| /** |
| * Set max-age to `maxAge`. |
| * |
| * @param {Number} maxAge |
| * @return {SendStream} |
| * @api public |
| */ |
| |
| SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) { |
| this._maxage = typeof maxAge === 'string' |
| ? ms(maxAge) |
| : Number(maxAge) |
| this._maxage = !isNaN(this._maxage) |
| ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) |
| : 0 |
| debug('max-age %d', this._maxage) |
| return this |
| }, 'send.maxage: pass maxAge as option') |
| |
| /** |
| * Emit error with `status`. |
| * |
| * @param {number} status |
| * @param {Error} [err] |
| * @private |
| */ |
| |
| SendStream.prototype.error = function error (status, err) { |
| // emit if listeners instead of responding |
| if (hasListeners(this, 'error')) { |
| return this.emit('error', createError(status, err, { |
| expose: false |
| })) |
| } |
| |
| var res = this.res |
| var msg = statuses[status] || String(status) |
| var doc = createHtmlDocument('Error', escapeHtml(msg)) |
| |
| // clear existing headers |
| clearHeaders(res) |
| |
| // add error headers |
| if (err && err.headers) { |
| setHeaders(res, err.headers) |
| } |
| |
| // send basic response |
| res.statusCode = status |
| res.setHeader('Content-Type', 'text/html; charset=UTF-8') |
| res.setHeader('Content-Length', Buffer.byteLength(doc)) |
| res.setHeader('Content-Security-Policy', "default-src 'self'") |
| res.setHeader('X-Content-Type-Options', 'nosniff') |
| res.end(doc) |
| } |
| |
| /** |
| * Check if the pathname ends with "/". |
| * |
| * @return {boolean} |
| * @private |
| */ |
| |
| SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () { |
| return this.path[this.path.length - 1] === '/' |
| } |
| |
| /** |
| * Check if this is a conditional GET request. |
| * |
| * @return {Boolean} |
| * @api private |
| */ |
| |
| SendStream.prototype.isConditionalGET = function isConditionalGET () { |
| return this.req.headers['if-match'] || |
| this.req.headers['if-unmodified-since'] || |
| this.req.headers['if-none-match'] || |
| this.req.headers['if-modified-since'] |
| } |
| |
| /** |
| * Check if the request preconditions failed. |
| * |
| * @return {boolean} |
| * @private |
| */ |
| |
| SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () { |
| var req = this.req |
| var res = this.res |
| |
| // if-match |
| var match = req.headers['if-match'] |
| if (match) { |
| var etag = res.getHeader('ETag') |
| return !etag || (match !== '*' && parseTokenList(match).every(function (match) { |
| return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag |
| })) |
| } |
| |
| // if-unmodified-since |
| var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) |
| if (!isNaN(unmodifiedSince)) { |
| var lastModified = parseHttpDate(res.getHeader('Last-Modified')) |
| return isNaN(lastModified) || lastModified > unmodifiedSince |
| } |
| |
| return false |
| } |
| |
| /** |
| * Strip content-* header fields. |
| * |
| * @private |
| */ |
| |
| SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () { |
| var res = this.res |
| var headers = getHeaderNames(res) |
| |
| for (var i = 0; i < headers.length; i++) { |
| var header = headers[i] |
| if (header.substr(0, 8) === 'content-' && header !== 'content-location') { |
| res.removeHeader(header) |
| } |
| } |
| } |
| |
| /** |
| * Respond with 304 not modified. |
| * |
| * @api private |
| */ |
| |
| SendStream.prototype.notModified = function notModified () { |
| var res = this.res |
| debug('not modified') |
| this.removeContentHeaderFields() |
| res.statusCode = 304 |
| res.end() |
| } |
| |
| /** |
| * Raise error that headers already sent. |
| * |
| * @api private |
| */ |
| |
| SendStream.prototype.headersAlreadySent = function headersAlreadySent () { |
| var err = new Error('Can\'t set headers after they are sent.') |
| debug('headers already sent') |
| this.error(500, err) |
| } |
| |
| /** |
| * 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 isCachable () { |
| var statusCode = this.res.statusCode |
| return (statusCode >= 200 && statusCode < 300) || |
| statusCode === 304 |
| } |
| |
| /** |
| * Handle stat() error. |
| * |
| * @param {Error} error |
| * @private |
| */ |
| |
| SendStream.prototype.onStatError = function onStatError (error) { |
| switch (error.code) { |
| case 'ENAMETOOLONG': |
| case 'ENOENT': |
| case 'ENOTDIR': |
| this.error(404, error) |
| break |
| default: |
| this.error(500, error) |
| break |
| } |
| } |
| |
| /** |
| * Check if the cache is fresh. |
| * |
| * @return {Boolean} |
| * @api private |
| */ |
| |
| SendStream.prototype.isFresh = function isFresh () { |
| return fresh(this.req.headers, { |
| 'etag': this.res.getHeader('ETag'), |
| 'last-modified': this.res.getHeader('Last-Modified') |
| }) |
| } |
| |
| /** |
| * Check if the range is fresh. |
| * |
| * @return {Boolean} |
| * @api private |
| */ |
| |
| SendStream.prototype.isRangeFresh = function isRangeFresh () { |
| var ifRange = this.req.headers['if-range'] |
| |
| if (!ifRange) { |
| return true |
| } |
| |
| // if-range as etag |
| if (ifRange.indexOf('"') !== -1) { |
| var etag = this.res.getHeader('ETag') |
| return Boolean(etag && ifRange.indexOf(etag) !== -1) |
| } |
| |
| // if-range as modified date |
| var lastModified = this.res.getHeader('Last-Modified') |
| return parseHttpDate(lastModified) <= parseHttpDate(ifRange) |
| } |
| |
| /** |
| * Redirect to path. |
| * |
| * @param {string} path |
| * @private |
| */ |
| |
| SendStream.prototype.redirect = function redirect (path) { |
| var res = this.res |
| |
| if (hasListeners(this, 'directory')) { |
| this.emit('directory', res, path) |
| return |
| } |
| |
| if (this.hasTrailingSlash()) { |
| this.error(403) |
| return |
| } |
| |
| var loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) |
| var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' + |
| escapeHtml(loc) + '</a>') |
| |
| // redirect |
| res.statusCode = 301 |
| res.setHeader('Content-Type', 'text/html; charset=UTF-8') |
| res.setHeader('Content-Length', Buffer.byteLength(doc)) |
| res.setHeader('Content-Security-Policy', "default-src 'self'") |
| res.setHeader('X-Content-Type-Options', 'nosniff') |
| res.setHeader('Location', loc) |
| res.end(doc) |
| } |
| |
| /** |
| * Pipe to `res. |
| * |
| * @param {Stream} res |
| * @return {Stream} res |
| * @api public |
| */ |
| |
| SendStream.prototype.pipe = function pipe (res) { |
| // root path |
| var root = this._root |
| |
| // references |
| this.res = res |
| |
| // decode the path |
| var path = decode(this.path) |
| if (path === -1) { |
| this.error(400) |
| return res |
| } |
| |
| // null byte(s) |
| if (~path.indexOf('\0')) { |
| this.error(400) |
| return res |
| } |
| |
| var parts |
| if (root !== null) { |
| // normalize |
| if (path) { |
| path = normalize('.' + sep + path) |
| } |
| |
| // malicious path |
| if (UP_PATH_REGEXP.test(path)) { |
| debug('malicious path "%s"', path) |
| this.error(403) |
| return res |
| } |
| |
| // explode path parts |
| parts = path.split(sep) |
| |
| // join / normalize from optional root dir |
| path = normalize(join(root, path)) |
| root = normalize(root + sep) |
| } else { |
| // ".." is malicious without "root" |
| if (UP_PATH_REGEXP.test(path)) { |
| debug('malicious path "%s"', path) |
| this.error(403) |
| return res |
| } |
| |
| // explode path parts |
| parts = normalize(path).split(sep) |
| |
| // resolve the path |
| path = resolve(path) |
| } |
| |
| // dotfile handling |
| if (containsDotFile(parts)) { |
| var access = this._dotfiles |
| |
| // legacy support |
| if (access === undefined) { |
| access = parts[parts.length - 1][0] === '.' |
| ? (this._hidden ? 'allow' : 'ignore') |
| : 'allow' |
| } |
| |
| debug('%s dotfile "%s"', access, path) |
| switch (access) { |
| case 'allow': |
| break |
| case 'deny': |
| this.error(403) |
| return res |
| case 'ignore': |
| default: |
| this.error(404) |
| return res |
| } |
| } |
| |
| // index file support |
| if (this._index.length && this.hasTrailingSlash()) { |
| this.sendIndex(path) |
| return res |
| } |
| |
| this.sendFile(path) |
| return res |
| } |
| |
| /** |
| * Transfer `path`. |
| * |
| * @param {String} path |
| * @api public |
| */ |
| |
| SendStream.prototype.send = function send (path, stat) { |
| var len = stat.size |
| var options = this.options |
| var opts = {} |
| var res = this.res |
| var req = this.req |
| var ranges = req.headers.range |
| var offset = options.start || 0 |
| |
| if (headersSent(res)) { |
| // impossible to send now |
| this.headersAlreadySent() |
| return |
| } |
| |
| debug('pipe "%s"', path) |
| |
| // set header fields |
| this.setHeader(path, stat) |
| |
| // set content-type |
| this.type(path) |
| |
| // conditional GET support |
| if (this.isConditionalGET()) { |
| if (this.isPreconditionFailure()) { |
| this.error(412) |
| return |
| } |
| |
| if (this.isCachable() && this.isFresh()) { |
| this.notModified() |
| return |
| } |
| } |
| |
| // 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 (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) { |
| // parse |
| ranges = parseRange(len, ranges, { |
| combine: true |
| }) |
| |
| // If-Range support |
| if (!this.isRangeFresh()) { |
| debug('range stale') |
| ranges = -2 |
| } |
| |
| // unsatisfiable |
| if (ranges === -1) { |
| debug('range unsatisfiable') |
| |
| // Content-Range |
| res.setHeader('Content-Range', contentRange('bytes', len)) |
| |
| // 416 Requested Range Not Satisfiable |
| return this.error(416, { |
| headers: {'Content-Range': res.getHeader('Content-Range')} |
| }) |
| } |
| |
| // valid (syntactically invalid/multiple ranges are treated as a regular response) |
| if (ranges !== -2 && ranges.length === 1) { |
| debug('range %j', ranges) |
| |
| // Content-Range |
| res.statusCode = 206 |
| res.setHeader('Content-Range', contentRange('bytes', len, ranges[0])) |
| |
| // adjust for requested range |
| offset += ranges[0].start |
| len = ranges[0].end - ranges[0].start + 1 |
| } |
| } |
| |
| // clone options |
| for (var prop in options) { |
| opts[prop] = options[prop] |
| } |
| |
| // set read options |
| opts.start = offset |
| opts.end = Math.max(offset, offset + len - 1) |
| |
| // content-length |
| res.setHeader('Content-Length', len) |
| |
| // HEAD support |
| if (req.method === 'HEAD') { |
| res.end() |
| return |
| } |
| |
| this.stream(path, opts) |
| } |
| |
| /** |
| * Transfer file for `path`. |
| * |
| * @param {String} path |
| * @api private |
| */ |
| SendStream.prototype.sendFile = function sendFile (path) { |
| var i = 0 |
| var self = this |
| |
| debug('stat "%s"', path) |
| fs.stat(path, function onstat (err, stat) { |
| if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) { |
| // not found, check extensions |
| return next(err) |
| } |
| if (err) return self.onStatError(err) |
| if (stat.isDirectory()) return self.redirect(path) |
| self.emit('file', path, stat) |
| self.send(path, stat) |
| }) |
| |
| function next (err) { |
| if (self._extensions.length <= i) { |
| return err |
| ? self.onStatError(err) |
| : self.error(404) |
| } |
| |
| var p = path + '.' + self._extensions[i++] |
| |
| debug('stat "%s"', p) |
| fs.stat(p, function (err, stat) { |
| if (err) return next(err) |
| if (stat.isDirectory()) return next() |
| self.emit('file', p, stat) |
| self.send(p, stat) |
| }) |
| } |
| } |
| |
| /** |
| * Transfer index for `path`. |
| * |
| * @param {String} path |
| * @api private |
| */ |
| SendStream.prototype.sendIndex = function sendIndex (path) { |
| var i = -1 |
| var self = this |
| |
| function next (err) { |
| if (++i >= self._index.length) { |
| if (err) return self.onStatError(err) |
| return self.error(404) |
| } |
| |
| var p = join(path, self._index[i]) |
| |
| debug('stat "%s"', p) |
| fs.stat(p, function (err, stat) { |
| if (err) return next(err) |
| if (stat.isDirectory()) return next() |
| self.emit('file', p, stat) |
| self.send(p, stat) |
| }) |
| } |
| |
| next() |
| } |
| |
| /** |
| * Stream `path` to the response. |
| * |
| * @param {String} path |
| * @param {Object} options |
| * @api private |
| */ |
| |
| SendStream.prototype.stream = function stream (path, options) { |
| // TODO: this is all lame, refactor meeee |
| var finished = false |
| var self = this |
| var res = this.res |
| |
| // pipe |
| var stream = fs.createReadStream(path, options) |
| this.emit('stream', stream) |
| stream.pipe(res) |
| |
| // response finished, done with the fd |
| onFinished(res, function onfinished () { |
| finished = true |
| destroy(stream) |
| }) |
| |
| // error handling code-smell |
| stream.on('error', function onerror (err) { |
| // request already finished |
| if (finished) return |
| |
| // clean up stream |
| finished = true |
| destroy(stream) |
| |
| // error |
| self.onStatError(err) |
| }) |
| |
| // end |
| stream.on('end', function onend () { |
| 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 type (path) { |
| var res = this.res |
| |
| if (res.getHeader('Content-Type')) return |
| |
| var type = mime.lookup(path) |
| |
| if (!type) { |
| debug('no content-type') |
| return |
| } |
| |
| var charset = mime.charsets.lookup(type) |
| |
| debug('content-type %s', type) |
| res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')) |
| } |
| |
| /** |
| * Set response header fields, most |
| * fields may be pre-defined. |
| * |
| * @param {String} path |
| * @param {Object} stat |
| * @api private |
| */ |
| |
| SendStream.prototype.setHeader = function setHeader (path, stat) { |
| var res = this.res |
| |
| this.emit('headers', res, path, stat) |
| |
| if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { |
| debug('accept ranges') |
| res.setHeader('Accept-Ranges', 'bytes') |
| } |
| |
| if (this._cacheControl && !res.getHeader('Cache-Control')) { |
| var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) |
| |
| if (this._immutable) { |
| cacheControl += ', immutable' |
| } |
| |
| debug('cache-control %s', cacheControl) |
| res.setHeader('Cache-Control', cacheControl) |
| } |
| |
| if (this._lastModified && !res.getHeader('Last-Modified')) { |
| var modified = stat.mtime.toUTCString() |
| debug('modified %s', modified) |
| res.setHeader('Last-Modified', modified) |
| } |
| |
| if (this._etag && !res.getHeader('ETag')) { |
| var val = etag(stat) |
| debug('etag %s', val) |
| res.setHeader('ETag', val) |
| } |
| } |
| |
| /** |
| * Clear all headers from a response. |
| * |
| * @param {object} res |
| * @private |
| */ |
| |
| function clearHeaders (res) { |
| var headers = getHeaderNames(res) |
| |
| for (var i = 0; i < headers.length; i++) { |
| res.removeHeader(headers[i]) |
| } |
| } |
| |
| /** |
| * Collapse all leading slashes into a single slash |
| * |
| * @param {string} str |
| * @private |
| */ |
| function collapseLeadingSlashes (str) { |
| for (var i = 0; i < str.length; i++) { |
| if (str[i] !== '/') { |
| break |
| } |
| } |
| |
| return i > 1 |
| ? '/' + str.substr(i) |
| : str |
| } |
| |
| /** |
| * Determine if path parts contain a dotfile. |
| * |
| * @api private |
| */ |
| |
| function containsDotFile (parts) { |
| for (var i = 0; i < parts.length; i++) { |
| var part = parts[i] |
| if (part.length > 1 && part[0] === '.') { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * Create a Content-Range header. |
| * |
| * @param {string} type |
| * @param {number} size |
| * @param {array} [range] |
| */ |
| |
| function contentRange (type, size, range) { |
| return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size |
| } |
| |
| /** |
| * Create a minimal HTML document. |
| * |
| * @param {string} title |
| * @param {string} body |
| * @private |
| */ |
| |
| function createHtmlDocument (title, body) { |
| return '<!DOCTYPE html>\n' + |
| '<html lang="en">\n' + |
| '<head>\n' + |
| '<meta charset="utf-8">\n' + |
| '<title>' + title + '</title>\n' + |
| '</head>\n' + |
| '<body>\n' + |
| '<pre>' + body + '</pre>\n' + |
| '</body>\n' + |
| '<html>\n' |
| } |
| |
| /** |
| * decodeURIComponent. |
| * |
| * Allows V8 to only deoptimize this fn instead of all |
| * of send(). |
| * |
| * @param {String} path |
| * @api private |
| */ |
| |
| function decode (path) { |
| try { |
| return decodeURIComponent(path) |
| } catch (err) { |
| return -1 |
| } |
| } |
| |
| /** |
| * Get the header names on a respnse. |
| * |
| * @param {object} res |
| * @returns {array[string]} |
| * @private |
| */ |
| |
| function getHeaderNames (res) { |
| return typeof res.getHeaderNames !== 'function' |
| ? Object.keys(res._headers || {}) |
| : res.getHeaderNames() |
| } |
| |
| /** |
| * Determine if emitter has listeners of a given type. |
| * |
| * The way to do this check is done three different ways in Node.js >= 0.8 |
| * so this consolidates them into a minimal set using instance methods. |
| * |
| * @param {EventEmitter} emitter |
| * @param {string} type |
| * @returns {boolean} |
| * @private |
| */ |
| |
| function hasListeners (emitter, type) { |
| var count = typeof emitter.listenerCount !== 'function' |
| ? emitter.listeners(type).length |
| : emitter.listenerCount(type) |
| |
| return count > 0 |
| } |
| |
| /** |
| * Determine if the response headers have been sent. |
| * |
| * @param {object} res |
| * @returns {boolean} |
| * @private |
| */ |
| |
| function headersSent (res) { |
| return typeof res.headersSent !== 'boolean' |
| ? Boolean(res._header) |
| : res.headersSent |
| } |
| |
| /** |
| * Normalize the index option into an array. |
| * |
| * @param {boolean|string|array} val |
| * @param {string} name |
| * @private |
| */ |
| |
| function normalizeList (val, name) { |
| var list = [].concat(val || []) |
| |
| for (var i = 0; i < list.length; i++) { |
| if (typeof list[i] !== 'string') { |
| throw new TypeError(name + ' must be array of strings or false') |
| } |
| } |
| |
| return list |
| } |
| |
| /** |
| * Parse an HTTP Date into a number. |
| * |
| * @param {string} date |
| * @private |
| */ |
| |
| function parseHttpDate (date) { |
| var timestamp = date && Date.parse(date) |
| |
| return typeof timestamp === 'number' |
| ? timestamp |
| : NaN |
| } |
| |
| /** |
| * Parse a HTTP token list. |
| * |
| * @param {string} str |
| * @private |
| */ |
| |
| function parseTokenList (str) { |
| var end = 0 |
| var list = [] |
| var start = 0 |
| |
| // gather tokens |
| for (var i = 0, len = str.length; i < len; i++) { |
| switch (str.charCodeAt(i)) { |
| case 0x20: /* */ |
| if (start === end) { |
| start = end = i + 1 |
| } |
| break |
| case 0x2c: /* , */ |
| list.push(str.substring(start, end)) |
| start = end = i + 1 |
| break |
| default: |
| end = i + 1 |
| break |
| } |
| } |
| |
| // final token |
| list.push(str.substring(start, end)) |
| |
| return list |
| } |
| |
| /** |
| * Set an object of headers on a response. |
| * |
| * @param {object} res |
| * @param {object} headers |
| * @private |
| */ |
| |
| function setHeaders (res, headers) { |
| var keys = Object.keys(headers) |
| |
| for (var i = 0; i < keys.length; i++) { |
| var key = keys[i] |
| res.setHeader(key, headers[key]) |
| } |
| } |