Ctrl K
ComunidadGuías…

bin/kresus.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node

const path = require('node:path');
const fs = require('node:fs');
const { parseArgs } = require('node:util');
const ini = require('ini');

function help(binaryName) {
    console.log(
        `Usage: ${binaryName}\n` +
            '\t-h or --help or help: displays this message.\n' +
            '\t-c $path or --config $path: path to the configuration file.\n' +
            '\tcreate:config: creates an empty configuration file up to date.\n' +
            '\tcreate:user $login [--admin]: creates a new user with given login,\n' +
            '\t\tand assigns it an ID. Pass "--admin" to create an administrator.\n' +
            '\tdelete:user $login: deletes a user with given login\n' +
            '\tlist:users: lists all the users with their id, login and admin status.'
    );
}

let explainedChmodError = false;
function tryChmod(pathname, mode) {
    try {
        fs.chmodSync(pathname, mode);
    } catch (_err) {
        if (!explainedChmodError) {
            console.warn(`To help ensuring your private data is safe, Kresus tried to chmod the
data directory (datadir in config.ini, or KRESUS_DATA_DIR as environment variable) with predefined
restrictive settings, but an error occurred:`);
            explainedChmodError = true;
        }
        console.warn('Unable to chmod', pathname);
    }
}

function recursiveChmod(pathname, fileMode, dirMode) {
    const stats = fs.statSync(pathname);
    if (stats.isFile()) {
        if (stats.mode !== fileMode) {
            tryChmod(pathname, fileMode);
        }
        return;
    }
    if (stats.isDirectory(pathname)) {
        if (stats.mode !== dirMode) {
            tryChmod(pathname, dirMode);
        }
        fs.readdirSync(pathname).forEach(dir => {
            recursiveChmod(path.join(pathname, dir), fileMode, dirMode);
        });
    }
}

function readConfigFromFile(pathname) {
    // In the stats retrieved from a file, the rights are the last 9 bits :
    // user rights / group rights / other rights
    const configFileACLMask = 0x1ff;

    let content = null;
    try {
        const mode = fs.statSync(pathname).mode;

        const rights = mode & configFileACLMask;

        // Allow:
        // - readable by user
        // - writeable by user
        // - readable by group
        const allowedFlags = fs.constants.S_IRUSR | fs.constants.S_IWUSR | fs.constants.S_IRGRP;

        // In production, check the config file has r or rw rights for the owner.
        if (process.env.NODE_ENV === 'production' && (rights & ~allowedFlags) !== 0) {
            console.error(`For security reasons, the configuration file ${pathname} should be at
most readable by its owner and group, writable by its owner. Please make sure to restrict
permissions on this file using the chmod command.`);
            process.exit(-1);
        }

        content = fs.readFileSync(pathname, { encoding: 'utf8' });
    } catch (e) {
        console.error(
            'Error when trying to read the configuration file (does the file at this path exist?)',
            e.toString(),
            '\n\n',
            e.stack
        );
        process.exit(-1);
    }

    let config = {};
    try {
        config = ini.parse(content);
    } catch (e) {
        console.error(
            'INI formatting error when reading the configuration file:',
            e.toString(),
            '\n\n',
            e.stack
        );
        process.exit(-1);
    }

    return config;
}

const ROOT = path.join(path.dirname(fs.realpathSync(__filename)), '..', 'build');
const configurator = require(path.join(ROOT, 'server', 'config.js'));

function runServer() {
    // Then only, import the server.
    const server = require(path.join(ROOT, 'server'));

    const dataDir = process.kresus.dataDir;
    if (!fs.existsSync(dataDir)) {
        fs.mkdirSync(dataDir);
    }

    // The server should only create files with +rw permissions for the current
    // user.
    const processUmask = 0o0077;
    process.umask(processUmask);

    // Ensure the data directory contains files only the current user can read and
    // write.
    recursiveChmod(
        dataDir,
        fs.constants.S_IRUSR | fs.constants.S_IWUSR,
        fs.constants.S_IRUSR | fs.constants.S_IWUSR | fs.constants.S_IXUSR
    );

    process.chdir(dataDir);

    server.start();
}

function createUser(login, admin = false) {
    const cli = require(path.join(ROOT, 'server', 'cli'));
    cli.createUser(login, admin).catch(error => {
        console.error(error);
        process.exit(-1);
    });
}

function deleteUser(login) {
    const cli = require(path.join(ROOT, 'server', 'cli'));
    cli.deleteUser(login).catch(error => {
        console.error(error);
        process.exit(-1);
    });
}

function listUsers() {
    const cli = require(path.join(ROOT, 'server', 'cli'));
    cli.listUsers().catch(error => {
        console.error(error);
        process.exit(-1);
    });
}

const binaryName = process.argv[1];

function exitWithError(...message) {
    console.error(...message);
    help(binaryName);
    process.exit(-1);
}

let parsedArgs;
try {
    parsedArgs = parseArgs({
        options: {
            help: { type: 'boolean', short: 'h' },
            config: { type: 'string', short: 'c' },
            admin: { type: 'boolean' },
        },
        allowPositionals: true,
    });
} catch (err) {
    exitWithError(err.message);
}

const { values, positionals } = parsedArgs;
const [commandName, ...commandPositionals] = positionals;

if (values.help || commandName === 'help') {
    help(binaryName);
    process.exit(0);
}

if (values.admin && commandName !== 'create:user') {
    exitWithError('The --admin option can only be used with create:user.');
}

const expectedPositionals = ['create:user', 'delete:user'].includes(commandName) ? 1 : 0;
if (commandPositionals.length > expectedPositionals) {
    exitWithError('Unexpected argument:', commandPositionals[expectedPositionals]);
}

let command = runServer;
const commandArgs = [];

switch (commandName) {
    case undefined:
        break;
    case 'create:config':
        console.log(configurator.generate());
        process.exit(0);
        break;
    case 'create:user':
    case 'delete:user': {
        const login = commandPositionals[0];
        if (!login) {
            exitWithError('Missing user login.');
        }
        if (commandName === 'create:user') {
            command = createUser;
            commandArgs.push(login, !!values.admin);
        } else {
            command = deleteUser;
            commandArgs.push(login);
        }
        break;
    }
    case 'list:users':
        command = listUsers;
        break;
    default:
        exitWithError('Unknown command:', commandName);
}

const config = values.config ? readConfigFromFile(values.config) : null;

if (!config) {
    console.warn(
        "Configuration file not provided. If this is intentional and you did not provide configuration directives through environment variables you'll see error messages during database setup."
    );
}

// First, define process.kresus.
configurator.apply(config || {});

// Then, call the right command.
command(...commandArgs);