@cloud-cli/cli
Version:
CLI for the Cloud CLI project
142 lines (141 loc) • 5.81 kB
JavaScript
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);
}
}