UNPKG

@thinremote/thinr-cli

Version:

CLI for ThinRemote - Remote management for IoT devices

311 lines (273 loc) 10.5 kB
import chalk from 'chalk'; import open, { apps } from 'open'; import ora from 'ora'; import { configExists, readConfig } from './config.js'; import api from './api.js'; /** * Parse target argument into address and port * @param {string} target - Target in format 'address:port', 'port', 'address', or 'protocol://hostname_or_ip:port' * @param {number} defaultPort - Default port if not specified * @returns {Object} Parsed address, port, and secure flag */ function parseTarget(target, defaultPort) { if (!target) { return { address: 'localhost', port: defaultPort, isSecure: false }; } // Check if target is just a number (port only) if (/^\d+$/.test(target)) { return { address: 'localhost', port: parseInt(target), isSecure: false }; } let isSecure = false; let cleanTarget = target; // Check if target starts with a protocol (full URL format) if (target.startsWith('https://')) { isSecure = true; cleanTarget = target.replace('https://', ''); } else if (target.startsWith('http://')) { isSecure = false; cleanTarget = target.replace('http://', ''); } // Remove any path/query/fragment from the URL (everything after the first slash) const slashIndex = cleanTarget.indexOf('/'); if (slashIndex !== -1) { cleanTarget = cleanTarget.substring(0, slashIndex); } // Parse the remaining part (hostname_or_ip:port or just hostname_or_ip) let address; let port; if (cleanTarget.includes(':')) { // Format: hostname_or_ip:port const lastColonIndex = cleanTarget.lastIndexOf(':'); address = cleanTarget.substring(0, lastColonIndex); const portStr = cleanTarget.substring(lastColonIndex + 1); // Check if the part after the last colon is actually a port number const parsedPort = parseInt(portStr); if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort <= 65535) { port = parsedPort; } else { // Not a valid port, treat the whole thing as address address = cleanTarget; port = defaultPort; } } else { // Format: just hostname_or_ip address = cleanTarget; port = defaultPort; } // If we detected a protocol but no explicit port, use protocol defaults if ((target.startsWith('https://') || target.startsWith('http://')) && port === defaultPort) { if (isSecure && defaultPort !== 443) { port = 443; // Default HTTPS port } else if (!isSecure && defaultPort !== 80) { port = 80; // Default HTTP port } } return { address: address || 'localhost', port: port, isSecure }; } /** * Create proxy action handler */ export async function handleProxyAction(deviceId, protocol, target, options) { // Check if configured if (!configExists()) { console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.')); process.exit(1); } // Determine default port based on protocol const defaultPort = protocol === 'http' ? 80 : (protocol === 'tls' ? 443 : 22); const { address, port, isSecure } = parseTarget(target, defaultPort); // Determine proxy configuration based on protocol let proxyConfig = { targetAddress: address, targetPort: port, serverPort: options.port ? parseInt(options.port) : null, targetSecure: false, serverSecure: false, web: false }; switch (protocol) { case 'tcp': // TCP proxy without TLS proxyConfig.serverSecure = false; break; case 'tls': // TLS proxy proxyConfig.targetSecure = true; proxyConfig.serverSecure = true; break; case 'http': // HTTP proxy proxyConfig.web = true; proxyConfig.serverSecure = true; // Default to HTTPS for web interface // Use the isSecure flag from parseTarget proxyConfig.targetSecure = isSecure || port === 443; break; } const proxyType = proxyConfig.web ? 'HTTP proxy' : `${protocol.toUpperCase()} proxy`; const spinner = ora(`Creating ${proxyType} to ${deviceId} (${address}:${port})...`).start(); let proxyId = null; try { // Create proxy const proxy = await createProxy(deviceId, proxyConfig); proxyId = proxy.proxyId; if (proxyConfig.web) { // For HTTP proxy, show target as URL format const targetProtocol = isSecure ? 'https' : 'http'; const targetUrl = `${targetProtocol}://${address}${port === 80 || port === 443 ? '' : `:${port}`}`; spinner.succeed(`HTTP proxy running at ${chalk.blue(proxy.url)}${deviceId} -> ${chalk.cyan(targetUrl)}`); // Open browser if not disabled if (options.openBrowser !== false) { await openBrowser(proxy.url); } } else { // For TCP/TLS proxy, show address:port format const secureInfo = isSecure ? ' [TLS]' : ''; spinner.succeed(`${protocol.toUpperCase()} proxy running on ${chalk.blue(proxy.serverHost + ':' + proxy.serverPort)}${deviceId} (${address}:${port})${secureInfo}`); } console.log(chalk.gray(`Press Ctrl+C to stop`)); // Handle process termination process.on('SIGINT', async () => { console.log(chalk.yellow('\nStopping proxy...')); if (proxyId) { await deleteProxy(proxyId); } process.exit(0); }); // Keep the process running process.stdin.resume(); } catch (error) { spinner.fail(`Failed to create ${proxyType}`); console.error(chalk.red(`Error: ${error.message}`)); // Clean up if needed if (proxyId) { await deleteProxy(proxyId); } process.exit(1); } } /** * Generate a random port number within a range * @param {number} min - Minimum port number * @param {number} max - Maximum port number * @returns {number} Random port number */ function getRandomPort(min = 50000, max = 51000) { return Math.floor(Math.random() * (max - min + 1)) + min; } /** * Create a proxy to a device * @param {string} deviceId - Device ID * @param {Object} options - Proxy options * @returns {Promise<Object>} Proxy information */ async function createProxy(deviceId, options) { // Get configuration const config = readConfig(); if (!config.token || !config.server || !config.username) { throw new Error('Not configured. Run thinr without parameters to set up.'); } const protocol = options.web ? 'http_iotmp' : 'tcp_iotmp'; const targetAddress = options.targetAddress || 'localhost'; const targetPort = options.targetPort || (options.web ? 80 : 22); const serverPort = options.serverPort || getRandomPort(); const serverSecure = options.serverSecure || (!!options.web); const targetSecure = options.targetSecure || false; // Create a unique proxy ID based on device, protocol and a timestamp const timestamp = new Date().getTime().toString(36); const proxyId = `${protocol}_${deviceId.slice(0,12)}_${timestamp}`; try { // Create proxy const response = await api.post( `/v1/proxies`, { enabled: true, config: { target: { type: 'address', user: config.username, device: deviceId, address: targetAddress, port: targetPort, secure: targetSecure }, protocol: protocol, source: { port: serverPort, secure: serverSecure } }, proxy: proxyId, name: options.web ? `Web access for ${deviceId}` : `TCP proxy for ${deviceId}`, description: `${options.web ? 'Web interface' : 'TCP proxy'} for ${deviceId}:${targetPort} created by ThingR CLI` }, { headers: { 'Content-Type': 'application/json' } } ); // Determine the URL based on protocol and SSL setting const scheme = serverSecure ? 'https' : 'http'; const url = options.web ? `${scheme}://${config.server}:${serverPort}` : null; return { proxyId: proxyId, url: url, serverPort: serverPort, serverHost: config.server, targetAddress: targetAddress, targetPort: targetPort, protocol: protocol, serverSecure: serverSecure, targetSecure: targetSecure }; } catch (error) { if (error.response) { if (error.response.status === 401) { throw new Error('Unauthorized. Your token may have expired. Please reconfigure.'); } else { throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`); } } else if (error.request) { throw new Error('No response from server. Please check your connection.'); } else { throw new Error(`Error: ${error.message}`); } } } /** * Delete a proxy * @param {string} proxyId - Proxy ID to delete */ async function deleteProxy(proxyId) { // Get configuration const config = readConfig(); if (!config.token || !config.server) { return; // Silently fail on config issues during cleanup } try { await api.delete( `/v1/proxies/${proxyId}`, ); return true; } catch (error) { console.error(chalk.red(`Error deleting proxy: ${error.message}`)); return false; } } /** * Open web browser to the proxy URL * @param {string} url - URL to open */ async function openBrowser(url) { try { await open(url, {app: [{name: apps.browser}, 'firefox-developer-edition']}); return true; } catch (error) { console.error(chalk.red(`Error opening browser: ${error.message}`)); return false; } }