UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

540 lines (471 loc) 21.3 kB
Object.defineProperty(exports, '__esModule', { value: true }); /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })(); 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; }; })(); exports.decorateSshConnectionDelegateWithTracking = decorateSshConnectionDelegateWithTracking; function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _ConnectionTracker2; function _ConnectionTracker() { return _ConnectionTracker2 = _interopRequireDefault(require('./ConnectionTracker')); } var _ssh22; function _ssh2() { return _ssh22 = require('ssh2'); } var _fsPlus2; function _fsPlus() { return _fsPlus2 = _interopRequireDefault(require('fs-plus')); } var _net2; function _net() { return _net2 = _interopRequireDefault(require('net')); } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _RemoteConnection2; function _RemoteConnection() { return _RemoteConnection2 = require('./RemoteConnection'); } var _commonsNodeFsPromise2; function _commonsNodeFsPromise() { return _commonsNodeFsPromise2 = _interopRequireDefault(require('../../commons-node/fsPromise')); } var _commonsNodePromise2; function _commonsNodePromise() { return _commonsNodePromise2 = require('../../commons-node/promise'); } var _lookupPreferIpV62; function _lookupPreferIpV6() { return _lookupPreferIpV62 = _interopRequireDefault(require('./lookup-prefer-ip-v6')); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)(); // Sync word and regex pattern for parsing command stdout. var READY_TIMEOUT_MS = 120 * 1000; var SFTP_TIMEOUT_MS = 20 * 1000; // Automatically retry with a password prompt if existing authentication methods fail. var PASSWORD_RETRIES = 3; // Name of the saved connection profile. var SupportedMethods = Object.freeze({ SSL_AGENT: 'SSL_AGENT', PASSWORD: 'PASSWORD', PRIVATE_KEY: 'PRIVATE_KEY' }); var ErrorType = Object.freeze({ UNKNOWN: 'UNKNOWN', HOST_NOT_FOUND: 'HOST_NOT_FOUND', CANT_READ_PRIVATE_KEY: 'CANT_READ_PRIVATE_KEY', SSH_CONNECT_TIMEOUT: 'SSH_CONNECT_TIMEOUT', SSH_CONNECT_FAILED: 'SSH_CONNECT_FAILED', SSH_AUTHENTICATION: 'SSH_AUTHENTICATION', DIRECTORY_NOT_FOUND: 'DIRECTORY_NOT_FOUND', SERVER_START_FAILED: 'SERVER_START_FAILED', SERVER_VERSION_MISMATCH: 'SERVER_VERSION_MISMATCH', SFTP_TIMEOUT: 'SFTP_TIMEOUT' }); /** * The server is asking for replies to the given prompts for * keyboard-interactive user authentication. * * @param name is generally what you'd use as * a window title (for GUI apps). * @param prompts is an array of { prompt: 'Password: ', * echo: false } style objects (here echo indicates whether user input * should be displayed on the screen). * @param finish: The answers for all prompts must be provided as an * array of strings and passed to finish when you are ready to continue. Note: * It's possible for the server to come back and ask more questions. */ var SshConnectionErrorLevelMap = new Map([['client-timeout', ErrorType.SSH_CONNECT_TIMEOUT], ['client-socket', ErrorType.SSH_CONNECT_FAILED], ['protocal', ErrorType.SSH_CONNECT_FAILED], ['client-authentication', ErrorType.SSH_AUTHENTICATION], ['agent', ErrorType.SSH_AUTHENTICATION], ['client-dns', ErrorType.SSH_AUTHENTICATION]]); var SshHandshake = (function () { _createClass(SshHandshake, null, [{ key: 'ErrorType', value: ErrorType, enumerable: true }, { key: 'SupportedMethods', value: SupportedMethods, enumerable: true }]); function SshHandshake(delegate, connection) { _classCallCheck(this, SshHandshake); this._delegate = delegate; this._connection = connection ? connection : new (_ssh22 || _ssh2()).Client(); this._connection.on('ready', this._onConnect.bind(this)); this._connection.on('error', this._onSshConnectionError.bind(this)); this._connection.on('keyboard-interactive', this._onKeyboardInteractive.bind(this)); } _createClass(SshHandshake, [{ key: '_willConnect', value: function _willConnect() { this._delegate.onWillConnect(this._config); } }, { key: '_didConnect', value: function _didConnect(connection) { this._delegate.onDidConnect(connection, this._config); } }, { key: '_error', value: function _error(message, errorType, error) { logger.error('SshHandshake failed: ' + errorType + ', ' + message, error); this._delegate.onError(errorType, error, this._config); } }, { key: '_onSshConnectionError', value: function _onSshConnectionError(error) { var _this = this; var errorLevel = error.level; // Upon authentication failure, fall back to using a password. if (errorLevel === 'client-authentication' && this._passwordRetryCount < PASSWORD_RETRIES) { var _ret = (function () { var config = _this._config; var retryText = _this._passwordRetryCount ? ' again' : ''; _this._delegate.onKeyboardInteractive('', '', '', // ignored [{ prompt: 'Authentication failed. Try entering your password' + retryText + ':', echo: true }], function (_ref) { var _ref2 = _slicedToArray(_ref, 1); var password = _ref2[0]; _this._connection.connect({ host: config.host, port: config.sshPort, username: config.username, password: password, tryKeyboard: true }); }); _this._passwordRetryCount++; return { v: undefined }; })(); if (typeof _ret === 'object') return _ret.v; } var errorType = SshConnectionErrorLevelMap.get(errorLevel) || SshHandshake.ErrorType.UNKNOWN; this._error('Ssh connection failed.', errorType, error); } }, { key: 'connect', value: _asyncToGenerator(function* (config) { this._config = config; this._passwordRetryCount = 0; this._willConnect(); var existingConnection = (_RemoteConnection2 || _RemoteConnection()).RemoteConnection.getByHostnameAndPath(this._config.host, this._config.cwd); if (existingConnection) { this._didConnect(existingConnection); return; } var connection = yield (_RemoteConnection2 || _RemoteConnection()).RemoteConnection.createConnectionBySavedConfig(this._config.host, this._config.cwd, this._config.displayTitle); if (connection) { this._didConnect(connection); return; } var address = null; try { address = yield (0, (_lookupPreferIpV62 || _lookupPreferIpV6()).default)(config.host); } catch (e) { this._error('Failed to resolve DNS.', SshHandshake.ErrorType.HOST_NOT_FOUND, e); } if (config.authMethod === SupportedMethods.SSL_AGENT) { // Point to ssh-agent's socket for ssh-agent-based authentication. var agent = process.env.SSH_AUTH_SOCK; if (!agent && /^win/.test(process.platform)) { // #100: On Windows, fall back to pageant. agent = 'pageant'; } this._connection.connect({ host: address, port: config.sshPort, username: config.username, agent: agent, tryKeyboard: true, readyTimeout: READY_TIMEOUT_MS }); } else if (config.authMethod === SupportedMethods.PASSWORD) { // The user has already entered the password once. this._passwordRetryCount++; // When the user chooses password-based authentication, we specify // the config as follows so that it tries simple password auth and // failing that it falls through to the keyboard interactive path this._connection.connect({ host: address, port: config.sshPort, username: config.username, password: config.password, tryKeyboard: true }); } else if (config.authMethod === SupportedMethods.PRIVATE_KEY) { // We use fs-plus's normalize() function because it will expand the ~, if present. var expandedPath = (_fsPlus2 || _fsPlus()).default.normalize(config.pathToPrivateKey); try { var privateKey = yield (_commonsNodeFsPromise2 || _commonsNodeFsPromise()).default.readFile(expandedPath); this._connection.connect({ host: address, port: config.sshPort, username: config.username, privateKey: privateKey, tryKeyboard: true, readyTimeout: READY_TIMEOUT_MS }); } catch (e) { this._error('Failed to read private key', SshHandshake.ErrorType.CANT_READ_PRIVATE_KEY, e); } } }) }, { key: 'cancel', value: function cancel() { this._connection.end(); } }, { key: '_onKeyboardInteractive', value: function _onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish) { this._delegate.onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish); } }, { key: '_forwardSocket', value: function _forwardSocket(socket) { this._connection.forwardOut(socket.remoteAddress, socket.remotePort, 'localhost', this._remotePort, function (err, stream) { if (err) { socket.end(); logger.error(err); return; } socket.pipe(stream); stream.pipe(socket); }); } }, { key: '_updateServerInfo', value: function _updateServerInfo(serverInfo) { (0, (_assert2 || _assert()).default)(serverInfo.port); this._remotePort = serverInfo.port; this._remoteHost = '' + (serverInfo.hostname || this._config.host); // Because the value for the Initial Directory that the user supplied may have // been a symlink that was resolved by the server, overwrite the original `cwd` // value with the resolved value. (0, (_assert2 || _assert()).default)(serverInfo.workspace); this._config.cwd = serverInfo.workspace; // The following keys are optional in `RemoteConnectionConfiguration`. // // Do not throw when any of them (`ca`, `cert`, or `key`) are undefined because that will be the // case when the server is started in "insecure" mode. See `::_isSecure`, which returns the // security of this connection after the server is started. if (serverInfo.ca != null) { this._certificateAuthorityCertificate = serverInfo.ca; } if (serverInfo.cert != null) { this._clientCertificate = serverInfo.cert; } if (serverInfo.key != null) { this._clientKey = serverInfo.key; } } }, { key: '_isSecure', value: function _isSecure() { return Boolean(this._certificateAuthorityCertificate && this._clientCertificate && this._clientKey); } }, { key: '_startRemoteServer', value: _asyncToGenerator(function* () { var _this2 = this; var sftpTimer = null; return new Promise(function (resolve, reject) { var stdOut = ''; var remoteTempFile = '/tmp/nuclide-sshhandshake-' + Math.random(); // TODO: escape any single quotes // TODO: the timeout value shall be configurable using .json file too (t6904691). var cmd = _this2._config.remoteServerCommand + ' --workspace=' + _this2._config.cwd + (' --common-name=' + _this2._config.host + ' --json-output-file=' + remoteTempFile + ' -t 60'); _this2._connection.exec(cmd, { pty: { term: 'nuclide' } }, function (err, stream) { if (err) { _this2._onSshConnectionError(err); return resolve(false); } stream.on('close', _asyncToGenerator(function* (code, signal) { // Note: this code is probably the code from the child shell if one // is in use. if (code === 0) { // Some servers have max channels set to 1, so add a delay to ensure // the old channel has been cleaned up on the server. // TODO(hansonw): Implement a proper retry mechanism. // But first, we have to clean up this callback hell. yield (0, (_commonsNodePromise2 || _commonsNodePromise()).sleep)(100); sftpTimer = setTimeout(function () { _this2._error('Failed to start sftp connection', SshHandshake.ErrorType.SFTP_TIMEOUT, new Error()); sftpTimer = null; _this2._connection.end(); resolve(false); }, SFTP_TIMEOUT_MS); _this2._connection.sftp(_asyncToGenerator(function* (error, sftp) { if (sftpTimer != null) { // Clear the sftp timer once we get a response. clearTimeout(sftpTimer); } else { // If the timer already triggered, we timed out. Just exit. return; } if (error) { _this2._error('Failed to start sftp connection', SshHandshake.ErrorType.SERVER_START_FAILED, error); return resolve(false); } var localTempFile = yield (_commonsNodeFsPromise2 || _commonsNodeFsPromise()).default.tempfile(); sftp.fastGet(remoteTempFile, localTempFile, _asyncToGenerator(function* (sftpError) { sftp.end(); if (sftpError) { _this2._error('Failed to transfer server start information', SshHandshake.ErrorType.SERVER_START_FAILED, sftpError); return resolve(false); } var serverInfo = null; var serverInfoJson = yield (_commonsNodeFsPromise2 || _commonsNodeFsPromise()).default.readFile(localTempFile); try { serverInfo = JSON.parse(serverInfoJson); } catch (e) { _this2._error('Malformed server start information', SshHandshake.ErrorType.SERVER_START_FAILED, new Error(serverInfoJson)); return resolve(false); } if (!serverInfo.success) { _this2._error('Remote server failed to start', SshHandshake.ErrorType.SERVER_START_FAILED, new Error(serverInfo.logs)); return resolve(false); } if (!serverInfo.workspace) { _this2._error('Could not find directory', SshHandshake.ErrorType.DIRECTORY_NOT_FOUND, new Error(serverInfo.logs)); return resolve(false); } // Update server info that is needed for setting up client. _this2._updateServerInfo(serverInfo); return resolve(true); })); })); } else { _this2._error('Remote shell execution failed', SshHandshake.ErrorType.UNKNOWN, new Error(stdOut)); return resolve(false); } })).on('data', function (data) { stdOut += data; }); }); }); }) }, { key: '_onConnect', value: _asyncToGenerator(function* () { var _this3 = this; if (!(yield this._startRemoteServer())) { return; } var connect = _asyncToGenerator(function* (config) { var connection = null; try { connection = yield (_RemoteConnection2 || _RemoteConnection()).RemoteConnection.findOrCreate(config); } catch (e) { _this3._error('Connection check failed', SshHandshake.ErrorType.SERVER_VERSION_MISMATCH, e); } if (connection != null) { _this3._didConnect(connection); // If we are secure then we don't need the ssh tunnel. if (_this3._isSecure()) { _this3._connection.end(); } } }); // Use an ssh tunnel if server is not secure if (this._isSecure()) { (0, (_assert2 || _assert()).default)(this._remoteHost); (0, (_assert2 || _assert()).default)(this._remotePort); connect({ host: this._remoteHost, port: this._remotePort, cwd: this._config.cwd, certificateAuthorityCertificate: this._certificateAuthorityCertificate, clientCertificate: this._clientCertificate, clientKey: this._clientKey, displayTitle: this._config.displayTitle }); } else { /* $FlowIssue t9212378 */ this._forwardingServer = (_net2 || _net()).default.createServer(function (sock) { _this3._forwardSocket(sock); }).listen(0, 'localhost', function () { var localPort = _this3._getLocalPort(); (0, (_assert2 || _assert()).default)(localPort); connect({ host: 'localhost', port: localPort, cwd: _this3._config.cwd, displayTitle: _this3._config.displayTitle }); }); } }) }, { key: '_getLocalPort', value: function _getLocalPort() { return this._forwardingServer ? this._forwardingServer.address().port : null; } }, { key: 'getConfig', value: function getConfig() { return this._config; } }]); return SshHandshake; })(); exports.SshHandshake = SshHandshake; function decorateSshConnectionDelegateWithTracking(delegate) { var connectionTracker = undefined; return { onKeyboardInteractive: function onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish) { (0, (_assert2 || _assert()).default)(connectionTracker); connectionTracker.trackPromptYubikeyInput(); delegate.onKeyboardInteractive(name, instructions, instructionsLang, prompts, function (answers) { (0, (_assert2 || _assert()).default)(connectionTracker); connectionTracker.trackFinishYubikeyInput(); finish(answers); }); }, onWillConnect: function onWillConnect(config) { connectionTracker = new (_ConnectionTracker2 || _ConnectionTracker()).default(config); delegate.onWillConnect(config); }, onDidConnect: function onDidConnect(connection, config) { (0, (_assert2 || _assert()).default)(connectionTracker); connectionTracker.trackSuccess(); delegate.onDidConnect(connection, config); }, onError: function onError(errorType, error, config) { (0, (_assert2 || _assert()).default)(connectionTracker); connectionTracker.trackFailure(errorType, error); delegate.onError(errorType, error, config); } }; } // host nuclide server is running on // ssh port of host nuclide server is running on // username to authenticate as // The path to private key // Command to use to start server // Path to remote directory user should start in upon connection. // Which of the authentication methods in `SupportedMethods` to use. // for simple password-based authentication /** Invoked when server requests keyboard interaction */ /** Invoked when trying to connect */ /** Invoked when connection is sucessful */ /** Invoked when connection is fails */