Source: commands.js

"use strict";
/**
 * Creates commands instance.
 *
 * @class
 * @name Commands
 * @classdesc Aggregates commands to manage `GlaceJS` proxy.
 * @arg {object} config - Commands config.
 * @arg {object} [opts] - Commands options.
 * @arg {function} [opts.logger] - Commands logger. Default is system logger.
 * @arg {boolean} [opts.colored=false] - Flag to use ANSI color in log message.
 * @arg {object} [opts.GlobalProxy] - Global proxy class.
 * @arg {object} [opts.HttpProxy] - HTTP proxy class.
 * @arg {object} [opts.isRunning] - Function to detect is process launched.
 */

var fs = require("fs");
var url = require("url");

require("colors");
var _ = require("lodash");
var chromeLauncher = require("chrome-launcher");
var fse = require("fs-extra");
var isRunning = require("is-running");
var temp = require("temp").track();
var U = require("glace-utils");

var cache = require("./middleware/cache");
var GlobalProxy = require("./globalProxy");
var HttpProxy = require("./httpProxy");

var LOG = U.logger;

var Commands = function (config, opts) {
    this._config = config;

    opts = U.defVal(opts, {});
    this._log = U.defVal(opts.logger, LOG.info.bind(LOG));
    this._coloredLog = U.defVal(opts.colored, false);

    this._chrome = null;
    this._httpProxy = null;
    this._globalProxy = null;

    this.__GlobalProxy = U.defVal(opts.GlobalProxy, GlobalProxy);
    this.__HttpProxy = U.defVal(opts.HttpProxy, HttpProxy);
    this.__isRunning = U.defVal(opts.isRunning, isRunning);
    this.__chromeLauncher = U.defVal(opts.chromeLauncher, chromeLauncher);
    this.__fs = U.defVal(opts.fs, fs);
    this.__fse = U.defVal(opts.fse, fse);
    this.__cache = U.defVal(opts.cache, cache);
};
module.exports = Commands;
/**
 * Command to set proxied URL.
 * 
 * @async
 * @method
 * @return {Promise}
 */
Commands.prototype.setProxiedUrl = async function (proxiedUrl) {
    this._config.web.url = proxiedUrl;
    if (this._isHttpProxyLaunched()) {
        this._httpProxy.setUrl(proxiedUrl);
    };
    if (this._isChromeLaunched()) return await this.restartChrome();
    return true;
};
/**
 * Command to launch HTTP proxy.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if HTTP proxy was launched. `false` if
 *  command can't be executed. Causes to not launch HTTP proxy:
 *  - HTTP proxy is launched already.
 *  - Proxied URL isn't specified.
 */
Commands.prototype.launchHttpProxy = async function () {
    var msg;

    if (this._isHttpProxyLaunched()) {
        msg = "HTTP proxy is launched already";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    if (!this._config.web.url) {
        msg = "HTTP proxy isn't launched because proxied URL is missed";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    this._httpProxy = this._httpProxy || new this.__HttpProxy({
        port: this._config.proxy.port,
        timeout: this._config.proxy.timeout,
        reconnect: this._config.proxy.reconnect,
        useCache: this._config.cache.use,
        url: this._config.web.url,
        speed: this._config.proxy.speed,
    });
    
    return this._httpProxy
        .start()
        .then(() => {
            if (this._coloredLog) {
                msg = `HTTP proxy is started on host ${U.hostname.cyan} ` +
                    `and port ${this._httpProxy._port.toString().yellow}`;
            } else {
                msg = `HTTP proxy is started on host '${U.hostname}' and ` +
                    `port '${this._httpProxy._port}'`;
            };
            this._log(msg);
        })
        .then(() => {
            if (this._isChromeLaunched()) return this.restartChrome();
        })
        .then(() => true);
};
/**
 * Command to stop HTTP proxy.
 *
 * @async
 * @method
 * @arg {object} [opts] - Options.
 * @arg {boolean} [opts.restartChrome=true] - Restart chrome.
 * @return {Promise<boolean>} `true` if HTTP proxy was stopped. `false` if
 *  command can't be executed. Causes to not stop HTTP proxy:
 *  - HTTP proxy isn't launched yet.
 */
Commands.prototype.stopHttpProxy = async function (opts) {

    if (!this._isHttpProxyLaunched()) {
        var msg = "HTTP proxy isn't launched yet";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    opts = U.defVal(opts, {});
    var restartChrome = U.defVal(opts.restartChrome, true);

    this._httpProxy.stop();
    if (restartChrome && this._isChromeLaunched()) await this.restartChrome();
    return true;
};
/**
 * Command to restart HTTP proxy.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if HTTP proxy was restarted. `false` if
 *  command can't be executed. Causes to not restart HTTP proxy are the same as
 *  for command to launch HTTP proxy.
 */
Commands.prototype.restartHttpProxy = function() {
    return this
        .stopHttpProxy({ restartChrome: false })
        .then(() => this.launchHttpProxy());
};
/**
 * Command to launch global transparent proxy.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if global transparent proxy isn't
 *  launched. `false` if command can't be executed. Causes to not launch
 *  global transparent proxy:
 *  - Global transparent proxy is launched already.
 */
Commands.prototype.launchGlobalProxy = function () {
    var msg;

    if (this._isGlobalProxyLaunched()) {
        msg = "Global transparent proxy is launched already";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    this._globalProxy = this._globalProxy || new this.__GlobalProxy({
        port: this._config.proxy.globalPort,
        timeout: this._config.proxy.timeout,
        reconnect: this._config.proxy.reconnect,
        installCertificate: this._config.proxy.installCertificate,
        sslCaDir: this._config.proxy.sslCaDir,
        useCache: this._config.cache.use,
        speed: this._config.proxy.speed,
    });

    return this._globalProxy
        .start()
        .then(() => {
            if (this._coloredLog) {
                msg = "Global transparent proxy is started on " +
                    `${U.hostname.cyan} and port ` +
                    `${this._globalProxy.getPort().toString().yellow}`;
            } else {
                msg = "Global transparent proxy is started on host " +
                    `'${U.hostname}' and port '${this._globalProxy.getPort()}'`;
            };
            this._log(msg);
        })
        .then(() => {
            if (this._isChromeLaunched()) return this.restartChrome();
        })
        .then(() => true);
};
/**
 * Command to stop global transparent proxy.
 *
 * @async
 * @method
 * @arg {object} [opts] - Options.
 * @arg {boolean} [opts.restartChrome=true] - Restart chrome.
 * @return {Promise<boolean>} `true` if global transparent proxy was stopped.
 *  `false` if command can't be executed. Causes to not stop global transparent
 *  proxy:
 *  - Global transparent proxy isn't launched yet.
 */
Commands.prototype.stopGlobalProxy = async function (opts) {

    if (!this._isGlobalProxyLaunched()) {
        var msg = "Global transparent proxy isn't launched yet";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    opts = U.defVal(opts, {});
    var restartChrome = U.defVal(opts.restartChrome, true);

    this._globalProxy.stop();
    if (restartChrome && this._isChromeLaunched()) await this.restartChrome();
    return true;
};
/**
 * Command to restart global transparent proxy.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if global transparent proxy was restarted.
 *  `false` if command can't be executed. Causes to not restart global
 *  transparent proxy are the same as for command to launch global
 *  transparent proxy.
 */
Commands.prototype.restartGlobalProxy = function() {
    return this
        .stopGlobalProxy({ restartChrome: false })
        .then(() => this.launchGlobalProxy());
};
/**
 * Command to launch chrome browser.
 *
 * @async
 * @method
 * @arg {object} [opts] - Options.
 * @arg {string[]} [opts.chromeOpts=[]] - Chrome options list.
 * @return {Promise<boolean>} `true` if chrome browser was launched. `false`
 *  if command can't be executed. Causes to not launch chrome browser:
 *  - HTTP proxy isn't launched yet.
 *  - Chrome browser is launched already.
 */
Commands.prototype.launchChrome = async function (opts) {

    opts = U.defVal(opts, {});
    var chromeOpts = U.defVal(opts.chromeOpts, []);

    if (this._isChromeLaunched()) {
        var msg = "Chrome browser is launched already";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    var chromeFlags = [
        "start-maximized",
        "disable-infobars",
        "enable-precise-memory-info",
        "disable-translate",
    ];

    var opt;
    for (opt of chromeOpts) {
        if (!isOptPresent(opt, chromeFlags)) chromeFlags.push(opt);
    };

    if (this._config.chrome.incognito && !isOptPresent("incognito", chromeFlags)) {
        chromeFlags.push("incognito");
    };

    if (this._isGlobalProxyLaunched()) {
        var proxyOpts = [
            "ignore-certificate-errors",
            `proxy-server=${U.hostname}:${this._globalProxy.getPort()}`,
            `proxy-bypass-list=localhost,127.0.0.1,${U.hostname}`,
        ];
        for (opt of proxyOpts) {
            if (!isOptPresent(opt, chromeFlags)) chromeFlags.push(opt);
        };
    };

    for (var i = 0; i < chromeFlags.length; i++) {
        if (!chromeFlags[i].startsWith("-")) {
            chromeFlags[i] = "--" + chromeFlags[i];
        };
    };

    var profileDir = temp.path();
    fse.mkdirsSync(profileDir);

    return this.__chromeLauncher.launch({

        startingUrl: this._chromeUrl(),
        userDataDir: profileDir,
        chromeFlags: chromeFlags,
        handleSIGINT: true,

    }).then(chrome => {

        this._chrome = chrome;

        var msg;
        if (this._coloredLog) {
            msg = "Chrome is launched with PID " +
                `${chrome.pid.toString().yellow}`;
        } else {
            msg = `Chrome is launched with PID '${chrome.pid}'`;
        }
        this._log(msg);

        LOG.info(`Chrome ${chrome.pid}`);
        LOG.info(`Chrome debugging port ${chrome.port}`);
        LOG.info(`Chrome profile ${profileDir}`);

        return true;
    });
};
/**
 * Command to close chrome browser.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if chrome browser was closed. `false` if
 *  command can't be executed. Causes to not close chrome browser:
 *  - Chrome browser isn't launched yet.
 */
Commands.prototype.closeChrome = async function () {

    if (!this._isChromeLaunched()) {
        var msg = "Chrome browser isn't launched yet";
        if (this._coloredLog) msg = msg.red;
        this._log(msg);
        return false;
    };

    await this._chrome.kill();
    this._chrome = null;
    return true;
};
/**
 * Command to restart chrome browser.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if chrome browser was restarted. `false`
 *  if command can't be executed. Causes to not restart chrome browser are the
 *  the same as for command to launch chrome browser.
 */
Commands.prototype.restartChrome = function () {
    return this.closeChrome().then(() => this.launchChrome());
};
/**
 * Command to set proxy speed.
 *
 * @async
 * @method
 * @arg {number} speed - Proxy speed, kb/s.
 * @arg {?number} [speed.req] - Requests speed, kb/s.
 * @arg {?number} [speed.res] - Responses speed, kb/s.
 * @return {Promise<boolean>} `true` if proxy speed was set. `false` if
 *  command can't be executed. Causes to not set proxy speed:
 *  - HTTP proxy isn't launched.
 */
Commands.prototype.setProxySpeed = async function (speed) {
    if (!this._checkProxy()) return false;

    if (this._httpProxy) {
        this._httpProxy.setSpeed(speed);
    };
    if (this._globalProxy) {
        this._globalProxy.setSpeed(speed);
    };
    return true;
};
/**
 * Command to reset proxy speed.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if proxy speed was reset. `false` if
 *  command can't be executed. Causes to not reset proxy speed:
 *  - HTTP proxy isn't launched.
 */
Commands.prototype.resetProxySpeed = async function () {
    if (!this._checkProxy()) return false;

    if (this._httpProxy) {
        this._httpProxy.resetSpeed();
    };
    if (this._globalProxy) {
        this._globalProxy.resetSpeed();
    };
    return true;
};
/**
 * Command to enable proxy cache.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if proxy cache wasn't enabled. `false` if
 *  command can't be executed. Causes to not enable proxy cache:
 *  - HTTP proxy isn't launched.
 */
Commands.prototype.enableProxyCache = async function () {
    if (!this._checkProxy()) return false;

    if (this._httpProxy) {
        this._httpProxy.useCache = true;
    };
    if (this._globalProxy) {
        this._globalProxy.useCache = true;
    };
    return true;
};
/**
 * Command to disable proxy cache.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` if proxy cache wasn't disabled. `false`
 *  if command can't be executed. Causes to not disable proxy cache:
 *  - HTTP proxy isn't launched.
 */
Commands.prototype.disableProxyCache = async function () {
    if (!this._checkProxy()) return false;

    if (this._httpProxy) {
        this._httpProxy.useCache = false;
    };
    if (this._globalProxy) {
        this._globalProxy.useCache = false;
    };
    return true;
};
/**
 * Command to clear proxy cache.
 *
 * @async
 * @method
 * @return {Promise<boolean>} `true` when cache will be cleared.
 */
Commands.prototype.clearProxyCache = async function () {
    if (this.__fs.existsSync(this._config.cache.folder)) {
        this.__fse.removeSync(this._config.cache.folder);
    };
    await this.__cache.init({ force: true });
    return true;
};
/**
 * Helper to check whether any proxy is launched.
 *
 * @method
 * @protected
 * @return {boolean} `true` if proxy exists and launched, `false` otherwise.
 */
Commands.prototype._checkProxy = function () {
    if ((this._isHttpProxyLaunched()) ||
        (this._isGlobalProxyLaunched())) return true;
    var msg = "No one of http or global proxy isn't launched yet";
    if (this._coloredLog) msg = msg.red;
    this._log(msg);
    return false;
};
/**
 * Helper to define whether chrome is launched.
 *
 * @method
 * @protected
 * @return {boolean}
 */
Commands.prototype._isChromeLaunched = function () {
    return !!(this._chrome && this.__isRunning(this._chrome.pid));
};
/**
 * Helper to get URL to open in chrome browser. If HTTP proxy is launched,
 *  it will return proxy URL.
 *
 * @method
 * @protected
 * @return {string} URL to open chrome.
 */
Commands.prototype._chromeUrl = function () {
    var result;

    if (this._isHttpProxyLaunched()) {
        result = _.trim(
            this._httpProxy.url + url.parse(this._config.web.url).pathname, "/");
    } else {
        result = this._config.web.url;
    };

    return result;
};
/**
 * Helper to define whether http proxy is running.
 *
 * @method
 * @protected
 * @return {boolean}
 */
Commands.prototype._isHttpProxyLaunched = function () {
    return !!(this._httpProxy && this._httpProxy.isRunning);
};
/**
 * Helper to define whether global proxy is running.
 *
 * @method
 * @protected
 * @return {boolean}
 */
Commands.prototype._isGlobalProxyLaunched = function () {
    return !!(this._globalProxy && this._globalProxy.isRunning);
};
/**
 * Helper to define whether chrome option is present in options list.
 * 
 * @ignore
 * @function
 * @arg {string} opt - Checking option.
 * @arg {string[]} opts - List of options.
 * @return {boolean}
 */
var isOptPresent = (opt, opts) => {
    var optStart = opt.split("=")[0];
    for (var o of opts) {
        if (o.split("=")[0] === optStart) return true;
    };
    return false;
};