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
JavaScript
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 */