diff options
| author | ruki <waruqi@gmail.com> | 2018-11-08 00:38:48 +0800 |
|---|---|---|
| committer | ruki <waruqi@gmail.com> | 2018-11-07 21:53:09 +0800 |
| commit | 26105034da4fcce7ac883c899d781f016559310d (patch) | |
| tree | c459a5dc4e3aa0972d9919033ece511ce76dd129 /node_modules/koa/lib | |
| parent | 2c77f00f1a7ecb6c8192f9c16d3b2001b254a107 (diff) | |
| download | xmake-docs-26105034da4fcce7ac883c899d781f016559310d.tar.gz xmake-docs-26105034da4fcce7ac883c899d781f016559310d.zip | |
switch to vuepress
Diffstat (limited to 'node_modules/koa/lib')
| -rw-r--r-- | node_modules/koa/lib/application.js | 244 | ||||
| -rw-r--r-- | node_modules/koa/lib/context.js | 241 | ||||
| -rw-r--r-- | node_modules/koa/lib/request.js | 727 | ||||
| -rw-r--r-- | node_modules/koa/lib/response.js | 558 |
4 files changed, 1770 insertions, 0 deletions
diff --git a/node_modules/koa/lib/application.js b/node_modules/koa/lib/application.js new file mode 100644 index 00000000..84107cb6 --- /dev/null +++ b/node_modules/koa/lib/application.js @@ -0,0 +1,244 @@ + +'use strict'; + +/** + * Module dependencies. + */ + +const isGeneratorFunction = require('is-generator-function'); +const debug = require('debug')('koa:application'); +const onFinished = require('on-finished'); +const response = require('./response'); +const compose = require('koa-compose'); +const isJSON = require('koa-is-json'); +const context = require('./context'); +const request = require('./request'); +const statuses = require('statuses'); +const Emitter = require('events'); +const util = require('util'); +const Stream = require('stream'); +const http = require('http'); +const only = require('only'); +const convert = require('koa-convert'); +const deprecate = require('depd')('koa'); + +/** + * Expose `Application` class. + * Inherits from `Emitter.prototype`. + */ + +module.exports = class Application extends Emitter { + /** + * Initialize a new `Application`. + * + * @api public + */ + + constructor() { + super(); + + this.proxy = false; + this.middleware = []; + this.subdomainOffset = 2; + this.env = process.env.NODE_ENV || 'development'; + this.context = Object.create(context); + this.request = Object.create(request); + this.response = Object.create(response); + if (util.inspect.custom) { + this[util.inspect.custom] = this.inspect; + } + } + + /** + * Shorthand for: + * + * http.createServer(app.callback()).listen(...) + * + * @param {Mixed} ... + * @return {Server} + * @api public + */ + + listen(...args) { + debug('listen'); + const server = http.createServer(this.callback()); + return server.listen(...args); + } + + /** + * Return JSON representation. + * We only bother showing settings. + * + * @return {Object} + * @api public + */ + + toJSON() { + return only(this, [ + 'subdomainOffset', + 'proxy', + 'env' + ]); + } + + /** + * Inspect implementation. + * + * @return {Object} + * @api public + */ + + inspect() { + return this.toJSON(); + } + + /** + * Use the given middleware `fn`. + * + * Old-style middleware will be converted. + * + * @param {Function} fn + * @return {Application} self + * @api public + */ + + use(fn) { + if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); + if (isGeneratorFunction(fn)) { + deprecate('Support for generators will be removed in v3. ' + + 'See the documentation for examples of how to convert old middleware ' + + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); + fn = convert(fn); + } + debug('use %s', fn._name || fn.name || '-'); + this.middleware.push(fn); + return this; + } + + /** + * Return a request handler callback + * for node's native http server. + * + * @return {Function} + * @api public + */ + + callback() { + const fn = compose(this.middleware); + + if (!this.listenerCount('error')) this.on('error', this.onerror); + + const handleRequest = (req, res) => { + const ctx = this.createContext(req, res); + return this.handleRequest(ctx, fn); + }; + + return handleRequest; + } + + /** + * Handle request in callback. + * + * @api private + */ + + handleRequest(ctx, fnMiddleware) { + const res = ctx.res; + res.statusCode = 404; + const onerror = err => ctx.onerror(err); + const handleResponse = () => respond(ctx); + onFinished(res, onerror); + return fnMiddleware(ctx).then(handleResponse).catch(onerror); + } + + /** + * Initialize a new context. + * + * @api private + */ + + createContext(req, res) { + const context = Object.create(this.context); + const request = context.request = Object.create(this.request); + const response = context.response = Object.create(this.response); + context.app = request.app = response.app = this; + context.req = request.req = response.req = req; + context.res = request.res = response.res = res; + request.ctx = response.ctx = context; + request.response = response; + response.request = request; + context.originalUrl = request.originalUrl = req.url; + context.state = {}; + return context; + } + + /** + * Default error handler. + * + * @param {Error} err + * @api private + */ + + onerror(err) { + if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)); + + if (404 == err.status || err.expose) return; + if (this.silent) return; + + const msg = err.stack || err.toString(); + console.error(); + console.error(msg.replace(/^/gm, ' ')); + console.error(); + } +}; + +/** + * Response helper. + */ + +function respond(ctx) { + // allow bypassing koa + if (false === ctx.respond) return; + + const res = ctx.res; + if (!ctx.writable) return; + + let body = ctx.body; + const code = ctx.status; + + // ignore body + if (statuses.empty[code]) { + // strip headers + ctx.body = null; + return res.end(); + } + + if ('HEAD' == ctx.method) { + if (!res.headersSent && isJSON(body)) { + ctx.length = Buffer.byteLength(JSON.stringify(body)); + } + return res.end(); + } + + // status body + if (null == body) { + body = ctx.message || String(code); + if (!res.headersSent) { + ctx.type = 'text'; + ctx.length = Buffer.byteLength(body); + } + return res.end(body); + } + + // responses + if (Buffer.isBuffer(body)) return res.end(body); + if ('string' == typeof body) return res.end(body); + if (body instanceof Stream) return body.pipe(res); + + // body: json + body = JSON.stringify(body); + if (!res.headersSent) { + ctx.length = Buffer.byteLength(body); + } + res.end(body); +} diff --git a/node_modules/koa/lib/context.js b/node_modules/koa/lib/context.js new file mode 100644 index 00000000..1b92f018 --- /dev/null +++ b/node_modules/koa/lib/context.js @@ -0,0 +1,241 @@ + +'use strict'; + +/** + * Module dependencies. + */ + +const util = require('util'); +const createError = require('http-errors'); +const httpAssert = require('http-assert'); +const delegate = require('delegates'); +const statuses = require('statuses'); +const Cookies = require('cookies'); + +const COOKIES = Symbol('context#cookies'); + +/** + * Context prototype. + */ + +const proto = module.exports = { + + /** + * util.inspect() implementation, which + * just returns the JSON output. + * + * @return {Object} + * @api public + */ + + inspect() { + if (this === proto) return this; + return this.toJSON(); + }, + + /** + * Return JSON representation. + * + * Here we explicitly invoke .toJSON() on each + * object, as iteration will otherwise fail due + * to the getters and cause utilities such as + * clone() to fail. + * + * @return {Object} + * @api public + */ + + toJSON() { + return { + request: this.request.toJSON(), + response: this.response.toJSON(), + app: this.app.toJSON(), + originalUrl: this.originalUrl, + req: '<original node req>', + res: '<original node res>', + socket: '<original node socket>' + }; + }, + + /** + * Similar to .throw(), adds assertion. + * + * this.assert(this.user, 401, 'Please login!'); + * + * See: https://github.com/jshttp/http-assert + * + * @param {Mixed} test + * @param {Number} status + * @param {String} message + * @api public + */ + + assert: httpAssert, + + /** + * Throw an error with `msg` and optional `status` + * defaulting to 500. Note that these are user-level + * errors, and the message may be exposed to the client. + * + * this.throw(403) + * this.throw('name required', 400) + * this.throw(400, 'name required') + * this.throw('something exploded') + * this.throw(new Error('invalid'), 400); + * this.throw(400, new Error('invalid')); + * + * See: https://github.com/jshttp/http-errors + * + * @param {String|Number|Error} err, msg or status + * @param {String|Number|Error} [err, msg or status] + * @param {Object} [props] + * @api public + */ + + throw(...args) { + throw createError(...args); + }, + + /** + * Default error handling. + * + * @param {Error} err + * @api private + */ + + onerror(err) { + // don't do anything if there is no error. + // this allows you to pass `this.onerror` + // to node-style callbacks. + if (null == err) return; + + if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err)); + + let headerSent = false; + if (this.headerSent || !this.writable) { + headerSent = err.headerSent = true; + } + + // delegate + this.app.emit('error', err, this); + + // nothing we can do here other + // than delegate to the app-level + // handler and log. + if (headerSent) { + return; + } + + const { res } = this; + + // first unset all headers + /* istanbul ignore else */ + if (typeof res.getHeaderNames === 'function') { + res.getHeaderNames().forEach(name => res.removeHeader(name)); + } else { + res._headers = {}; // Node < 7.7 + } + + // then set those specified + this.set(err.headers); + + // force text/plain + this.type = 'text'; + + // ENOENT support + if ('ENOENT' == err.code) err.status = 404; + + // default to 500 + if ('number' != typeof err.status || !statuses[err.status]) err.status = 500; + + // respond + const code = statuses[err.status]; + const msg = err.expose ? err.message : code; + this.status = err.status; + this.length = Buffer.byteLength(msg); + this.res.end(msg); + }, + + get cookies() { + if (!this[COOKIES]) { + this[COOKIES] = new Cookies(this.req, this.res, { + keys: this.app.keys, + secure: this.request.secure + }); + } + return this[COOKIES]; + }, + + set cookies(_cookies) { + this[COOKIES] = _cookies; + } +}; + +/** + * Custom inspection implementation for newer Node.js versions. + * + * @return {Object} + * @api public + */ + +/* istanbul ignore else */ +if (util.inspect.custom) { + module.exports[util.inspect.custom] = module.exports.inspect; +} + +/** + * Response delegation. + */ + +delegate(proto, 'response') + .method('attachment') + .method('redirect') + .method('remove') + .method('vary') + .method('set') + .method('append') + .method('flushHeaders') + .access('status') + .access('message') + .access('body') + .access('length') + .access('type') + .access('lastModified') + .access('etag') + .getter('headerSent') + .getter('writable'); + +/** + * Request delegation. + */ + +delegate(proto, 'request') + .method('acceptsLanguages') + .method('acceptsEncodings') + .method('acceptsCharsets') + .method('accepts') + .method('get') + .method('is') + .access('querystring') + .access('idempotent') + .access('socket') + .access('search') + .access('method') + .access('query') + .access('path') + .access('url') + .access('accept') + .getter('origin') + .getter('href') + .getter('subdomains') + .getter('protocol') + .getter('host') + .getter('hostname') + .getter('URL') + .getter('header') + .getter('headers') + .getter('secure') + .getter('stale') + .getter('fresh') + .getter('ips') + .getter('ip'); diff --git a/node_modules/koa/lib/request.js b/node_modules/koa/lib/request.js new file mode 100644 index 00000000..0b0b2625 --- /dev/null +++ b/node_modules/koa/lib/request.js @@ -0,0 +1,727 @@ + +'use strict'; + +/** + * Module dependencies. + */ + +const URL = require('url').URL; +const net = require('net'); +const accepts = require('accepts'); +const contentType = require('content-type'); +const stringify = require('url').format; +const parse = require('parseurl'); +const qs = require('querystring'); +const typeis = require('type-is'); +const fresh = require('fresh'); +const only = require('only'); +const util = require('util'); + +const IP = Symbol('context#ip'); + +/** + * Prototype. + */ + +module.exports = { + + /** + * Return request header. + * + * @return {Object} + * @api public + */ + + get header() { + return this.req.headers; + }, + + /** + * Set request header. + * + * @api public + */ + + set header(val) { + this.req.headers = val; + }, + + /** + * Return request header, alias as request.header + * + * @return {Object} + * @api public + */ + + get headers() { + return this.req.headers; + }, + + /** + * Set request header, alias as request.header + * + * @api public + */ + + set headers(val) { + this.req.headers = val; + }, + + /** + * Get request URL. + * + * @return {String} + * @api public + */ + + get url() { + return this.req.url; + }, + + /** + * Set request URL. + * + * @api public + */ + + set url(val) { + this.req.url = val; + }, + + /** + * Get origin of URL. + * + * @return {String} + * @api public + */ + + get origin() { + return `${this.protocol}://${this.host}`; + }, + + /** + * Get full request URL. + * + * @return {String} + * @api public + */ + + get href() { + // support: `GET http://example.com/foo` + if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl; + return this.origin + this.originalUrl; + }, + + /** + * Get request method. + * + * @return {String} + * @api public + */ + + get method() { + return this.req.method; + }, + + /** + * Set request method. + * + * @param {String} val + * @api public + */ + + set method(val) { + this.req.method = val; + }, + + /** + * Get request pathname. + * + * @return {String} + * @api public + */ + + get path() { + return parse(this.req).pathname; + }, + + /** + * Set pathname, retaining the query-string when present. + * + * @param {String} path + * @api public + */ + + set path(path) { + const url = parse(this.req); + if (url.pathname === path) return; + + url.pathname = path; + url.path = null; + + this.url = stringify(url); + }, + + /** + * Get parsed query-string. + * + * @return {Object} + * @api public + */ + + get query() { + const str = this.querystring; + const c = this._querycache = this._querycache || {}; + return c[str] || (c[str] = qs.parse(str)); + }, + + /** + * Set query-string as an object. + * + * @param {Object} obj + * @api public + */ + + set query(obj) { + this.querystring = qs.stringify(obj); + }, + + /** + * Get query string. + * + * @return {String} + * @api public + */ + + get querystring() { + if (!this.req) return ''; + return parse(this.req).query || ''; + }, + + /** + * Set querystring. + * + * @param {String} str + * @api public + */ + + set querystring(str) { + const url = parse(this.req); + if (url.search === `?${str}`) return; + + url.search = str; + url.path = null; + + this.url = stringify(url); + }, + + /** + * Get the search string. Same as the querystring + * except it includes the leading ?. + * + * @return {String} + * @api public + */ + + get search() { + if (!this.querystring) return ''; + return `?${this.querystring}`; + }, + + /** + * Set the search string. Same as + * request.querystring= but included for ubiquity. + * + * @param {String} str + * @api public + */ + + set search(str) { + this.querystring = str; + }, + + /** + * Parse the "Host" header field host + * and support X-Forwarded-Host when a + * proxy is enabled. + * + * @return {String} hostname:port + * @api public + */ + + get host() { + const proxy = this.app.proxy; + let host = proxy && this.get('X-Forwarded-Host'); + if (!host) { + if (this.req.httpVersionMajor >= 2) host = this.get(':authority'); + if (!host) host = this.get('Host'); + } + if (!host) return ''; + return host.split(/\s*,\s*/)[0]; + }, + + /** + * Parse the "Host" header field hostname + * and support X-Forwarded-Host when a + * proxy is enabled. + * + * @return {String} hostname + * @api public + */ + + get hostname() { + const host = this.host; + if (!host) return ''; + if ('[' == host[0]) return this.URL.hostname || ''; // IPv6 + return host.split(':')[0]; + }, + + /** + * Get WHATWG parsed URL. + * Lazily memoized. + * + * @return {URL|Object} + * @api public + */ + + get URL() { + /* istanbul ignore else */ + if (!this.memoizedURL) { + const protocol = this.protocol; + const host = this.host; + const originalUrl = this.originalUrl || ''; // avoid undefined in template string + try { + this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`); + } catch (err) { + this.memoizedURL = Object.create(null); + } + } + return this.memoizedURL; + }, + + /** + * Check if the request is fresh, aka + * Last-Modified and/or the ETag + * still match. + * + * @return {Boolean} + * @api public + */ + + get fresh() { + const method = this.method; + const s = this.ctx.status; + + // GET or HEAD for weak freshness validation only + if ('GET' != method && 'HEAD' != method) return false; + + // 2xx or 304 as per rfc2616 14.26 + if ((s >= 200 && s < 300) || 304 == s) { + return fresh(this.header, this.response.header); + } + + return false; + }, + + /** + * Check if the request is stale, aka + * "Last-Modified" and / or the "ETag" for the + * resource has changed. + * + * @return {Boolean} + * @api public + */ + + get stale() { + return !this.fresh; + }, + + /** + * Check if the request is idempotent. + * + * @return {Boolean} + * @api public + */ + + get idempotent() { + const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']; + return !!~methods.indexOf(this.method); + }, + + /** + * Return the request socket. + * + * @return {Connection} + * @api public + */ + + get socket() { + return this.req.socket; + }, + + /** + * Get the charset when present or undefined. + * + * @return {String} + * @api public + */ + + get charset() { + let type = this.get('Content-Type'); + if (!type) return ''; + + try { + type = contentType.parse(type); + } catch (e) { + return ''; + } + + return type.parameters.charset || ''; + }, + + /** + * Return parsed Content-Length when present. + * + * @return {Number} + * @api public + */ + + get length() { + const len = this.get('Content-Length'); + if (len == '') return; + return ~~len; + }, + + /** + * Return the protocol string "http" or "https" + * when requested with TLS. When the proxy setting + * is enabled the "X-Forwarded-Proto" header + * field will be trusted. If you're running behind + * a reverse proxy that supplies https for you this + * may be enabled. + * + * @return {String} + * @api public + */ + + get protocol() { + if (this.socket.encrypted) return 'https'; + if (!this.app.proxy) return 'http'; + const proto = this.get('X-Forwarded-Proto'); + return proto ? proto.split(/\s*,\s*/)[0] : 'http'; + }, + + /** + * Short-hand for: + * + * this.protocol == 'https' + * + * @return {Boolean} + * @api public + */ + + get secure() { + return 'https' == this.protocol; + }, + + /** + * When `app.proxy` is `true`, parse + * the "X-Forwarded-For" ip address list. + * + * For example if the value were "client, proxy1, proxy2" + * you would receive the array `["client", "proxy1", "proxy2"]` + * where "proxy2" is the furthest down-stream. + * + * @return {Array} + * @api public + */ + + get ips() { + const proxy = this.app.proxy; + const val = this.get('X-Forwarded-For'); + return proxy && val + ? val.split(/\s*,\s*/) + : []; + }, + + /** + * Return request's remote address + * When `app.proxy` is `true`, parse + * the "X-Forwarded-For" ip address list and return the first one + * + * @return {String} + * @api public + */ + + get ip() { + if (!this[IP]) { + this[IP] = this.ips[0] || this.socket.remoteAddress || ''; + } + return this[IP]; + }, + + set ip(_ip) { + this[IP] = _ip; + }, + + /** + * Return subdomains as an array. + * + * Subdomains are the dot-separated parts of the host before the main domain + * of the app. By default, the domain of the app is assumed to be the last two + * parts of the host. This can be changed by setting `app.subdomainOffset`. + * + * For example, if the domain is "tobi.ferrets.example.com": + * If `app.subdomainOffset` is not set, this.subdomains is + * `["ferrets", "tobi"]`. + * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`. + * + * @return {Array} + * @api public + */ + + get subdomains() { + const offset = this.app.subdomainOffset; + const hostname = this.hostname; + if (net.isIP(hostname)) return []; + return hostname + .split('.') + .reverse() + .slice(offset); + }, + + /** + * Get accept object. + * Lazily memoized. + * + * @return {Object} + * @api private + */ + get accept() { + return this._accept || (this._accept = accepts(this.req)); + }, + + /** + * Set accept object. + * + * @param {Object} + * @api private + */ + set accept(obj) { + return this._accept = obj; + }, + + /** + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `false`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single mime type string + * such as "application/json", the extension name + * such as "json" or an array `["json", "html", "text/plain"]`. When a list + * or array is given the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * this.accepts('html'); + * // => "html" + * + * // Accept: text/*, application/json + * this.accepts('html'); + * // => "html" + * this.accepts('text/html'); + * // => "text/html" + * this.accepts('json', 'text'); + * // => "json" + * this.accepts('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * this.accepts('image/png'); + * this.accepts('png'); + * // => false + * + * // Accept: text/*;q=.5, application/json + * this.accepts(['html', 'json']); + * this.accepts('html', 'json'); + * // => "json" + * + * @param {String|Array} type(s)... + * @return {String|Array|false} + * @api public + */ + + accepts(...args) { + return this.accept.types(...args); + }, + + /** + * Return accepted encodings or best fit based on `encodings`. + * + * Given `Accept-Encoding: gzip, deflate` + * an array sorted by quality is returned: + * + * ['gzip', 'deflate'] + * + * @param {String|Array} encoding(s)... + * @return {String|Array} + * @api public + */ + + acceptsEncodings(...args) { + return this.accept.encodings(...args); + }, + + /** + * Return accepted charsets or best fit based on `charsets`. + * + * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` + * an array sorted by quality is returned: + * + * ['utf-8', 'utf-7', 'iso-8859-1'] + * + * @param {String|Array} charset(s)... + * @return {String|Array} + * @api public + */ + + acceptsCharsets(...args) { + return this.accept.charsets(...args); + }, + + /** + * Return accepted languages or best fit based on `langs`. + * + * Given `Accept-Language: en;q=0.8, es, pt` + * an array sorted by quality is returned: + * + * ['es', 'pt', 'en'] + * + * @param {String|Array} lang(s)... + * @return {Array|String} + * @api public + */ + + acceptsLanguages(...args) { + return this.accept.languages(...args); + }, + + /** + * Check if the incoming request contains the "Content-Type" + * header field, and it contains any of the give mime `type`s. + * If there is no request body, `null` is returned. + * If there is no content type, `false` is returned. + * Otherwise, it returns the first `type` that matches. + * + * Examples: + * + * // With Content-Type: text/html; charset=utf-8 + * this.is('html'); // => 'html' + * this.is('text/html'); // => 'text/html' + * this.is('text/*', 'application/json'); // => 'text/html' + * + * // When Content-Type is application/json + * this.is('json', 'urlencoded'); // => 'json' + * this.is('application/json'); // => 'application/json' + * this.is('html', 'application/*'); // => 'application/json' + * + * this.is('html'); // => false + * + * @param {String|Array} types... + * @return {String|false|null} + * @api public + */ + + is(types) { + if (!types) return typeis(this.req); + if (!Array.isArray(types)) types = [].slice.call(arguments); + return typeis(this.req, types); + }, + + /** + * Return the request mime type void of + * parameters such as "charset". + * + * @return {String} + * @api public + */ + + get type() { + const type = this.get('Content-Type'); + if (!type) return ''; + return type.split(';')[0]; + }, + + /** + * Return request header. + * + * The `Referrer` header field is special-cased, + * both `Referrer` and `Referer` are interchangeable. + * + * Examples: + * + * this.get('Content-Type'); + * // => "text/plain" + * + * this.get('content-type'); + * // => "text/plain" + * + * this.get('Something'); + * // => '' + * + * @param {String} field + * @return {String} + * @api public + */ + + get(field) { + const req = this.req; + switch (field = field.toLowerCase()) { + case 'referer': + case 'referrer': + return req.headers.referrer || req.headers.referer || ''; + default: + return req.headers[field] || ''; + } + }, + + /** + * Inspect implementation. + * + * @return {Object} + * @api public + */ + + inspect() { + if (!this.req) return; + return this.toJSON(); + }, + + /** + * Return JSON representation. + * + * @return {Object} + * @api public + */ + + toJSON() { + return only(this, [ + 'method', + 'url', + 'header' + ]); + } +}; + +/** + * Custom inspection implementation for newer Node.js versions. + * + * @return {Object} + * @api public + */ + +/* istanbul ignore else */ +if (util.inspect.custom) { + module.exports[util.inspect.custom] = module.exports.inspect; +} diff --git a/node_modules/koa/lib/response.js b/node_modules/koa/lib/response.js new file mode 100644 index 00000000..371875fe --- /dev/null +++ b/node_modules/koa/lib/response.js @@ -0,0 +1,558 @@ + +'use strict'; + +/** + * Module dependencies. + */ + +const contentDisposition = require('content-disposition'); +const ensureErrorHandler = require('error-inject'); +const getType = require('cache-content-type'); +const onFinish = require('on-finished'); +const isJSON = require('koa-is-json'); +const escape = require('escape-html'); +const typeis = require('type-is').is; +const statuses = require('statuses'); +const destroy = require('destroy'); +const assert = require('assert'); +const extname = require('path').extname; +const vary = require('vary'); +const only = require('only'); +const util = require('util'); + +/** + * Prototype. + */ + +module.exports = { + + /** + * Return the request socket. + * + * @return {Connection} + * @api public + */ + + get socket() { + return this.res.socket; + }, + + /** + * Return response header. + * + * @return {Object} + * @api public + */ + + get header() { + const { res } = this; + return typeof res.getHeaders === 'function' + ? res.getHeaders() + : res._headers || {}; // Node < 7.7 + }, + + /** + * Return response header, alias as response.header + * + * @return {Object} + * @api public + */ + + get headers() { + return this.header; + }, + + /** + * Get response status code. + * + * @return {Number} + * @api public + */ + + get status() { + return this.res.statusCode; + }, + + /** + * Set response status code. + * + * @param {Number} code + * @api public + */ + + set status(code) { + if (this.headerSent) return; + + assert('number' == typeof code, 'status code must be a number'); + assert(statuses[code], `invalid status code: ${code}`); + this._explicitStatus = true; + this.res.statusCode = code; + if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]; + if (this.body && statuses.empty[code]) this.body = null; + }, + + /** + * Get response status message + * + * @return {String} + * @api public + */ + + get message() { + return this.res.statusMessage || statuses[this.status]; + }, + + /** + * Set response status message + * + * @param {String} msg + * @api public + */ + + set message(msg) { + this.res.statusMessage = msg; + }, + + /** + * Get response body. + * + * @return {Mixed} + * @api public + */ + + get body() { + return this._body; + }, + + /** + * Set response body. + * + * @param {String|Buffer|Object|Stream} val + * @api public + */ + + set body(val) { + const original = this._body; + this._body = val; + + // no content + if (null == val) { + if (!statuses.empty[this.status]) this.status = 204; + this.remove('Content-Type'); + this.remove('Content-Length'); + this.remove('Transfer-Encoding'); + return; + } + + // set the status + if (!this._explicitStatus) this.status = 200; + + // set the content-type only if not yet set + const setType = !this.header['content-type']; + + // string + if ('string' == typeof val) { + if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'; + this.length = Buffer.byteLength(val); + return; + } + + // buffer + if (Buffer.isBuffer(val)) { + if (setType) this.type = 'bin'; + this.length = val.length; + return; + } + + // stream + if ('function' == typeof val.pipe) { + onFinish(this.res, destroy.bind(null, val)); + ensureErrorHandler(val, err => this.ctx.onerror(err)); + + // overwriting + if (null != original && original != val) this.remove('Content-Length'); + + if (setType) this.type = 'bin'; + return; + } + + // json + this.remove('Content-Length'); + this.type = 'json'; + }, + + /** + * Set Content-Length field to `n`. + * + * @param {Number} n + * @api public + */ + + set length(n) { + this.set('Content-Length', n); + }, + + /** + * Return parsed response Content-Length when present. + * + * @return {Number} + * @api public + */ + + get length() { + const len = this.header['content-length']; + const body = this.body; + + if (null == len) { + if (!body) return; + if ('string' == typeof body) return Buffer.byteLength(body); + if (Buffer.isBuffer(body)) return body.length; + if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body)); + return; + } + + return ~~len; + }, + + /** + * Check if a header has been written to the socket. + * + * @return {Boolean} + * @api public + */ + + get headerSent() { + return this.res.headersSent; + }, + + /** + * Vary on `field`. + * + * @param {String} field + * @api public + */ + + vary(field) { + if (this.headerSent) return; + + vary(this.res, field); + }, + + /** + * Perform a 302 redirect to `url`. + * + * The string "back" is special-cased + * to provide Referrer support, when Referrer + * is not present `alt` or "/" is used. + * + * Examples: + * + * this.redirect('back'); + * this.redirect('back', '/index.html'); + * this.redirect('/login'); + * this.redirect('http://google.com'); + * + * @param {String} url + * @param {String} [alt] + * @api public + */ + + redirect(url, alt) { + // location + if ('back' == url) url = this.ctx.get('Referrer') || alt || '/'; + this.set('Location', url); + + // status + if (!statuses.redirect[this.status]) this.status = 302; + + // html + if (this.ctx.accepts('html')) { + url = escape(url); + this.type = 'text/html; charset=utf-8'; + this.body = `Redirecting to <a href="${url}">${url}</a>.`; + return; + } + + // text + this.type = 'text/plain; charset=utf-8'; + this.body = `Redirecting to ${url}.`; + }, + + /** + * Set Content-Disposition header to "attachment" with optional `filename`. + * + * @param {String} filename + * @api public + */ + + attachment(filename, options) { + if (filename) this.type = extname(filename); + this.set('Content-Disposition', contentDisposition(filename, options)); + }, + + /** + * Set Content-Type response header with `type` through `mime.lookup()` + * when it does not contain a charset. + * + * Examples: + * + * this.type = '.html'; + * this.type = 'html'; + * this.type = 'json'; + * this.type = 'application/json'; + * this.type = 'png'; + * + * @param {String} type + * @api public + */ + + set type(type) { + type = getType(type); + if (type) { + this.set('Content-Type', type); + } else { + this.remove('Content-Type'); + } + }, + + /** + * Set the Last-Modified date using a string or a Date. + * + * this.response.lastModified = new Date(); + * this.response.lastModified = '2013-09-13'; + * + * @param {String|Date} type + * @api public + */ + + set lastModified(val) { + if ('string' == typeof val) val = new Date(val); + this.set('Last-Modified', val.toUTCString()); + }, + + /** + * Get the Last-Modified date in Date form, if it exists. + * + * @return {Date} + * @api public + */ + + get lastModified() { + const date = this.get('last-modified'); + if (date) return new Date(date); + }, + + /** + * Set the ETag of a response. + * This will normalize the quotes if necessary. + * + * this.response.etag = 'md5hashsum'; + * this.response.etag = '"md5hashsum"'; + * this.response.etag = 'W/"123456789"'; + * + * @param {String} etag + * @api public + */ + + set etag(val) { + if (!/^(W\/)?"/.test(val)) val = `"${val}"`; + this.set('ETag', val); + }, + + /** + * Get the ETag of a response. + * + * @return {String} + * @api public + */ + + get etag() { + return this.get('ETag'); + }, + + /** + * Return the response mime type void of + * parameters such as "charset". + * + * @return {String} + * @api public + */ + + get type() { + const type = this.get('Content-Type'); + if (!type) return ''; + return type.split(';')[0]; + }, + + /** + * Check whether the response is one of the listed types. + * Pretty much the same as `this.request.is()`. + * + * @param {String|Array} types... + * @return {String|false} + * @api public + */ + + is(types) { + const type = this.type; + if (!types) return type || false; + if (!Array.isArray(types)) types = [].slice.call(arguments); + return typeis(type, types); + }, + + /** + * Return response header. + * + * Examples: + * + * this.get('Content-Type'); + * // => "text/plain" + * + * this.get('content-type'); + * // => "text/plain" + * + * @param {String} field + * @return {String} + * @api public + */ + + get(field) { + return this.header[field.toLowerCase()] || ''; + }, + + /** + * Set header `field` to `val`, or pass + * an object of header fields. + * + * Examples: + * + * this.set('Foo', ['bar', 'baz']); + * this.set('Accept', 'application/json'); + * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); + * + * @param {String|Object|Array} field + * @param {String} val + * @api public + */ + + set(field, val) { + if (this.headerSent) return; + + if (2 == arguments.length) { + if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v)); + else if (typeof val !== 'string') val = String(val); + this.res.setHeader(field, val); + } else { + for (const key in field) { + this.set(key, field[key]); + } + } + }, + + /** + * Append additional header `field` with value `val`. + * + * Examples: + * + * ``` + * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']); + * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); + * this.append('Warning', '199 Miscellaneous warning'); + * ``` + * + * @param {String} field + * @param {String|Array} val + * @api public + */ + + append(field, val) { + const prev = this.get(field); + + if (prev) { + val = Array.isArray(prev) + ? prev.concat(val) + : [prev].concat(val); + } + + return this.set(field, val); + }, + + /** + * Remove header `field`. + * + * @param {String} name + * @api public + */ + + remove(field) { + if (this.headerSent) return; + + this.res.removeHeader(field); + }, + + /** + * Checks if the request is writable. + * Tests for the existence of the socket + * as node sometimes does not set it. + * + * @return {Boolean} + * @api private + */ + + get writable() { + // can't write any more after response finished + if (this.res.finished) return false; + + const socket = this.res.socket; + // There are already pending outgoing res, but still writable + // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 + if (!socket) return true; + return socket.writable; + }, + + /** + * Inspect implementation. + * + * @return {Object} + * @api public + */ + + inspect() { + if (!this.res) return; + const o = this.toJSON(); + o.body = this.body; + return o; + }, + + /** + * Return JSON representation. + * + * @return {Object} + * @api public + */ + + toJSON() { + return only(this, [ + 'status', + 'message', + 'header' + ]); + }, + + /** + * Flush any set headers, and begin the body + */ + flushHeaders() { + this.res.flushHeaders(); + } +}; + +/** + * Custom inspection implementation for newer Node.js versions. + * + * @return {Object} + * @api public + */ +if (util.inspect.custom) { + module.exports[util.inspect.custom] = module.exports.inspect; +} |
