alwaysai
Version:
The alwaysAI command-line interface (CLI)
318 lines • 13.8 kB
JavaScript
;
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