"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

function _bluebird() {
  const data = require("bluebird");

  _bluebird = function () {
    return data;
  };

  return data;
}

function _ssh() {
  const data = _interopRequireDefault(require("ssh2"));

  _ssh = function () {
    return data;
  };

  return data;
}

function _ramda() {
  const data = _interopRequireDefault(require("ramda"));

  _ramda = function () {
    return data;
  };

  return data;
}

function os() {
  const data = _interopRequireWildcard(require("os"));

  os = function () {
    return data;
  };

  return data;
}

function _lodash() {
  const data = _interopRequireDefault(require("lodash.merge"));

  _lodash = function () {
    return data;
  };

  return data;
}

function _prompts() {
  const data = require("../../../prompts");

  _prompts = function () {
    return data;
  };

  return data;
}

function _keyGetter() {
  const data = _interopRequireDefault(require("./key-getter"));

  _keyGetter = function () {
    return data;
  };

  return data;
}

function _componentObjects() {
  const data = _interopRequireDefault(require("../../component-objects"));

  _componentObjects = function () {
    return data;
  };

  return data;
}

function _exceptions() {
  const data = require("../exceptions");

  _exceptions = function () {
    return data;
  };

  return data;
}

function _bitId() {
  const data = require("../../../bit-id");

  _bitId = function () {
    return data;
  };

  return data;
}

function _utils() {
  const data = require("../../../utils");

  _utils = function () {
    return data;
  };

  return data;
}

function _componentNotFound() {
  const data = _interopRequireDefault(require("../../../scope/exceptions/component-not-found"));

  _componentNotFound = function () {
    return data;
  };

  return data;
}

function _component() {
  const data = _interopRequireDefault(require("../../../consumer/component"));

  _component = function () {
    return data;
  };

  return data;
}

function _checkVersionCompatibility() {
  const data = _interopRequireDefault(require("../check-version-compatibility"));

  _checkVersionCompatibility = function () {
    return data;
  };

  return data;
}

function _logger() {
  const data = _interopRequireDefault(require("../../../logger/logger"));

  _logger = function () {
    return data;
  };

  return data;
}

function _constants() {
  const data = require("../../../constants");

  _constants = function () {
    return data;
  };

  return data;
}

function _removedComponents() {
  const data = require("../../removed-components");

  _removedComponents = function () {
    return data;
  };

  return data;
}

function _mergeConflictOnRemote() {
  const data = _interopRequireDefault(require("../../exceptions/merge-conflict-on-remote"));

  _mergeConflictOnRemote = function () {
    return data;
  };

  return data;
}

function _analytics() {
  const data = require("../../../analytics/analytics");

  _analytics = function () {
    return data;
  };

  return data;
}

function _globalConfig() {
  const data = require("../../../api/consumer/lib/global-config");

  _globalConfig = function () {
    return data;
  };

  return data;
}

function _generalError() {
  const data = _interopRequireDefault(require("../../../error/general-error"));

  _generalError = function () {
    return data;
  };

  return data;
}

function _customError() {
  const data = _interopRequireDefault(require("../../../error/custom-error"));

  _customError = function () {
    return data;
  };

  return data;
}

function _remoteResolverError() {
  const data = _interopRequireDefault(require("../exceptions/remote-resolver-error"));

  _remoteResolverError = function () {
    return data;
  };

  return data;
}

function _exportAnotherOwnerPrivate() {
  const data = _interopRequireDefault(require("../exceptions/export-another-owner-private"));

  _exportAnotherOwnerPrivate = function () {
    return data;
  };

  return data;
}

const checkVersionCompatibility = _ramda().default.once(_checkVersionCompatibility().default);

const PASSPHRASE_MESSAGE = 'Encrypted private key detected, but no passphrase given';
const AUTH_FAILED_MESSAGE = 'All configured authentication methods failed';
const PASSPHRASE_POSSIBLY_MISSING_MESSAGE = 'Cannot parse privateKey: Unsupported key format';
const cachedPassphrase = null;

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

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

class SSH {
  // Username entered by the user on the prompt user/pass process
  constructor({
    path,
    username,
    port,
    host
  }) {
    this.path = path;
    this.username = username;
    this.port = port;
    this.host = host || '';
  }
  /**
   * strategies:
   * 1) token (generated by bit-login command)
   * 2) ssh-agent (public-key should be saved on bitsrc, user needs to enable ssh-agent in its os. the agent saves the passphrase, so no need to enter)
   * 3) ssh-key. (user can specify location by `bit config`, if not, the default one is used. doesn't support passphrase)
   * 4) prompt of user/password
   */


  connect() {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      const strategies = {
        token: _this._tokenAuthentication,
        'ssh-agent': _this._sshAgentAuthentication,
        'ssh-key': _this._sshKeyAuthentication,
        'user-password': _this._userPassAuthentication
      };
      const strategiesNames = Object.keys(strategies);
      const strategiesFailures = [];

      for (const strategyName of strategiesNames) {
        _logger().default.debug(`ssh, trying to connect using ${strategyName}`);

        const strategyFunc = strategies[strategyName].bind(_this);

        try {
          const strategyResult = yield strategyFunc(); // eslint-disable-line

          if (strategyResult) return strategyResult;
        } catch (err) {
          _logger().default.debug(`ssh, failed to connect using ${strategyName}. ${err.message}`);

          if (err instanceof AuthenticationStrategyFailed) {
            strategiesFailures.push(err.message);
          } else {
            throw err;
          }
        }
      }

      _logger().default.errorAndAddBreadCrumb('ssh', 'all connection strategies have been failed!');

      strategiesFailures.unshift('The following strategies were failed');
      throw new (_exceptions().AuthenticationFailed)(strategiesFailures.join('\n[-] '));
    })();
  }

  _tokenAuthentication() {
    var _this2 = this;

    return (0, _bluebird().coroutine)(function* () {
      const sshConfig = _this2._composeTokenAuthObject();

      if (!sshConfig) {
        throw new AuthenticationStrategyFailed('user token not defined in bit-config. please run `bit login` to authenticate.');
      }

      const authFailedMsg = 'failed to authenticate with user token. generate a new token by running `bit logout && bit login`.';
      return _this2._connectWithConfig(sshConfig, 'token', authFailedMsg);
    })();
  }

  _sshAgentAuthentication() {
    var _this3 = this;

    return (0, _bluebird().coroutine)(function* () {
      if (!_this3._hasAgentSocket()) {
        throw new AuthenticationStrategyFailed('unable to get SSH keys from ssh-agent to. perhaps service is down or disabled.');
      }

      const sshConfig = (0, _lodash().default)(_this3._composeBaseObject(), {
        agent: process.env.SSH_AUTH_SOCK
      });
      const authFailedMsg = 'no matching private key found in ssh-agent to authenticate to remote server.';
      return _this3._connectWithConfig(sshConfig, 'ssh-agent', authFailedMsg);
    })();
  }

  _sshKeyAuthentication() {
    var _this4 = this;

    return (0, _bluebird().coroutine)(function* () {
      const keyBuffer = yield (0, _keyGetter().default)();

      if (!keyBuffer) {
        throw new AuthenticationStrategyFailed('SSH key not found in `~/.ssh/id_rsa` or `ssh_key_file` config in `bit config` either not configured or refers to wrong path.');
      }

      const sshConfig = (0, _lodash().default)(_this4._composeBaseObject(), {
        privateKey: keyBuffer
      });
      const authFailedMsg = 'failed connecting to remote server using `~/.ssh/id_rsa` or `ssh_key_file` in `bit config`.';
      return _this4._connectWithConfig(sshConfig, 'ssh-key', authFailedMsg);
    })();
  }

  _userPassAuthentication() {
    var _this5 = this;

    return (0, _bluebird().coroutine)(function* () {
      const sshConfig = yield _this5._composeUserPassObject();
      const authFailedMsg = 'unable to connect using provided username and password combination.';
      return _this5._connectWithConfig(sshConfig, 'user-password', authFailedMsg);
    })();
  }

  close() {
    this.connection.end();
    return this;
  }

  _composeBaseObject(passphrase) {
    return {
      username: this.username,
      host: this.host,
      port: this.port,
      passphrase,
      readyTimeout: _constants().DEFAULT_SSH_READY_TIMEOUT
    };
  }

  _composeTokenAuthObject() {
    const token = (0, _globalConfig().getSync)(_constants().CFG_USER_TOKEN_KEY);

    if (token) {
      this._sshUsername = 'token';
      return (0, _lodash().default)(this._composeBaseObject(), {
        username: 'token',
        password: token
      });
    }

    return null;
  }

  _composeUserPassObject() {
    return (0, _prompts().userpass)().then(({
      username,
      password
    }) => {
      _analytics().Analytics.setExtraData('authentication_method', 'user_password');

      this._sshUsername = username;
      return (0, _lodash().default)(this._composeBaseObject(), {
        username,
        password
      });
    });
  }

  _hasAgentSocket() {
    return !!process.env.SSH_AUTH_SOCK;
  }

  _connectWithConfig(sshConfig, authenticationType, authFailedMsg) {
    var _this6 = this;

    return (0, _bluebird().coroutine)(function* () {
      const connectWithConfigP = () => {
        const conn = new (_ssh().default)();
        return new Promise((resolve, reject) => {
          conn.on('error', err => {
            reject(err);
          }).on('ready', () => {
            resolve(conn);
          }).connect(sshConfig);
        });
      };

      try {
        _this6.connection = yield connectWithConfigP();

        _analytics().Analytics.setExtraData('authentication_method', authenticationType);

        _logger().default.debug(`ssh, authenticated successfully using ${authenticationType}`);

        return _this6;
      } catch (err) {
        if (err.message === AUTH_FAILED_MESSAGE) {
          throw new AuthenticationStrategyFailed(authFailedMsg);
        }

        _logger().default.error(err);

        if (err.code === 'ENOTFOUND') {
          throw new (_generalError().default)(`unable to find the SSH server. host: ${err.host}, port: ${err.port}. Original error message: ${err.message}`);
        }

        if (err.message === PASSPHRASE_POSSIBLY_MISSING_MESSAGE) {
          const macMojaveOs = process.platform === 'darwin' && os().release() === '18.2.0';
          let passphrasePossiblyMissing = 'error connecting with private ssh key. in case passphrase is used, use ssh-agent.';

          if (macMojaveOs) {
            passphrasePossiblyMissing += ' for macOS Mojave users, use `-m PEM` for `ssh-keygen` command to generate a valid SSH key';
          }

          throw new AuthenticationStrategyFailed(passphrasePossiblyMissing);
        }

        throw new AuthenticationStrategyFailed(`${authFailedMsg} due to an error "${err.message}"`);
      }
    })();
  }

  buildCmd(commandName, path, payload, context) {
    return `bit ${commandName} ${(0, _utils().toBase64)(path)} ${(0, _utils().packCommand)((0, _utils().buildCommandMessage)(payload, context))}`;
  }

  exec(commandName, payload, context) {
    _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((resolve, reject) => {
      let res = '';
      let err; // 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

      const cmd = this.buildCmd(commandName, absolutePath(this.path || ''), commandName === '_put' ? null : payload, context);

      if (!this.connection) {
        err = 'ssh connection is not defined';

        _logger().default.error(err);

        return reject(err);
      }

      this.connection.exec(cmd, (error, stream) => {
        if (error) {
          _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', response => {
          res += response.toString();
        }).on('exit', code => {
          _logger().default.debug(`ssh: exit. Exit code: ${code}`);

          const 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', (code, signal) => {
          if (commandName === '_put') res = res.replace(payload, '');

          _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', response => {
          err = response.toString();

          _logger().default.error(`ssh: got an error, ${err}`);
        });
      });
    });
  }

  errorHandler(code, err) {
    let parsedError;

    try {
      const {
        headers,
        payload
      } = this._unpack(err, false);

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

    switch (code) {
      default:
        return new (_exceptions().UnexpectedNetworkError)(parsedError ? parsedError.message : err);

      case 127:
        return new (_componentNotFound().default)(parsedError && parsedError.id || err);

      case 128:
        return new (_exceptions().PermissionDenied)(`${this.host}:${this.path}`);

      case 129:
        return new (_exceptions().RemoteScopeNotFound)(parsedError && parsedError.name || err);

      case 130:
        return new (_exceptions().PermissionDenied)(`${this.host}:${this.path}`);

      case 131:
        return new (_mergeConflictOnRemote().default)(parsedError && parsedError.idsAndVersions ? parsedError.idsAndVersions : []);

      case 132:
        return new (_customError().default)(parsedError && parsedError.message ? parsedError.message : err);

      case 133:
        return new (_exceptions().OldClientVersion)(parsedError && parsedError.message ? parsedError.message : err);

      case 134:
        {
          const msg = parsedError && parsedError.message ? parsedError.message : err;
          const sourceScope = parsedError && parsedError.sourceScope ? parsedError.sourceScope : 'unknown';
          const destinationScope = parsedError && parsedError.destinationScope ? parsedError.destinationScope : 'unknown';
          return new (_exportAnotherOwnerPrivate().default)(msg, sourceScope, destinationScope);
        }
    }
  }

  _unpack(data, base64 = true) {
    try {
      return (0, _utils().unpackCommand)(data, base64);
    } catch (err) {
      _logger().default.error(`unpackCommand found on error "${err}", while paring the following string: ${data}`);

      throw new (_exceptions().SSHInvalidResponse)(data);
    }
  }

  pushMany(manyComponentObjects, context) {
    // 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().default.manyToString(manyComponentObjects), context).then(data => {
      const {
        payload,
        headers
      } = this._unpack(data);

      checkVersionCompatibility(headers.version);
      return payload.ids;
    });
  }

  deleteMany(ids, force, context) {
    return this.exec('_delete', {
      bitIds: ids,
      force
    }, context).then(data => {
      const {
        payload
      } = this._unpack(data);

      return Promise.resolve(_removedComponents().RemovedObjects.fromObjects(payload));
    });
  }

  deprecateMany(ids, context) {
    return this.exec('_deprecate', {
      ids
    }, context).then(data => {
      const {
        payload
      } = this._unpack(data);

      return Promise.resolve(payload);
    });
  }

  undeprecateMany(ids, context) {
    return this.exec('_undeprecate', {
      ids
    }, context).then(data => {
      const {
        payload
      } = this._unpack(data);

      return Promise.resolve(payload);
    });
  }

  push(componentObjects) {
    return this.pushMany([componentObjects]);
  }

  describeScope() {
    return this.exec('_scope').then(data => {
      const {
        payload,
        headers
      } = this._unpack(data);

      checkVersionCompatibility(headers.version);
      return payload;
    }).catch(err => {
      throw new (_exceptions().RemoteScopeNotFound)(this.path);
    });
  }

  list(namespacesUsingWildcards) {
    var _this7 = this;

    return (0, _bluebird().coroutine)(function* () {
      return _this7.exec('_list', namespacesUsingWildcards).then(
      /*#__PURE__*/
      function () {
        var _ref = (0, _bluebird().coroutine)(function* (str) {
          const {
            payload,
            headers
          } = _this7._unpack(str);

          checkVersionCompatibility(headers.version);
          payload.forEach(result => {
            result.id = new (_bitId().BitId)(result.id);
          });
          return payload;
        });

        return function (_x) {
          return _ref.apply(this, arguments);
        };
      }());
    })();
  }

  latestVersions(componentIds) {
    const componentIdsStr = componentIds.map(componentId => componentId.toString());
    return this.exec('_latest', componentIdsStr).then(str => {
      const {
        payload,
        headers
      } = this._unpack(str);

      checkVersionCompatibility(headers.version);
      return payload;
    });
  }

  search(query, reindex) {
    return this.exec('_search', {
      query,
      reindex: reindex.toString()
    }).then(data => {
      const {
        payload,
        headers
      } = this._unpack(data);

      checkVersionCompatibility(headers.version);
      return payload;
    });
  }

  show(id) {
    return this.exec('_show', id.toString()).then(str => {
      const {
        payload,
        headers
      } = this._unpack(str);

      checkVersionCompatibility(headers.version);
      return str ? _component().default.fromString(payload) : null;
    });
  }

  fetch(ids, noDeps = false, context) {
    let options = '';
    const idsStr = ids.serialize();
    if (noDeps) options = '--no-dependencies';
    return this.exec(`_fetch ${options}`, idsStr, context).then(str => {
      const parseResponse = () => {
        try {
          const results = JSON.parse(str);
          return results;
        } catch (err) {
          throw new (_exceptions().SSHInvalidResponse)(str);
        }
      };

      const {
        payload,
        headers
      } = parseResponse();
      checkVersionCompatibility(headers.version);

      const componentObjects = _componentObjects().default.manyFromString(payload);

      return componentObjects;
    });
  }

}

exports.default = SSH;

class AuthenticationStrategyFailed extends Error {}