Home Reference Source

core/request/network.js

import Promise from 'es6-promise';
import { Buffer } from 'buffer';
import qs from 'qs';
import assign from 'lodash/assign';
import defaults from 'lodash/defaults';
import isEmpty from 'lodash/isEmpty';
import url from 'url';
import isString from 'lodash/isString';
import { Client } from '../client';
import { Query } from '../query';
import { Aggregation } from '../aggregation';
import { isDefined, appendQuery } from '../utils';
import { InvalidCredentialsError, NoActiveUserError, KinveyError } from '../errors';
import { Request, RequestMethod } from './request';
import { Headers } from './headers';
import { NetworkRack } from './rack';
import { KinveyResponse } from './response';

/**
 * @private
 */
export class NetworkRequest extends Request {
  constructor(options = {}) {
    super(options);
    this.rack = NetworkRack;
  }
}

/**
 * @private
 */
export const AuthType = {
  All: 'All',
  App: 'App',
  Basic: 'Basic',
  Default: 'Default',
  Master: 'Master',
  None: 'None',
  Session: 'Session',
  Client: 'Client'
};
Object.freeze(AuthType);

const Auth = {
  /**
   * Authenticate through (1) user credentials, (2) Master Secret, or (3) App
   * Secret.
   *
   * @returns {Object}
   */
  all(client) {
    return Auth.session(client)
      .catch(() => Auth.basic(client));
  },

  /**
   * Authenticate through App Secret.
   *
   * @returns {Object}
   */
  app(client) {
    if (!client.appKey || !client.appSecret) {
      return Promise.reject(
        new Error('Missing client appKey and/or appSecret.'
          + ' Use Kinvey.initialize() to set the appKey and appSecret for the client.')
      );
    }

    return Promise.resolve({
      scheme: 'Basic',
      username: client.appKey,
      password: client.appSecret
    });
  },

  /**
   * Authenticate through (1) Master Secret, or (2) App Secret.
   *
   * @returns {Object}
   */
  basic(client) {
    return Auth.master(client)
      .catch(() => Auth.app(client));
  },

  client(client, clientId) {
    if (!client.appKey || !client.appSecret) {
      return Promise.reject(
        new Error('Missing client appKey and/or appSecret'
          + ' Use Kinvey.initialize() to set the appKey and appSecret for the client.')
      );
    }
    if (!clientId){
      clientId = client.appKey;
    }
    return Promise.resolve({
      scheme: 'Basic',
      username: clientId,
      password: client.appSecret
    });
  },

  /**
   * Authenticate through Master Secret.
   *
   * @returns {Object}
   */
  master(client) {
    if (!client.appKey || !client.masterSecret) {
      return Promise.reject(
        new Error('Missing client appKey and/or masterSecret.'
          + ' Use Kinvey.initialize() to set the appKey and masterSecret for the client.')
      );
    }

    return Promise.resolve({
      scheme: 'Basic',
      username: client.appKey,
      password: client.masterSecret
    });
  },

  /**
   * Do not authenticate.
   *
   * @returns {Null}
   */
  none() {
    return Promise.resolve(null);
  },

  /**
   * Authenticate through user credentials.
   *
   * @returns {Object}
   */
  session(client) {
    const activeUser = client.getActiveUser();

    if (!isDefined(activeUser)) {
      return Promise.reject(
        new NoActiveUserError('There is not an active user. Please login a user and retry the request.')
      );
    }

    if (!isDefined(activeUser._kmd) || !isDefined(activeUser._kmd.authtoken)) {
      return Promise.reject(
        new NoActiveUserError('The active user does not have a valid auth token.')
      );
    }

    return Promise.resolve({
      scheme: 'Kinvey',
      credentials: activeUser._kmd.authtoken
    });
  }
};

function byteCount(str) {
  if (str) {
    let count = 0;
    const stringLength = str.length;
    str = String(str || '');

    for (let i = 0; i < stringLength; i += 1) {
      const partCount = encodeURI(str[i]).split('%').length;
      count += partCount === 1 ? 1 : partCount - 1;
    }

    return count;
  }

  return 0;
}

/**
 * @private
 */
export class Properties extends Headers { }

/**
 * @private
 */
export class KinveyRequest extends NetworkRequest {
  constructor(options = {}) {
    super(options);

    options = assign({
      skipBL: false,
      trace: false
    }, options);

    this.authType = options.authType || AuthType.None;
    this.query = options.query;
    this.aggregation = options.aggregation;
    this.properties = options.properties || new Properties();
    this.skipBL = options.skipBL === true;
    this.trace = options.trace === true;
    this.clientId = options.clientId;
    this.kinveyFileTTL = options.kinveyFileTTL;
    this.kinveyFileTLS = options.kinveyFileTLS;
  }

  static execute(options, client, dataOnly = true) {
    const o = assign({
      method: RequestMethod.GET,
      authType: AuthType.Default
    }, options);
    client = client || Client.sharedInstance();

    if (!o.url && isString(o.pathname) && client) {
      o.url = url.format({
        protocol: client.apiProtocol,
        host: client.apiHost,
        pathname: o.pathname
      });
    }

    let prm = new KinveyRequest(o).execute();
    if (dataOnly) {
      prm = prm.then(r => r.data);
    }
    return prm;
  }

  get appVersion() {
    return this.client.appVersion;
  }

  get query() {
    return this._query;
  }

  set query(query) {
    if (isDefined(query) && !(query instanceof Query)) {
      throw new KinveyError('Invalid query. It must be an instance of the Query class.');
    }

    this._query = query;
  }

  get aggregation() {
    return this._aggregation;
  }

  set aggregation(aggregation) {
    if (isDefined(aggregation) && !(aggregation instanceof Aggregation)) {
      throw new KinveyError('Invalid aggregation. It must be an instance of the Aggregation class.');
    }

    if (isDefined(aggregation)) {
      this.body = aggregation.toPlainObject();
    }

    this._aggregation = aggregation;
  }

  get headers() {
    const headers = super.headers;

    // Add the Accept header
    if (!headers.has('Accept')) {
      headers.set('Accept', 'application/json; charset=utf-8');
    }

    // Add the Content-Type header
    if (!headers.has('Content-Type')) {
      headers.set('Content-Type', 'application/json; charset=utf-8');
    }

    // Add the X-Kinvey-API-Version header
    if (!headers.has('X-Kinvey-Api-Version')) {
      headers.set('X-Kinvey-Api-Version', 4);
    }


    // Add or remove the X-Kinvey-Skip-Business-Logic header
    if (this.skipBL === true) {
      headers.set('X-Kinvey-Skip-Business-Logic', true);
    } else {
      headers.remove('X-Kinvey-Skip-Business-Logic');
    }

    // Add or remove the X-Kinvey-Include-Headers-In-Response and X-Kinvey-ResponseWrapper headers
    if (this.trace === true) {
      headers.set('X-Kinvey-Include-Headers-In-Response', 'X-Kinvey-Request-Id');
      headers.set('X-Kinvey-ResponseWrapper', true);
    } else {
      headers.remove('X-Kinvey-Include-Headers-In-Response');
      headers.remove('X-Kinvey-ResponseWrapper');
    }

    // Add or remove the X-Kinvey-Client-App-Version header
    if (this.appVersion) {
      headers.set('X-Kinvey-Client-App-Version', this.appVersion);
    } else {
      headers.remove('X-Kinvey-Client-App-Version');
    }

    // Add or remove X-Kinvey-Custom-Request-Properties header
    if (this.properties) {
      const customPropertiesHeader = this.properties.toString();

      if (!isEmpty(customPropertiesHeader)) {
        const customPropertiesByteCount = byteCount(customPropertiesHeader);

        if (customPropertiesByteCount >= 2000) {
          throw new Error(
            `The custom properties are ${customPropertiesByteCount} bytes.` +
            'It must be less then 2000 bytes.',
            'Please remove some custom properties.');
        }

        headers.set('X-Kinvey-Custom-Request-Properties', customPropertiesHeader);
      } else {
        headers.remove('X-Kinvey-Custom-Request-Properties');
      }
    } else {
      headers.remove('X-Kinvey-Custom-Request-Properties');
    }

    // Return the headers
    return headers;
  }

  set headers(headers) {
    super.headers = headers;
  }

  get url() {
    const urlString = super.url;
    let queryString = { kinveyfile_ttl: this.kinveyFileTTL, kinveyfile_tls: this.kinveyFileTLS };

    if (this.query) {
      queryString = Object.assign({}, queryString, this.query.toQueryString());
    }

    if (isEmpty(queryString)) {
      return urlString;
    }

    return appendQuery(urlString, qs.stringify(queryString));
  }

  set url(urlString) {
    super.url = urlString;
  }

  get properties() {
    return this._properties;
  }

  set properties(properties) {
    if (properties && (properties instanceof Properties) === false) {
      properties = new Properties(properties);
    }

    this._properties = properties;
  }

  getAuthorizationHeader() {
    let promise = Promise.resolve(undefined);

    // Add or remove the Authorization header
    if (this.authType) {
      // Get the auth info based on the set AuthType
      switch (this.authType) {
        case AuthType.All:
          promise = Auth.all(this.client);
          break;
        case AuthType.App:
          promise = Auth.app(this.client);
          break;
        case AuthType.Basic:
          promise = Auth.basic(this.client);
          break;
        case AuthType.Client:
          promise = Auth.client(this.client, this.clientId);
          break;
        case AuthType.Master:
          promise = Auth.master(this.client);
          break;
        case AuthType.None:
          promise = Auth.none(this.client);
          break;
        case AuthType.Session:
          promise = Auth.session(this.client);
          break;
        default:
          promise = Auth.session(this.client)
            .catch((error) => {
              return Auth.master(this.client)
                .catch(() => {
                  throw error;
                });
            });
      }
    }

    return promise
      .then((authInfo) => {
        // Add the auth info to the Authorization header
        if (isDefined(authInfo)) {
          let credentials = authInfo.credentials;

          if (authInfo.username) {
            credentials = new Buffer(`${authInfo.username}:${authInfo.password}`).toString('base64');
          }

          return `${authInfo.scheme} ${credentials}`;
        }

        return undefined;
      });
  }

  /** @returns {Promise} */
  execute(rawResponse = false, retry = true) {
    return this.getAuthorizationHeader()
      .then((authorizationHeader) => {
        if (isDefined(authorizationHeader)) {
          this.headers.set('Authorization', authorizationHeader);
        } else {
          this.headers.remove('Authorization');
        }
      })
      .then(() => {
        return super.execute();
      })
      .then((response) => {
        if ((response instanceof KinveyResponse) === false) {
          response = new KinveyResponse({
            statusCode: response.statusCode,
            headers: response.headers,
            data: response.data
          });
        }

        if (rawResponse === false && response.isSuccess() === false) {
          throw response.error;
        }

        return response;
      })
      .catch((error) => {
        if (retry && error instanceof InvalidCredentialsError) {
          const activeUser = this.client.getActiveUser();

          if (isDefined(activeUser)) {
            const socialIdentity = isDefined(activeUser._socialIdentity) ? activeUser._socialIdentity : {};
            const sessionKey = Object.keys(socialIdentity)
              .find(sessionKey => socialIdentity[sessionKey].identity === 'kinveyAuth');
            const oldSession = socialIdentity[sessionKey];

            if (isDefined(oldSession)) {
              const request = new KinveyRequest({
                method: RequestMethod.POST,
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded'
                },
                authType: AuthType.App,
                url: url.format({
                  protocol: this.client.micProtocol,
                  host: this.client.micHost,
                  pathname: '/oauth/token'
                }),
                body: {
                  grant_type: 'refresh_token',
                  client_id: oldSession.client_id,
                  redirect_uri: oldSession.redirect_uri,
                  refresh_token: oldSession.refresh_token
                },
                properties: this.properties,
                timeout: this.timeout
              });
              return request.execute()
                .then(response => response.data)
                .then((session) => {
                  session.identity = oldSession.identity;
                  session.client_id = oldSession.client_id;
                  session.redirect_uri = oldSession.redirect_uri;
                  session.protocol = this.client.micProtocol;
                  session.host = this.client.micHost;
                  return session;
                })
                .then((session) => {
                  const data = {};
                  socialIdentity[session.identity] = session;
                  data._socialIdentity = socialIdentity;

                  const request = new KinveyRequest({
                    method: RequestMethod.POST,
                    authType: AuthType.App,
                    url: url.format({
                      protocol: this.client.apiProtocol,
                      host: this.client.apiHost,
                      pathname: `/user/${this.client.appKey}/login`
                    }),
                    properties: this.properties,
                    body: data,
                    timeout: this.timeout,
                    client: this.client
                  });
                  return request.execute()
                    .then((response) => response.data)
                    .then((user) => {
                      user._socialIdentity[session.identity] = defaults(user._socialIdentity[session.identity], session);
                      return this.client.setActiveUser(user);
                    });
                })
                .then(() => {
                  return this.execute(rawResponse, false);
                })
                .catch(() => Promise.reject(error));
            }
          }
        }

        return Promise.reject(error);
      });
  }
}