From 9881cce647195b95637f62550d39ebe37751729d Mon Sep 17 00:00:00 2001 From: Chrystian Huot Date: Tue, 1 Jun 2021 15:27:50 -0400 Subject: [PATCH] New cmd.js for hardcore users --- .gitignore | 1 + cmd.js | 485 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 486 insertions(+) create mode 100755 cmd.js diff --git a/.gitignore b/.gitignore index 5c5eb51..6fa2810 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .vscode +*.token /dist \ No newline at end of file diff --git a/cmd.js b/cmd.js new file mode 100755 index 0000000..9c4d8e3 --- /dev/null +++ b/cmd.js @@ -0,0 +1,485 @@ +#!/usr/bin/env node + +/* + * ***************************************************************************** + * Copyright (C) 2019-2021 Chrystian Huot + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +'use strict'; + +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import path from 'path'; + +const APP_NAME = path.parse(process.argv[1]).base; + +const ARGUMENT_CODE = '--code'; +const ARGUMENT_IDENT = '--ident'; +const ARGUMENT_IN = '--in'; +const ARGUMENT_OUT = '--out'; +const ARGUMENT_PASSWORD = '--password'; +const ARGUMENT_TOKEN = '--token'; +const ARGUMENT_URL = '--url'; + +const COMMAND_CONFIG_GET = 'config-get'; +const COMMAND_CONFIG_SET = 'config-set'; +const COMMAND_HELP = 'help'; +const COMMAND_LOGIN = 'login'; +const COMMAND_LOGOUT = 'logout'; +const COMMAND_USER_ADD = 'user-add'; +const COMMAND_USER_REMOVE = 'user-remove'; + +const DEFAULT_ADMIN_PASSWORD = 'rdio-scanner'; +const DEFAULT_URL = 'http://localhost:3000/'; +const DEFAULT_TOKEN_FILE = `.${path.parse(process.argv[1]).name}.token`; + +const PADDING = 11; + +const URL_API = '/api/admin'; +const URL_CONFIG = `${URL_API}/config`; +const URL_LOGIN = `${URL_API}/login`; +const URL_LOGOUT = `${URL_API}/logout`; + +let tokenFile = DEFAULT_TOKEN_FILE; + +export class Cmd { + constructor() { + this.command = null; + this.inputFile = null; + this.outputFile = null; + this.password = process.env.RDIO_ADMIN_PASSWORD || DEFAULT_ADMIN_PASSWORD; + this.token = null; + this.url = new URL(DEFAULT_URL); + + const args = process.argv.slice(2).reduce((p, c) => p.concat(c.split(/(--\w+)=(\S+)/).filter((f) => f)), []); + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case COMMAND_CONFIG_GET: + case COMMAND_CONFIG_SET: + case COMMAND_HELP: + case COMMAND_LOGIN: + case COMMAND_LOGOUT: + case COMMAND_USER_ADD: + case COMMAND_USER_REMOVE: + this.command = args[i]; + break; + + case ARGUMENT_CODE: + this.code = args[++i]; + break; + + case ARGUMENT_IDENT: + this.ident = args[++i]; + break; + + case ARGUMENT_IN: + this.inputFile = args[++i]; + break; + + case ARGUMENT_OUT: + this.outputFile = args[++i]; + break; + + case ARGUMENT_PASSWORD: + this.password = args[++i]; + break; + + case ARGUMENT_TOKEN: + tokenFile = args[++i]; + break; + + case ARGUMENT_URL: + try { + this.url = new URL(args[++i]); + } catch (error) { + exitWithError('Invalid URL'); + } + if (!/^https?:/i.test(this.url)) { + exitWithError('Invalid URL'); + } + break; + } + } + + this.token = readTokenFile(); + + switch (this.command) { + case COMMAND_CONFIG_GET: + this.configGet(); + break; + + case COMMAND_CONFIG_SET: + this.configSet(); + break; + + case COMMAND_LOGIN: + this.login(); + break; + + case COMMAND_LOGOUT: + this.logout(); + break; + + case COMMAND_USER_ADD: + this.userAdd(); + break; + + case COMMAND_USER_REMOVE: + this.userRemove(); + break; + + default: + this.usage(); + } + } + + async configGet() { + if (!this.outputFile) { + exitWithError('Missing --out arguments.'); + return; + } + + const res = await httpGet(new URL(URL_CONFIG, this.url), this.token); + + if (res.statusCode === 200) { + saveFile(this.outputFile, JSON.stringify(res.data.config, null, 2)); + exitWithMessage(`Server's configuration saved to ${this.outputFile}`); + } else { + exitWithError(res.error || res.data); + } + } + + async configSet() { + if (!this.inputFile) { + exitWithError('Missing --in arguments.'); + return; + } + + let config = readFile(this.inputFile); + + try { + config = JSON.parse(config); + } catch (error) { + exitWithError(error.message); + return; + } + + const res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token); + + if (res.statusCode === 200) { + exitWithMessage(`Server's configuration applied from file ${this.inputFile}`); + + } else { + exitWithError(res.error || res.data); + } + } + + async login() { + const res = await httpPost(new URL(URL_LOGIN, this.url), { password: this.password }); + + if (res.statusCode === 200) { + this.token = res.data.token; + saveTokenFile(this.token); + exitWithMessage('Logged in.'); + + } else { + exitWithError(res.error || res.data); + } + } + + async logout() { + const res = await httpPost(new URL(URL_LOGOUT, this.url), null, this.token); + + this.token = null; + saveTokenFile(this.token); + + if (res.statusCode === 200) { + exitWithMessage('Logged out.'); + + } else { + exitWithError(res.error || res.data); + } + } + + usage() { + exitWithMessage([ + 'Available Commands:', + ` ${COMMAND_CONFIG_GET.padEnd(PADDING)} – Retrieve server's configuration.`, + '', + ` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_CONFIG_GET} ${ARGUMENT_OUT} `, + '', + ` ${COMMAND_CONFIG_SET.padEnd(PADDING)} – Set server's configuration.`, + '', + ` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_CONFIG_SET} ${ARGUMENT_IN} `, + '', + ` ${COMMAND_LOGIN.padEnd(PADDING)} – Login to administrative backend.`, + '', + ` ${''.padStart(PADDING)} $ RDIO_ADMIN_PASSWORD= ./${APP_NAME} ${COMMAND_LOGIN}`, + ` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_LOGIN} ${ARGUMENT_PASSWORD} `, + '', + ` ${COMMAND_LOGOUT.padEnd(PADDING)} – Logout from administrative backend.`, + '', + ` ${COMMAND_USER_ADD.padEnd(PADDING)} – Add a user access.`, + '', + ` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_USER_ADD} ${ARGUMENT_IDENT} ${ARGUMENT_CODE} `, + '', + ` ${COMMAND_USER_REMOVE.padEnd(PADDING)} – Remove a user access.`, + '', + ` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_USER_REMOVE} ${ARGUMENT_IDENT} `, + '', + 'Global options:', + ` ${ARGUMENT_TOKEN.padEnd(PADDING)} – Session token keystore. Default is '${DEFAULT_TOKEN_FILE}'.`, + ` ${ARGUMENT_URL.padEnd(PADDING)} – Server remote address. Default is '${DEFAULT_URL}'.`, + '', + ].join('\n')); + } + + async userAdd() { + if (!this.code || !this.ident) { + exitWithError('Missing --ident or --code arguments.'); + return; + } + + let res = await httpGet(new URL(URL_CONFIG, this.url), this.token); + + if (res.statusCode !== 200) { + exitWithError(res.error || res.data); + return; + } + + const config = res.data.config; + + config.access = config.access.filter((u) => u.ident.toLowerCase() !== this.ident.toLowerCase()); + + config.access = config.access.concat({ + code: this.code, + ident: this.ident, + systems: '*', + }); + + res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token); + + if (res.statusCode === 200) { + exitWithMessage(`User ${this.ident} added.`); + + } else { + exitWithError(res.error || res.data); + } + } + + async userRemove() { + if (!this.ident) { + exitWithError('Missing --ident arguments.'); + return; + } + + let res = await httpGet(new URL(URL_CONFIG, this.url), this.token); + + if (res.statusCode !== 200) { + exitWithError(res.error || res.data); + return; + } + + const config = res.data.config; + + if (config.access.findIndex((u) => u.ident.toLowerCase() === this.ident.toLowerCase()) === -1) { + exitWithError(`Unknown user ${this.ident}`); + return; + } + + config.access = config.access.filter((u) => u.ident.toLowerCase() !== this.ident.toLowerCase()); + + res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token); + + if (res.statusCode === 200) { + exitWithMessage(`User ${this.ident} removed.`); + + } else { + exitWithError(res.error || res.data); + } + } +} + +function exitWithError(error) { + process.stdout.write(`ERROR: ${error}\n`); + process.exit(1); +} + +function exitWithMessage(message) { + process.stdout.write(`${message}\n`); + process.exit(0); +} + +async function httpGet(url, autorization) { + return new Promise((resolve) => { + const result = { data: null, error: null, statusCode: null }; + const options = autorization ? { headers: { Authorization: autorization } } : {}; + + try { + const req = (url.protocol === 'https:' ? https : http).get(url, options, (res) => { + result.statusCode = res.statusCode; + + res.setEncoding('utf8'); + res.on('data', (chunk) => result.data = (result.data || '') + chunk); + res.on('end', () => { + try { + result.data = JSON.parse(result.data); + } catch (error) { } + resolve(result); + }); + }); + + req.on('error', (error) => { + result.error = error.code; + resolve(result); + }); + + } catch (error) { + result.error = error.message; + resolve(result); + } + }); +} + +async function httpPost(url, postData, autorization) { + return new Promise((resolve) => { + const result = { data: null, error: null, statusCode: null }; + + postData = JSON.stringify(postData || {}); + + const options = { + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + method: 'POST', + }; + + if (autorization) { + options.headers.Authorization = autorization; + } + + try { + const req = (url.protocol === 'https:' ? https : http).request(url, options, (res) => { + result.statusCode = res.statusCode; + + res.setEncoding('utf8'); + res.on('data', (chunk) => result.data = (result.data || '') + chunk); + res.on('end', () => { + try { + result.data = JSON.parse(result.data); + } catch (error) { } + + resolve(result); + }); + }); + + req.on('error', (error) => { + result.error = error.code; + resolve(result); + }); + req.write(postData); + req.end(); + + } catch (error) { + result.error = error.message; + resolve(result); + } + }); +} + +async function httpPut(url, putData, autorization) { + return new Promise((resolve) => { + const result = { data: null, error: null, statusCode: null }; + + putData = JSON.stringify(putData); + + const options = { + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(putData), + }, + method: 'PUT', + }; + + if (autorization) { + options.headers.Authorization = autorization; + } + + try { + const req = (url.protocol === 'https:' ? https : http).request(url, options, (res) => { + result.statusCode = res.statusCode; + + res.setEncoding('utf8'); + res.on('data', (chunk) => result.data = (result.data || '') + chunk); + res.on('end', () => { + try { + result.data = JSON.parse(result.data); + } catch (error) { } + + resolve(result); + }); + }); + + req.on('error', (error) => { + result.error = error.code; + resolve(result); + }); + req.write(putData); + req.end(); + + } catch (error) { + result.error = error.message; + resolve(result); + } + }); +} + +function readFile(file) { + if (fs.existsSync(file)) { + try { + return fs.readFileSync(file); + + } catch (error) { + exitWithError(error.message); + } + + } else { + exitWithError(`Inexistent token keystore file ${file}`); + } +} + +function readTokenFile() { + return readFile(tokenFile); +} + +function saveFile(file, data) { + try { + fs.writeFileSync(file, data || ''); + + } catch (error) { + exitWithError(error.message); + } +} + +function saveTokenFile(token) { + saveFile(tokenFile, token); +} + +new Cmd(); \ No newline at end of file