bin/kresus.js
Rama main · réplica de lectura del repositorio original.
#!/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);