app/extend/request.js

'use strict';

const querystring = require('querystring');
const accepts = require('accepts');

const _querycache = Symbol('_querycache');
const _queriesCache = Symbol('_queriesCache');
const PROTOCOL = Symbol('PROTOCOL');
const HOST = Symbol('HOST');
const ACCEPTS = Symbol('ACCEPTS');
const IPS = Symbol('IPS');
const RE_ARRAY_KEY = /[^\[\]]+\[\]$/;

module.exports = {
  /**
   * Parse the "Host" header field host
   * and support X-Forwarded-Host when a
   * proxy is enabled.
   * @member {String} Request#host
   * @example
   * ip + port
   * ```js
   * this.request.host
   * => '127.0.0.1:7001'
   * ```
   * or domain
   * ```js
   * this.request.host
   * => 'demo.eggjs.org'
   * ```
   */
  get host() {
    if (this[HOST]) return this[HOST];

    let host;
    if (this.app.config.proxy) {
      host = getFromHeaders(this, this.app.config.hostHeaders);
    }
    host = host || this.get('host') || '';
    this[HOST] = host.split(/\s*,\s*/)[0];
    return this[HOST];
  },

  /**
   * @member {String} Request#protocol
   * @example
   * ```js
   * this.request.protocol
   * => 'https'
   * ```
   */
  get protocol() {
    if (this[PROTOCOL]) return this[PROTOCOL];
    // detect encrypted socket
    if (this.socket && this.socket.encrypted) {
      this[PROTOCOL] = 'https';
      return this[PROTOCOL];
    }
    // get from headers specified in `app.config.protocolHeaders`
    if (this.app.config.proxy) {
      const proto = getFromHeaders(this, this.app.config.protocolHeaders);
      if (proto) {
        this[PROTOCOL] = proto.split(/\s*,\s*/)[0];
        return this[PROTOCOL];
      }
    }
    // use protocol specified in `app.conig.protocol`
    this[PROTOCOL] = this.app.config.protocol || 'http';
    return this[PROTOCOL];
  },

  /**
   * Get all pass through ip addresses from the request.
   * Enable only on `app.config.proxy = true`
   *
   * @member {Array} Request#ips
   * @example
   * ```js
   * this.request.ips
   * => ['100.23.1.2', '201.10.10.2']
   * ```
   */
  get ips() {
    if (this[IPS]) return this[IPS];

    // return empty array when proxy=false
    if (!this.app.config.proxy) {
      this[IPS] = [];
      return this[IPS];
    }

    const val = getFromHeaders(this, this.app.config.ipHeaders) || '';
    this[IPS] = val ? val.split(/\s*,\s*/) : [];

    if (this.app.config.maxProxyCount > 0) {
      // if maxProxyCount present, only keep `maxProxyCount + 1` ips
      // [ illegalIp, clientRealIp, proxyIp1, proxyIp2 ...]
      this[IPS] = this[IPS].slice(-(this.app.config.maxProxyCount + 1));
    }
    return this[IPS];
  },

  /**
   * Get the request remote IPv4 address
   * @member {String} Request#ip
   * @return {String} IPv4 address
   * @example
   * ```js
   * this.request.ip
   * => '127.0.0.1'
   * => '111.10.2.1'
   * ```
   */
  get ip() {
    if (this._ip) {
      return this._ip;
    }
    const ip = this.ips[0] || this.socket.remoteAddress;
    // will be '::ffff:x.x.x.x', should conver to standard IPv4 format
    // https://zh.wikipedia.org/wiki/IPv6
    this._ip = ip && ip.indexOf('::ffff:') > -1 ? ip.substring(7) : ip;
    return this._ip;
  },

  /**
   * Set the request remote IPv4 address
   * @member {String} Request#ip
   * @param {String} ip - IPv4 address
   * @example
   * ```js
   * this.request.ip
   * => '127.0.0.1'
   * => '111.10.2.1'
   * ```
   */
  set ip(ip) {
    this._ip = ip;
  },

  /**
   * detect if response should be json
   * 1. url path ends with `.json`
   * 2. response type is set to json
   * 3. detect by request accept header
   *
   * @member {Boolean} Request#acceptJSON
   * @since 1.0.0
   */
  get acceptJSON() {
    if (this.path.endsWith('.json')) return true;
    if (this.response.type && this.response.type.indexOf('json') >= 0) return true;
    if (this.accepts('html', 'text', 'json') === 'json') return true;
    return false;
  },

  // How to read query safely
  // https://github.com/koajs/qs/issues/5
  _customQuery(cacheName, filter) {
    const str = this.querystring || '';
    let c = this[cacheName];
    if (!c) {
      c = this[cacheName] = {};
    }
    let cacheQuery = c[str];
    if (!cacheQuery) {
      cacheQuery = c[str] = {};
      const isQueries = cacheName === _queriesCache;
      // `querystring.parse` CANNOT parse something like `a[foo]=1&a[bar]=2`
      const query = str ? querystring.parse(str) : {};
      for (const key in query) {
        if (!key) {
          // key is '', like `a=b&`
          continue;
        }
        const value = filter(query[key]);
        cacheQuery[key] = value;
        if (isQueries && RE_ARRAY_KEY.test(key)) {
          // `this.queries['key'] => this.queries['key[]']` is compatibly supported
          const subkey = key.substring(0, key.length - 2);

          if (!cacheQuery[subkey]) {
            cacheQuery[subkey] = value;
          }
        }
      }
    }
    return cacheQuery;
  },

  /**
   * get params pass by querystring, all values are of string type.
   * @member {Object} Request#query
   * @example
   * ```js
   * GET http://127.0.0.1:7001?name=Foo&age=20&age=21
   * this.query
   * => { 'name': 'Foo', 'age': '20' }
   *
   * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val
   * this.query
   * =>
   * {
   *   "a": "b",
   *   "o[foo]": "bar",
   *   "b[]": "1",
   *   "e": "val"
   * }
   * ```
   */
  get query() {
    return this._customQuery(_querycache, firstValue);
  },

  /**
   * get params pass by querystring, all value are Array type. {@link Request#query}
   * @member {Array} Request#queries
   * @example
   * ```js
   * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val
   * this.queries
   * =>
   * {
   *   "a": ["b", "c"],
   *   "o[foo]": ["bar"],
   *   "b[]": ["1", "2"],
   *   "e": ["val"]
   * }
   * ```
   */
  get queries() {
    return this._customQuery(_queriesCache, arrayValue);
  },

  get accept() {
    let accept = this[ACCEPTS];
    if (accept) {
      return accept;
    }
    accept = this[ACCEPTS] = accepts(this.req);
    return accept;
  },

  /**
   * Set query-string as an object.
   *
   * @function Request#query
   * @param {Object} obj set querystring and query object for request.
   * @return {void}
   */
  set query(obj) {
    this.querystring = querystring.stringify(obj);
  },
};


function firstValue(value) {
  if (Array.isArray(value)) {
    value = value[0];
  }
  return value;
}

function arrayValue(value) {
  if (!Array.isArray(value)) {
    value = [ value ];
  }
  return value;
}

function getFromHeaders(ctx, names) {
  if (!names) return '';
  names = names.split(/\s*,\s*/);
  for (const name of names) {
    const value = ctx.get(name);
    if (value) return value;
  }
  return '';
}