UNPKG

@cloud-cli/cli

Version:

CLI for the Cloud CLI project

142 lines (141 loc) 5.81 kB
import { readFile } from 'node:fs/promises'; import { createServer } from 'node:http'; import { validateKey } from './authorization.js'; import { getConfig } from './configuration.js'; import { events } from './constants.js'; import { Logger } from './logger.js'; async function getClientJs(request) { const file = import.meta.resolve('./clients/fetch.mjs').slice(7); const source = await readFile(file, 'utf-8'); return source.replace('__API_BASEURL__', 'https://' + String(request.headers['x-forwarded-for'])); } export class HttpServer { constructor(commands, settings) { this.commands = commands; this.settings = settings; this.serverParams = { run: (commandName, args) => this.run(commandName, args), }; } async handleRequest(request, response) { if (request.method === 'GET' && request.url === '/index.mjs') { response.writeHead(200, { 'Content-Type': 'text/javascript', 'Access-Control-Allow-Origin': '*', }); response.end(await getClientJs(request)); return; } if (request.method === 'GET' && request.url === '/:log-stream') { if (!validateKey(request, response, this.settings)) { return; } response.setHeader('Cache-Control', 'no-store'); response.setHeader('Content-Type', 'text/event-stream'); const onLog = (log) => { response.write('event: log'); response.write('data: ' + log + '\n\n'); }; events.on('log', onLog); response.on('close', () => events.off('log', onLog)); response.on('error', () => events.off('log', onLog)); return; } if (request.method !== 'POST') { response.writeHead(405, 'Invalid method'); response.end(); return; } if (!validateKey(request, response, this.settings)) { return; } const [command, functionName] = String(request.url).slice(1).split('.'); if (!command && functionName === 'help') { this.writeAvailableCommands(response); return; } const functionMap = this.commands.map.get(command); if (!this.isValidCommand(functionMap, command, functionName)) { Logger.debug(`Invalid: ${command}.${functionName}`); response.writeHead(400, 'Bad command, function or options. Try cy .help for options'); this.writeAvailableCommands(response); return; } try { const payload = await this.parseBody(request); const output = await this.runCommand(functionMap, command, functionName, payload); const text = JSON.stringify(output || '', null, 2); response.writeHead(200, 'OK'); response.end(text); } catch (error) { Logger.log(error); response.writeHead(500, 'Oops'); response.write(error.message || error); response.end(); } } run(name, args) { const [command, functionName] = name.split('.'); const target = this.commands.map.get(command); if (!this.isValidCommand(target, command, functionName)) { throw new Error('Invalid command invoked: ' + name); } return this.runCommand(target, command, functionName, args); } async start() { const { apiHost, apiPort } = this.settings; const server = createServer((request, response) => this.handleRequest(request, response)); await this.commands.initialize(); return new Promise((resolve) => { server.on('listening', () => resolve(server)); server.listen(apiPort, apiHost); Logger.log(`Started services at ${apiHost}:${apiPort}.`); }); } getAvailableCommands() { const help = {}; this.commands.map.forEach((object, command) => { if (!(object && typeof object === 'object')) { return; } const properties = Object.getOwnPropertyNames(object); const commands = properties.filter((name) => name !== 'constructor' && typeof object[name] === 'function'); if (commands.length) { help[command] = commands; } }); return help; } writeAvailableCommands(response) { const help = this.getAvailableCommands(); response.end(JSON.stringify(help, null, 2)); } parseBody(request) { return new Promise((resolve, reject) => { const chunks = []; request.on('data', (c) => chunks.push(c)); request.on('end', () => { const text = Buffer.concat(chunks).toString('utf-8'); try { resolve(JSON.parse(text)); } catch (e) { reject(e); } }); request.on('error', reject); request.on('close', () => reject(new Error('Request closed'))); }); } isValidCommand(functionMap, command, functionName) { return functionMap && command && functionName && typeof functionMap[functionName] === 'function'; } async runCommand(functionMap, command, functionName, params) { const moduleConfig = getConfig(command); const optionFromFile = moduleConfig.commands?.[functionName] ?? {}; const mergedOptions = Object.assign({}, params, optionFromFile); Logger.debug(`Running command: ${command}.${functionName}`, mergedOptions); return await functionMap[functionName](mergedOptions, this.serverParams); } }