123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- '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 query string.
- *
- * @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 query string
- * 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*/, 1)[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(':', 1)[0];
- },
- /**
- * Get WHATWG parsed URL.
- * Lazily memoized.
- *
- * @return {URL|Object}
- * @api public
- */
- get URL() {
- /* istanbul ignore else */
- if (!this.memoizedURL) {
- const originalUrl = this.originalUrl || ''; // avoid undefined in template string
- try {
- this.memoizedURL = new URL(`${this.origin}${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() {
- try {
- const { parameters } = contentType.parse(this.req);
- return parameters.charset || '';
- } catch (e) {
- return '';
- }
- },
- /**
- * 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*/, 1)[0] : 'http';
- },
- /**
- * Shorthand 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 was "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(this.app.proxyIpHeader);
- let ips = proxy && val
- ? val.split(/\s*,\s*/)
- : [];
- if (this.app.maxIpsCount > 0) {
- ips = ips.slice(-this.app.maxIpsCount);
- }
- return ips;
- },
- /**
- * 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) {
- 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 if it contains any of the given 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|String[]} [type]
- * @param {String[]} [types]
- * @return {String|false|null}
- * @api public
- */
- is(type, ...types) {
- return typeis(this.req, type, ...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;
- }
|