UNPKG

alwaysai

Version:

The alwaysAI command-line interface (CLI)

318 lines 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkDeviceAgentSecureTunnelCompatibility = exports.addHttpProxies = exports.updatePorts = exports.constructShadow = exports.deviceConnect = void 0; const alwayscli_1 = require("@alwaysai/alwayscli"); const chalk = require("chalk"); const cli_inputs_1 = require("../../cli-inputs"); const user_1 = require("../../components/user"); const util_1 = require("../../util"); const secure_tunnel_1 = require("../../util/secure-tunnel"); const list_1 = require("./list"); const shadows_1 = require("../../util/shadows"); const semver_1 = require("semver"); const logSymbols = require("log-symbols"); // ---------------------------------------------------------------------------- // Local functions // ---------------------------------------------------------------------------- /** * Gets list of connected devices (uuid and device friendly name only) * @param {list} deviceList - list connected devices * @returns {ConnectedDevice[]} filters out disconnected devices and returns a list of connected devices */ const ipPortRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):([0-9]{1,5})$/; function getConnectedDevices(deviceList) { const connectedDevices = deviceList.filter((device) => device.status === 'connected'); return connectedDevices.map((device) => ({ uuid: device.uuid, friendlyName: device.friendly_name })); } /** * Prompts user to select the device to connect to * @param {ConnectedDevice[]} connectedDevices - list of connected devices * @returns {string} uuid of the selected device */ async function selectDeviceToConnect(connectedDevices) { const choices = [{ title: 'None', value: '' }]; connectedDevices.forEach((device) => { choices.push({ title: `${device.friendlyName}\t(${device.uuid})`, value: `${device.uuid}` }); }); const choice = await (0, util_1.promptForInput)({ purpose: 'to select target device to connect', questions: [ { type: 'select', name: 'targetDevice', message: chalk.yellowBright.bold('Select device to ssh to (disconnected devices are not listed)'), initial: 0, choices } ] }); return choice.targetDevice; } /** * Prompts user to enter user name for the remote host * @returns {string} user name for the remote host */ async function getRemoteUserLogin() { const remoteUserLoginAnswer = await (0, util_1.promptForInput)({ purpose: 'to set a remote user login', questions: [ { type: 'text', name: 'login', message: chalk.yellowBright.bold('Enter a remote user login') } ] }); return remoteUserLoginAnswer.login; } // ---------------------------------------------------------------------------- // Main function // ---------------------------------------------------------------------------- exports.deviceConnect = (0, alwayscli_1.CliLeaf)({ name: 'connect', description: 'Connect to remote devices', namedInputs: { yes: cli_inputs_1.yesCliInput, device: (0, alwayscli_1.CliStringInput)({ description: 'Device UUID' }), 'local-port': (0, alwayscli_1.CliNumberInput)({ description: 'Local port to use for connection' }), proxy: (0, alwayscli_1.CliStringInput)({ description: 'HTTP proxy in the format [ip:port]. Example: 100.70.31.118:80' }), username: (0, alwayscli_1.CliStringInput)({ description: 'Device username' }) }, hidden: true, async action(_, opts) { const { yes, device, username, proxy } = opts; const localPort = opts['local-port']; const deletedPorts = (0, secure_tunnel_1.secureTunnelJsonFile)().removeExpiredItems(); await (0, secure_tunnel_1.removeLocalhostPortsFromKnownHostsFile)(deletedPorts); if (yes) { if (device === undefined) { throw new alwayscli_1.CliUsageError((0, util_1.RequiredWithYesMessage)('device', undefined)); } if (username === undefined) { throw new alwayscli_1.CliUsageError((0, util_1.RequiredWithYesMessage)('username', undefined)); } } if (proxy !== undefined && !ipPortRegex.test(proxy)) { throw new alwayscli_1.CliUsageError('Invalid IP:Port format. Please enter a valid IP:Port'); } await (0, user_1.checkUserIsLoggedInComponent)({ yes }); let selectedDeviceUuid = device; if (selectedDeviceUuid === undefined) { const deviceList = await (0, list_1.getDeviceList)(); util_1.logger.debug(JSON.stringify(deviceList, null, 4)); if (deviceList.length === 0) { return (0, util_1.echo)(chalk.redBright.bold('No devices found')); } const connectedDevices = getConnectedDevices(deviceList); util_1.logger.debug(`connectedDevices = ${JSON.stringify(connectedDevices, null, 4)}`); if (connectedDevices.length === 0) { return (0, util_1.echo)(chalk.redBright.bold('No connected devices found')); } selectedDeviceUuid = await selectDeviceToConnect(connectedDevices); } util_1.logger.debug(`selectedDevice = ${selectedDeviceUuid}`); if (selectedDeviceUuid === '') { return (0, util_1.echo)(chalk.redBright.bold('No device selected')); } await checkDeviceAgentSecureTunnelCompatibility(selectedDeviceUuid, 'ssh'); //gather http ports let httpPorts = []; if (proxy === undefined) { if (!yes) { httpPorts = await promptForHttpPortProxy(); } } else { httpPorts.push({ enabled: true, type: 'HTTP', ip: proxy.split(':')[0], port: parseInt(proxy.split(':')[1]) }); } if (httpPorts.length > 0) { await checkDeviceAgentSecureTunnelCompatibility(selectedDeviceUuid, 'portProxy'); } const yesProxys = httpPorts.length > 0; const requiredProxys = yesProxys ? { SSH: 1, HTTP: 1 } : { SSH: 1, HTTP: 0 }; if (yesProxys) { const { payload } = await constructShadow(selectedDeviceUuid, httpPorts); const updatedShadow = await (0, shadows_1.updateShadow)(selectedDeviceUuid, payload); util_1.logger.debug(`updatedShadow = ${JSON.stringify(updatedShadow, null, 4)}`); } const openTunnelData = yesProxys ? await (0, secure_tunnel_1.getSourceAccessToken)(selectedDeviceUuid, requiredProxys) : await (0, secure_tunnel_1.getSourceAccessToken)(selectedDeviceUuid); util_1.logger.debug(`openTunnelData = ${JSON.stringify(openTunnelData, null, 4)}`); const secureTunnelInfoFromAWS = (0, secure_tunnel_1.processOpenTunnelData)(openTunnelData, selectedDeviceUuid, yesProxys, localPort ? (_) => { return localPort; } : undefined); if (secureTunnelInfoFromAWS.sourceAccessToken.length > 0) { const remoteUserLogin = username ? username : await getRemoteUserLogin(); util_1.logger.debug(`remoteUserLogin = ${remoteUserLogin}`); (0, secure_tunnel_1.secureTunnelJsonFile)().setItem(selectedDeviceUuid, secureTunnelInfoFromAWS); const startParams = { deviceUuid: selectedDeviceUuid, imageName: (0, secure_tunnel_1.getDockerImageName)(), sshPort: secureTunnelInfoFromAWS.sshPort, httpPorts, sourceAccessToken: secureTunnelInfoFromAWS.sourceAccessToken }; await (0, secure_tunnel_1.startDockerLocalProxyContainer)(startParams); await (0, secure_tunnel_1.startSecureTunnelSshSession)(remoteUserLogin, secureTunnelInfoFromAWS.sshPort); await (0, secure_tunnel_1.removeLocalhostPortsFromKnownHostsFile)([ secureTunnelInfoFromAWS.sshPort ]); await (0, secure_tunnel_1.stopDockerLocalProxyContainer)(selectedDeviceUuid); if (proxy || yesProxys) { const { payload } = await constructShadow(selectedDeviceUuid, httpPorts); await disableAllPortsInShadow(payload.state.desired.st_ports, selectedDeviceUuid); } } } }); async function promptForHttpPortProxy() { const answers = await (0, util_1.promptForInput)({ purpose: 'for http proxy initialization', questions: [ { type: 'confirm', name: 'value', message: chalk.yellowBright.bold('Add HTTP port proxy?'), initial: false }, { type: (prev) => (prev ? 'text' : null), name: 'port', message: chalk.yellowBright.bold('Enter the proxy IP address:port'), validate: (value) => { return ipPortRegex.test(value) ? true : 'Invalid IP:Port format. Please enter a valid IP:Port'; } } ] }); const httpPorts = []; if (answers.port) { httpPorts.push({ enabled: true, type: 'HTTP', ip: answers.port.split(':')[0], port: parseInt(answers.port.split(':')[1]) }); } return httpPorts; } async function constructShadow(selectedDeviceUuid, httpPorts) { var _a; const shadow = await (0, shadows_1.getShadow)(selectedDeviceUuid); let httpPortsOutput = []; if (((_a = shadow.payload.state) === null || _a === void 0 ? void 0 : _a.reported) && Object.keys(shadow.payload.state.reported).length > 0) { const { reported } = shadow.payload.state; // eslint-disable-next-line prefer-const let { foundHttp, httpPortsInput } = updatePorts(reported, httpPorts); //if the exact same http proxy is not found, add it if (!foundHttp) { httpPortsInput = addHttpProxies(httpPorts, httpPortsInput); } httpPortsOutput = [...httpPortsInput]; } else { httpPortsOutput = [ ...httpPorts, { enabled: true, type: 'SSH', ip: '0.0.0.0', port: 22 } ]; } const payload = Object.assign({ state: { desired: { st_ports: httpPortsOutput } } }, (shadow && { version: shadow.payload.version })); return { payload }; } exports.constructShadow = constructShadow; async function disableAllPortsInShadow(stPorts, selectedDeviceUuid) { const shadow = await (0, shadows_1.getShadow)(selectedDeviceUuid); const disabledPorts = stPorts.map((port) => { port.enabled = false; return port; }); const payload = Object.assign({ state: { desired: { st_ports: disabledPorts } } }, (shadow.payload.code === shadows_1.HttpStatusCode.OK && { version: shadow.payload.version })); await (0, shadows_1.updateShadow)(selectedDeviceUuid, payload); } function updatePorts(reported, httpPorts) { let found = false; const httpPortsInput = ((reported === null || reported === void 0 ? void 0 : reported.st_ports) || []).map((port) => { httpPorts.forEach((ip) => { if (port.ip === ip.ip && port.port === ip.port) { port.enabled = true; found = true; } else if (port.type === 'HTTP' && port.enabled === true) { port.enabled = false; } }); if (port.type === 'SSH') { port.enabled = true; } return port; }); return { httpPortsInput, foundHttp: found }; } exports.updatePorts = updatePorts; function addHttpProxies(httpPorts, httpPortsInput) { httpPorts.forEach((ip) => { const proxy = { enabled: true, type: 'HTTP', ip: ip.ip, port: ip.port }; httpPortsInput.push(proxy); }); return httpPortsInput; } exports.addHttpProxies = addHttpProxies; async function checkDeviceAgentSecureTunnelCompatibility(thingId, feature) { var _a; const shadow = await (0, shadows_1.getSystemInfoShadow)(thingId); if ((_a = shadow.versions) === null || _a === void 0 ? void 0 : _a.agent) { const agentVersion = shadow.versions.agent; if (!IsFeatureSupported(SecureTunnelFeatureVersion[feature], agentVersion)) { throw new alwayscli_1.CliUsageError(`The device agent version ${agentVersion} is not compatible with the feature: ${feature}. Please update the device agent to the minimum version ${SecureTunnelFeatureVersion[feature]}.`); } } else { (0, util_1.echo)(`${logSymbols.warning} Cannot determine ${feature} support for device ${thingId}. Ensure your CLI and Device Agent are updated to the latest versions if connection fails.`); } } exports.checkDeviceAgentSecureTunnelCompatibility = checkDeviceAgentSecureTunnelCompatibility; var SecureTunnelFeatureVersion; (function (SecureTunnelFeatureVersion) { SecureTunnelFeatureVersion["ssh"] = "0.2.0"; SecureTunnelFeatureVersion["portProxy"] = "1.4.0"; })(SecureTunnelFeatureVersion || (SecureTunnelFeatureVersion = {})); function IsFeatureSupported(featureVersion, currentVersion) { if (!featureVersion || !currentVersion) { return false; } return (0, semver_1.gte)(currentVersion, featureVersion); } //# sourceMappingURL=connect.js.map