'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _ssh;

function _load_ssh() {
  return _ssh = _interopRequireDefault(require('ssh2'));
}

var _ramda;

function _load_ramda() {
  return _ramda = _interopRequireDefault(require('ramda'));
}

var _lodash;

function _load_lodash() {
  return _lodash = _interopRequireDefault(require('lodash.merge'));
}

var _prompts;

function _load_prompts() {
  return _prompts = require('../../../prompts');
}

var _keyGetter;

function _load_keyGetter() {
  return _keyGetter = _interopRequireDefault(require('./key-getter'));
}

var _componentObjects;

function _load_componentObjects() {
  return _componentObjects = _interopRequireDefault(require('../../component-objects'));
}

var _exceptions;

function _load_exceptions() {
  return _exceptions = require('../exceptions');
}

var _bitId;

function _load_bitId() {
  return _bitId = require('../../../bit-id');
}

var _utils;

function _load_utils() {
  return _utils = require('../../../utils');
}

var _componentNotFound;

function _load_componentNotFound() {
  return _componentNotFound = _interopRequireDefault(require('../../../scope/exceptions/component-not-found'));
}

var _component;

function _load_component() {
  return _component = _interopRequireDefault(require('../../../consumer/component'));
}

var _checkVersionCompatibility;

function _load_checkVersionCompatibility() {
  return _checkVersionCompatibility = _interopRequireDefault(require('../check-version-compatibility'));
}

var _logger;

function _load_logger() {
  return _logger = _interopRequireDefault(require('../../../logger/logger'));
}

var _constants;

function _load_constants() {
  return _constants = require('../../../constants');
}

var _removedComponents;

function _load_removedComponents() {
  return _removedComponents = require('../../removed-components');
}

var _mergeConflictOnRemote;

function _load_mergeConflictOnRemote() {
  return _mergeConflictOnRemote = _interopRequireDefault(require('../../exceptions/merge-conflict-on-remote'));
}

var _analytics;

function _load_analytics() {
  return _analytics = require('../../../analytics/analytics');
}

var _globalConfig;

function _load_globalConfig() {
  return _globalConfig = require('../../../api/consumer/lib/global-config');
}

var _generalError;

function _load_generalError() {
  return _generalError = _interopRequireDefault(require('../../../error/general-error'));
}

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var checkVersionCompatibility = (_ramda || _load_ramda()).default.once((_checkVersionCompatibility || _load_checkVersionCompatibility()).default);
var rejectNils = (_ramda || _load_ramda()).default.reject((_ramda || _load_ramda()).default.isNil);
var PASSPHRASE_MESSAGE = 'Encrypted private key detected, but no passphrase given';
var AUTH_FAILED_MESSAGE = 'All configured authentication methods failed';

var cachedPassphrase = null;

function absolutePath(path) {
  if (!path.startsWith('/')) return `~/${path}`;
  return path;
}

function clean(str) {
  return str.replace('\n', '');
}

var SSH = function () {
  // Username entered by the user on the prompt user/pass process

  function SSH(_ref) {
    var path = _ref.path,
        username = _ref.username,
        port = _ref.port,
        host = _ref.host;

    _classCallCheck(this, SSH);

    this.path = path;
    this.username = username;
    this.port = port;
    this.host = host || '';
  }

  _createClass(SSH, [{
    key: 'buildCmd',
    value: function buildCmd(commandName, path, payload, context) {
      return `bit ${commandName} ${(0, (_utils || _load_utils()).toBase64)(path)} ${(0, (_utils || _load_utils()).packCommand)((0, (_utils || _load_utils()).buildCommandMessage)(payload, context))}`;
    }
  }, {
    key: 'exec',
    value: function exec(commandName, payload, context) {
      var _this = this;

      (_logger || _load_logger()).default.debug(`ssh: going to run a remote command ${commandName}, path: ${this.path}`);
      // Add the entered username to context
      if (this._sshUsername) {
        context = context || {};
        context.sshUsername = this._sshUsername;
      }
      return new Promise(function (resolve, reject) {
        var res = '';
        var err = void 0;
        // No need to use packCommand on the payload in case of put command
        // because we handle all the base64 stuff in a better way inside the ComponentObjects.manyToString
        // inside pushMany function here
        var cmd = _this.buildCmd(commandName, absolutePath(_this.path || ''), commandName === '_put' ? null : payload, context);
        if (!_this.connection) {
          err = 'ssh connection is not defined';
          (_logger || _load_logger()).default.error(err);
          return reject(err);
        }
        _this.connection.exec(cmd, function (error, stream) {
          if (error) {
            (_logger || _load_logger()).default.error('ssh, exec returns an error: ', error);
            return reject(error);
          }
          if (commandName === '_put') {
            stream.stdin.write(payload);
            stream.stdin.end();
          }
          stream.on('data', function (response) {
            res += response.toString();
          }).on('exit', function (code) {
            (_logger || _load_logger()).default.info(`ssh: server had been exiting before closing. Exit code: ${code}`);
            var promiseExit = function promiseExit() {
              return code && code !== 0 ? reject(_this.errorHandler(code, err)) : resolve(clean(res));
            };
            // sometimes the connection 'exit' before 'close' and then it doesn't have the data (err) ready yet.
            // in that case, we prefer to wait until the onClose will terminate the promise.
            // sometimes though, the connection only 'exit' and never 'close' (happened when _put command sent back
            // more than 1MB of data), in that case, the following setTimeout will terminate the promise.
            setTimeout(promiseExit, 2000);
          }).on('close', function (code, signal) {
            if (commandName === '_put') res = res.replace(payload, '');
            (_logger || _load_logger()).default.debug(`ssh: returned with code: ${code}, signal: ${signal}.`);
            // DO NOT CLOSE THE CONNECTION (using this.connection.end()), it causes bugs when there are several open
            // connections. Same bugs occur when running "this.connection.end()" on "end" or "exit" events.
            return code && code !== 0 ? reject(_this.errorHandler(code, err)) : resolve(clean(res));
          }).stderr.on('data', function (response) {
            err = response.toString();
            (_logger || _load_logger()).default.error(`ssh: got an error, ${err}`);
          });
        });
      });
    }
  }, {
    key: 'errorHandler',
    value: function errorHandler(code, err) {
      var parsedError = void 0;
      try {
        var _unpack2 = this._unpack(err, false),
            headers = _unpack2.headers,
            payload = _unpack2.payload;

        checkVersionCompatibility(headers.version);
        parsedError = payload;
      } catch (e) {
        // be greacfull when can't parse error message
        (_logger || _load_logger()).default.error(`ssh: failed parsing error as JSON, error: ${err}`);
      }

      switch (code) {
        default:
          return new (_exceptions || _load_exceptions()).UnexpectedNetworkError(parsedError ? parsedError.message : err);
        case 127:
          return new (_componentNotFound || _load_componentNotFound()).default(parsedError && parsedError.id || err);
        case 128:
          return new (_exceptions || _load_exceptions()).PermissionDenied(`${this.host}:${this.path}`);
        case 129:
          return new (_exceptions || _load_exceptions()).RemoteScopeNotFound(parsedError && parsedError.name || err);
        case 130:
          return new (_exceptions || _load_exceptions()).PermissionDenied(`${this.host}:${this.path}`);
        case 131:
          return new (_mergeConflictOnRemote || _load_mergeConflictOnRemote()).default(parsedError && parsedError.idsAndVersions ? parsedError.idsAndVersions : []);
      }
    }
  }, {
    key: '_unpack',
    value: function _unpack(data) {
      var base64 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

      try {
        return (0, (_utils || _load_utils()).unpackCommand)(data, base64);
      } catch (err) {
        (_logger || _load_logger()).default.error(`unpackCommand found on error "${err}", while paring the following string: ${data}`);
        throw new (_exceptions || _load_exceptions()).SSHInvalidResponse(data);
      }
    }
  }, {
    key: 'pushMany',
    value: function pushMany(manyComponentObjects, context) {
      var _this2 = this;

      // This ComponentObjects.manyToString will handle all the base64 stuff so we won't send this payload
      // to the pack command (to prevent duplicate base64)
      return this.exec('_put', (_componentObjects || _load_componentObjects()).default.manyToString(manyComponentObjects), context).then(function (data) {
        var _unpack3 = _this2._unpack(data),
            payload = _unpack3.payload,
            headers = _unpack3.headers;

        checkVersionCompatibility(headers.version);
        return payload.ids;
      });
    }
  }, {
    key: 'deleteMany',
    value: function deleteMany(bitIds, force, context) {
      var _this3 = this;

      return this.exec('_delete', {
        bitIds: bitIds.map(function (id) {
          return id.toStringWithoutVersion();
        }),
        force
      }, context).then(function (data) {
        var _unpack4 = _this3._unpack(data),
            payload = _unpack4.payload;

        return Promise.resolve((_removedComponents || _load_removedComponents()).RemovedObjects.fromObjects(payload));
      });
    }
  }, {
    key: 'deprecateMany',
    value: function deprecateMany(bitIds, context) {
      var _this4 = this;

      return this.exec('_deprecate', {
        bitIds: bitIds.map(function (x) {
          return x.toStringWithoutVersion();
        })
      }, context).then(function (data) {
        var _unpack5 = _this4._unpack(data),
            payload = _unpack5.payload;

        return Promise.resolve(payload);
      });
    }
  }, {
    key: 'push',
    value: function push(componentObjects) {
      return this.pushMany([componentObjects]);
    }
  }, {
    key: 'describeScope',
    value: function describeScope() {
      var _this5 = this;

      return this.exec('_scope').then(function (data) {
        var _unpack6 = _this5._unpack(data),
            payload = _unpack6.payload,
            headers = _unpack6.headers;

        checkVersionCompatibility(headers.version);
        return payload;
      }).catch(function (err) {
        throw new (_exceptions || _load_exceptions()).RemoteScopeNotFound(_this5.path);
      });
    }
  }, {
    key: 'list',
    value: function list() {
      var _this6 = this;

      return this.exec('_list').then(function (str) {
        var _unpack7 = _this6._unpack(str),
            payload = _unpack7.payload,
            headers = _unpack7.headers;

        checkVersionCompatibility(headers.version);
        return rejectNils(payload.map(function (c) {
          return c ? (_component || _load_component()).default.fromString(c) : null;
        }));
      });
    }
  }, {
    key: 'latestVersions',
    value: function latestVersions(componentIds) {
      var _this7 = this;

      var componentIdsStr = componentIds.map(function (componentId) {
        return componentId.toString();
      });
      return this.exec('_latest', componentIdsStr).then(function (str) {
        var _unpack8 = _this7._unpack(str),
            payload = _unpack8.payload,
            headers = _unpack8.headers;

        checkVersionCompatibility(headers.version);
        return payload;
      });
    }
  }, {
    key: 'search',
    value: function search(query, reindex) {
      var _this8 = this;

      return this.exec('_search', { query, reindex: reindex.toString() }).then(function (data) {
        var _unpack9 = _this8._unpack(data),
            payload = _unpack9.payload,
            headers = _unpack9.headers;

        checkVersionCompatibility(headers.version);
        return payload;
      });
    }
  }, {
    key: 'show',
    value: function show(id) {
      var _this9 = this;

      return this.exec('_show', id.toString()).then(function (str) {
        var _unpack10 = _this9._unpack(str),
            payload = _unpack10.payload,
            headers = _unpack10.headers;

        checkVersionCompatibility(headers.version);
        return str ? (_component || _load_component()).default.fromString(payload) : null;
      });
    }
  }, {
    key: 'fetch',
    value: function fetch(ids) {
      var noDeps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
      var context = arguments[2];

      var options = '';
      ids = ids.map(function (bitId) {
        return bitId.toString();
      });
      if (noDeps) options = '--no-dependencies';
      return this.exec(`_fetch ${options}`, ids, context).then(function (str) {
        var _JSON$parse = JSON.parse(str),
            payload = _JSON$parse.payload,
            headers = _JSON$parse.headers;

        checkVersionCompatibility(headers.version);
        var componentObjects = (_componentObjects || _load_componentObjects()).default.manyFromString(payload);
        return componentObjects;
      });
    }
  }, {
    key: 'close',
    value: function close() {
      this.connection.end();
      return this;
    }
  }, {
    key: 'comoseBaseObject',
    value: function comoseBaseObject(passphrase) {
      return {
        username: this.username,
        host: this.host,
        port: this.port,
        passphrase,
        readyTimeout: (_constants || _load_constants()).DEFAULT_SSH_READY_TIMEOUT
      };
    }
  }, {
    key: 'composeTokenAuthObject',
    value: function composeTokenAuthObject() {
      var token = (0, (_globalConfig || _load_globalConfig()).getSync)((_constants || _load_constants()).CFG_USER_TOKEN_KEY);
      if (token) {
        (_logger || _load_logger()).default.debug('SSH: connecting using token');
        this._sshUsername = 'token';
        return (0, (_lodash || _load_lodash()).default)(this.comoseBaseObject(), { username: 'token', password: token });
      }
      (_logger || _load_logger()).default.debug('SSH: there is no token configured)');
    }
  }, {
    key: 'composeSshAuthObject',
    value: function composeSshAuthObject(key, skipAgent) {
      (_logger || _load_logger()).default.debug(`SSH: reading ssh key file ${key}`);
      (_logger || _load_logger()).default.debug('SSH: checking if ssh agent has socket');
      (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'reading ssh key file');
      // if ssh-agent socket exists, use it.
      if (this.hasAgentSocket() && !skipAgent) {
        (_logger || _load_logger()).default.debug('SSH: connecting using ssh agent socket');
        (_analytics || _load_analytics()).Analytics.setExtraData('authentication_method', 'ssh-agent');
        return (0, (_lodash || _load_lodash()).default)(this.comoseBaseObject(), { agent: process.env.SSH_AUTH_SOCK });
      }
      (_logger || _load_logger()).default.debug('SSH: there is no ssh agent socket (or its been disabled)');
      (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'there is no ssh agent socket (or its been disabled)');
      (_logger || _load_logger()).default.debug(`SSH: reading ssh key file ${key}`);

      // otherwise just search for merge
      var keyBuffer = (0, (_keyGetter || _load_keyGetter()).default)(key);
      if (keyBuffer) {
        (_analytics || _load_analytics()).Analytics.setExtraData('authentication_method', 'ssh-key');
        return (0, (_lodash || _load_lodash()).default)(this.comoseBaseObject(), { privateKey: keyBuffer });
      }
      (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'reading ssh key file failed');
      (_logger || _load_logger()).default.debug('SSH: reading ssh key file failed');
    }
  }, {
    key: 'composeUserPassObject',
    value: function composeUserPassObject() {
      var _this10 = this;

      return (0, (_prompts || _load_prompts()).userpass)().then(function (_ref2) {
        var username = _ref2.username,
            password = _ref2.password;

        (_analytics || _load_analytics()).Analytics.setExtraData('authentication_method', 'user_password');
        _this10._sshUsername = username;
        return (0, (_lodash || _load_lodash()).default)(_this10.comoseBaseObject(), { username, password });
      });
    }
  }, {
    key: 'hasAgentSocket',
    value: function hasAgentSocket() {
      return !!process.env.SSH_AUTH_SOCK;
    }
  }, {
    key: 'tokenAuthentication',
    value: function tokenAuthentication() {
      var _this11 = this;

      var conn = new (_ssh || _load_ssh()).default();
      return new Promise(function (resolve, reject) {
        (_analytics || _load_analytics()).Analytics.setExtraData('authentication_method', 'token');
        var sshConfig = _this11.composeTokenAuthObject();
        if (!sshConfig) reject();
        conn.on('error', function (err) {
          if (err.message === AUTH_FAILED_MESSAGE) {
            return reject(new (_exceptions || _load_exceptions()).AuthenticationFailed());
          }
          return reject(err);
        }).on('ready', function () {
          _this11.connection = conn;
          resolve(_this11);
        }).connect(sshConfig);
      });
    }
  }, {
    key: 'userPassAuthentication',
    value: function userPassAuthentication() {
      var _this12 = this;

      var conn = new (_ssh || _load_ssh()).default();
      return new Promise(function (resolve, reject) {
        return _this12.composeUserPassObject().then(function (sshConfig) {
          conn.on('error', function (err) {
            if (err.message === AUTH_FAILED_MESSAGE) {
              return reject(new (_exceptions || _load_exceptions()).AuthenticationFailed());
            }
            return reject(err);
          }).on('ready', function () {
            _this12.connection = conn;
            resolve(_this12);
          }).connect(sshConfig);
        }).catch(function (err) {
          return reject(err);
        });
      });
    }
  }, {
    key: 'sshAuthentication',
    value: function sshAuthentication(key, passphrase) {
      var _this13 = this;

      var skipAgent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

      var conn = new (_ssh || _load_ssh()).default();
      return new Promise(function (resolve, reject) {
        var sshConfig = _this13.composeSshAuthObject(key, skipAgent);
        if (!sshConfig) reject();
        conn.on('error', function (err) {
          (_logger || _load_logger()).default.debug('SSH: connection on error event');
          (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'connection on error event');
          if (_this13.hasAgentSocket() && err.message === AUTH_FAILED_MESSAGE) {
            (_logger || _load_logger()).default.debug('SSH: retry in case ssh-agent failed');
            (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'retry in case ssh-agent failed');
            // retry in case ssh-agent failed
            if (err.message === PASSPHRASE_MESSAGE) {
              (_logger || _load_logger()).default.debug('SSH: Encrypted private key detected, but no passphrase given');
              (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'Encrypted private key detected, but no passphrase given');
              if (cachedPassphrase) {
                (_logger || _load_logger()).default.debug('SSH: trying to use cached passphrase');
                return _this13.sshAuthentication(key, cachedPassphrase);
              }
              (_logger || _load_logger()).default.debug('SSH: prompt for passphrase');
              (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'prompt for passphrase');
              return (0, (_prompts || _load_prompts()).passphrase)().then(function (res) {
                cachedPassphrase = res.passphrase;
                return _this13.sshAuthentication(key, cachedPassphrase).catch(function () {
                  (_logger || _load_logger()).default.debug('SSH: connecting using passphrase failed, trying again');
                  (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'connecting using passphrase failed, trying again');
                  cachedPassphrase = undefined;
                  return _this13.sshAuthentication(key, cachedPassphrase);
                });
              });
            }
            return reject(err);
          }

          (_logger || _load_logger()).default.debug('SSH: auth failed', err);
          (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'auth failed');
          if (err.message === AUTH_FAILED_MESSAGE) {
            return reject(new (_exceptions || _load_exceptions()).AuthenticationFailed());
          }

          return reject(err);
        }).on('ready', function () {
          _this13.connection = conn;
          resolve(_this13);
        }).connect(sshConfig);
      });
    }

    // @TODO refactor this method

  }, {
    key: 'connect',
    value: function connect(key, passphrase) {
      var _this14 = this;

      (_logger || _load_logger()).default.debug('SSH: starting ssh connection process');
      (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'starting ssh connection process');
      return this.tokenAuthentication().catch(function () {
        return _this14.sshAuthentication(key, passphrase).catch(function () {
          return _this14.sshAuthentication(key, passphrase, true).catch(function () {
            return _this14.userPassAuthentication();
          });
        }).catch(function (e) {
          if (e.skip) {
            return _this14.connect(key, passphrase);
          }
          (_logger || _load_logger()).default.debug('SSH: connection failed', e);
          (_analytics || _load_analytics()).Analytics.addBreadCrumb('ssh', 'connection failed');
          if (e.code === 'ENOTFOUND') {
            throw new (_generalError || _load_generalError()).default(`unable to find the SSH server. host: ${e.host}, port: ${e.port}. Original error message: ${e.message}`);
          }

          throw e;
        });
      });
    }
  }]);

  return SSH;
}();

exports.default = SSH;