diff --git a/cli.cpp b/cli.cpp new file mode 100644 index 0000000..cc8ce68 --- /dev/null +++ b/cli.cpp @@ -0,0 +1,220 @@ +#include +#include +#include + +#include "cli.h" +#include "utils.h" +#include "random.h" +#include "authenticator.h" +#include "config.h" +#include "logger.h" + +CLIHandler::CLIHandler(Config &config, Database &db) +{ + this->db = &db; + this->conf = &config; +} + +std::pair CLIHandler::user_add(const std::vector &args) +{ + std::string username = args.at(0); + std::string password = args.at(1); + + auto userDao = db->createUserDao(); + + Permissions perms = this->conf->handlersConfig.anon_permissions; + int p = perms.getPermissions(); + p |= PERM_CAN_CREATE | PERM_CAN_SEARCH | PERM_CAN_EDIT; + Permissions newPermissions = Permissions{p}; + + Random r; + User user; + user.enabled = true; + user.login = username; + user.salt = r.getRandom(AUTH_DEFAULT_SALT_SIZE); + user.permissions = newPermissions; + + Authenticator auth{*userDao}; + std::vector hashResult = auth.hash(password, user.salt); + if(hashResult.empty()) + { + return {false, "Error during hashing - Got empty hash"}; + } + user.password = hashResult; + + try + { + userDao->save(user); + } + catch(std::runtime_error &e) + { + return {false, "Exception: " + std::string(e.what())}; + } + return {true, ""}; +} + +std::pair CLIHandler::user_change_pw(const std::vector &args) +{ + std::string username = args.at(0); + std::string password = args.at(1); + + auto userDao = db->createUserDao(); + + auto user = userDao->find(username); + if(user) + { + Random r; + Authenticator auth{*userDao}; + user->salt = r.getRandom(AUTH_DEFAULT_SALT_SIZE); + user->password = auth.hash(password, user->salt); + if(user->password.empty()) + { + return {false, "Error during hashing - Got empty hash"}; + } + + userDao->save(*user); + } + else + { + return {false, "User not found"}; + } +} + +std::pair CLIHandler::user_rename(const std::vector &args) +{ + + return {true, ""}; +} + +std::pair CLIHandler::user_set_perms(const std::vector &args) +{ + auto userDao = this->db->createUserDao(); + std::string username = args.at(0); + std::string permission_string = args.at(1); + + Permissions perms{permission_string}; + + auto user = userDao->find(username); + if(user) + { + user->permissions = perms; + userDao->save(*user); + user_show({username}); + return {true, ""}; + } + + return {false, "User not found"}; +} + +std::pair CLIHandler::user_list(const std::vector &args) +{ + auto userDao = this->db->createUserDao(); + QueryOption o; + auto result = userDao->list(o); + std::stringstream stream; + for(User &u : result) + { + stream << u.login << "\t" << std::string(u.enabled ? "enabled" : "disabled") << "\t" << u.permissions.toString() + << std::endl; + } + return {true, stream.str()}; +} + +std::pair CLIHandler::user_show(const std::vector &args) +{ + std::string username = args.at(0); + auto userDao = this->db->createUserDao(); + auto user = userDao->find(username); + std::stringstream stream; + if(user) + { + stream << "Username: " << user->login << std::endl; + + stream << "Enabled: " << std::string(user->enabled ? "yes" : "no") << std::endl; + stream << "Permissions (general): " << user->permissions.toString() << std::endl; + return {true, stream.str()}; + } + return {false, "User not found"}; +} + +std::pair CLIHandler::attach(const std::vector &args) +{ + /* TODO: consider authentication */ + pid_t pid = getpid(); + return {true, "Hi, I am pid: " + std::to_string(pid)}; +} + +std::pair CLIHandler::cli_help(const std::vector &args) +{ + std::string command; + if(args.size() > 0) + command = args[0]; + std::stringstream stream; + for(struct cmd &cmd : cmds) + { + if(command != "" && cmd.name != command) + { + continue; + } + + stream << cmd.name << " - " << cmd.helptext << std::endl; + for(struct cmd &subCmd : cmd.subCommands) + { + stream << "\t" << subCmd.name << " " << subCmd.helptext << std::endl; + } + stream << std::endl; + } + return {true, stream.str()}; +} + +std::pair CLIHandler::processCommand(const std::vector &commands, std::string cmd, + const std::vector &args) +{ + auto c = std::find_if(commands.begin(), commands.end(), + [&cmd](const struct CLIHandler::cmd &a) { return a.name == cmd; }); + if(c == commands.end()) + { + std::cout << "No such command: " << cmd << std::endl; + return cli_help({}); + } + + if(!c->subCommands.empty() && args.size() >= c->required_args) + { + std::string newcmd = args[0]; + std::vector newargs = args; + newargs.erase(newargs.begin()); + return processCommand(c->subCommands, newcmd, newargs); + } + if(args.size() < c->required_args) + { + return {false, "not enough parameters passed"}; + } + + try + { + return c->func(this, args); + } + catch(std::runtime_error &e) + { + return {false, "Exception: " + std::string(e.what())}; + } + return {false, ""}; +} + +std::pair CLIHandler::processCommand(std::string cmd, const std::vector &args) +{ + return processCommand(this->cmds, cmd, args); +} + +std::pair> CLIHandler::splitCommand(std::string input) +{ + input = utils::trim(input); + std::vector splitted = utils::split(input, "\\s+"); + if(splitted.empty()) + { + return {" ", splitted}; + } + std::string cmd = splitted[0]; + splitted.erase(splitted.begin()); + return {cmd, splitted}; +} diff --git a/cli.h b/cli.h new file mode 100644 index 0000000..ab26530 --- /dev/null +++ b/cli.h @@ -0,0 +1,66 @@ +#ifndef CLI_H +#define CLI_H +#include +#include +#include +#include "database/database.h" +#include "config.h" + +class CLIHandler +{ + struct cmd + { + std::string name; + std::string helptext; + unsigned int required_args; + std::vector subCommands; + std::function(CLIHandler *, const std::vector &)> func; + }; + + private: + Database *db; + Config *conf; + + protected: + std::pair attach(const std::vector &args); + std::pair cli_help(const std::vector &args); + std::pair user_add(const std::vector &args); + std::pair user_change_pw(const std::vector &args); + std::pair user_rename(const std::vector &args); + std::pair user_set_perms(const std::vector &args); + std::pair user_list(const std::vector &args); + std::pair user_show(const std::vector &args); + + std::vector cmds{ + {{"user", + "user operations on the database", + 1, + {{{"add", "[user] [password] - creates a user", 2, {}, &CLIHandler::user_add}, + {"changepw", "[user] [password] - changes the password of user", 2, {}, &CLIHandler::user_change_pw}, + {"rename", "[user] [new name] - renames a user", 2, {}, &CLIHandler::user_rename}, + {"setperms", "[user] [perms] - sets the permissions of the user", 2, {}, &CLIHandler::user_set_perms}, + {"list", "- lists users", 0, {}, &CLIHandler::user_list}, + {"show", "[user] - show detailed information about user", 1, {}, &CLIHandler::user_show}}}, + &CLIHandler::cli_help}, + {"exit", + "exit cli", + 0, + {}, + [](CLIHandler *, const std::vector &args) -> std::pair + { + exit(EXIT_SUCCESS); + return {true, ""}; + }}, + {"help", "print this help", 0, {}, &CLIHandler::cli_help}, + {"attach", "attach to running instance", 0, {}, &CLIHandler::attach}}}; + + std::pair processCommand(const std::vector &commands, std::string cmd, + const std::vector &args); + + public: + CLIHandler(Config &config, Database &d); + std::pair processCommand(std::string cmd, const std::vector &args); + static std::pair> splitCommand(std::string input); +}; + +#endif // CLI_H diff --git a/cliconsole.cpp b/cliconsole.cpp new file mode 100644 index 0000000..28e1d10 --- /dev/null +++ b/cliconsole.cpp @@ -0,0 +1,137 @@ +#include "cliconsole.h" + +CLIConsole::CLIConsole(CLIHandler &cliHandler, std::string socketPath) +{ + this->handler = &cliHandler; + this->socketPath = socketPath; +} + +std::pair CLIConsole::send(std::string input) +{ + ssize_t ret = + sendto(this->sock, input.c_str(), input.size(), 0, (const sockaddr *)&this->server, sizeof(this->server)); + if((size_t)ret != input.size()) + { + return {false, "sendto failed: " + std::to_string(ret) + " " + std::string(strerror(errno))}; + } + char buffer[1024] = {0}; + ret = recvfrom(this->sock, buffer, sizeof(buffer) - 1, 0, NULL, NULL); + if(ret == -1) + { + return {false, "recvfrom failed: " + std::string(strerror(errno))}; + } + + bool success = false; + std::string_view view = buffer; + if(view[0] == '1') + { + success = true; + } + view.remove_prefix(1); + std::string msg = std::string{view}; + + return {success, msg}; +} + +void CLIConsole::attach() +{ + if(attached) + { + std::cout << "Already attached" << std::endl; + return; + } + if(socketPath.size() > sizeof(this->server.sun_path) - 1) + { + std::cout << "Socket path too long" << std::endl; + return; + } + memset(&this->server, 0, sizeof(this->server)); + this->server.sun_family = AF_UNIX; + memcpy(&this->server.sun_path, socketPath.c_str(), socketPath.size()); + this->server.sun_path[socketPath.size()] = 0; + + int s = socket(AF_UNIX, SOCK_DGRAM, 0); + if(s == -1) + { + std::cout << "Failed to create socket" << strerror(errno) << std::endl; + return; + } + this->sock = s; + + struct sockaddr_un client; + client.sun_family = AF_UNIX; + client.sun_path[0] = '\0'; + + int ret = bind(this->sock, (struct sockaddr *)&client, sizeof(client)); + if(ret != 0) + { + std::cout << "bind() failed: " << strerror(errno) << std::endl; + return; + } + auto result = this->send("attach"); + if(result.first) + { + std::cout << "Attached successfully: " << result.second << std::endl; + this->attached = true; + } + else + { + std::cout << "Attached unsuccessfully: " << result.second << std::endl; + } +} + +void CLIConsole::startInteractive() +{ + std::cout << "qswiki CLI" << std::endl; + std::cout << "not attached - use 'attach' to connect to running instance" << std::endl; + + while(true) + { + std::string input; + std::cout << "> "; + std::getline(std::cin, input); + + if(std::cin.bad() || std::cin.eof()) + { + std::cout << "Exiting" << std::endl; + return; + } + if(input.empty()) + { + continue; + } + + auto pair = CLIHandler::splitCommand(input); + if(pair.first == "exit") + { + std::cout << "Exiting CLI"; + exit(EXIT_SUCCESS); + } + if(pair.first == "attach") + { + attach(); + continue; + } + + std::pair result; + if(!attached) + { + result = handler->processCommand(pair.first, pair.second); + } + else + { + result = this->send(input); + } + + if(!result.second.empty()) + { + std::cout << result.second << std::endl; + } + if(!result.first) + { + std::cout << "Command failed" << std::endl; + } + } + + std::cout << "\n"; +} diff --git a/cliconsole.h b/cliconsole.h new file mode 100644 index 0000000..1545020 --- /dev/null +++ b/cliconsole.h @@ -0,0 +1,27 @@ +#ifndef CLICONSOLE_H +#define CLICONSOLE_H + +#include +#include +#include +#include +#include +#include "cli.h" + +class CLIConsole +{ + private: + struct sockaddr_un server; + int sock; + CLIHandler *handler; + std::string socketPath; + bool attached = false; + std::pair send(std::string input); + void attach(); + + public: + CLIConsole(CLIHandler &cliHandler, std::string socketPath); + void startInteractive(); +}; + +#endif // CLICONSOLE_H diff --git a/cliserver.cpp b/cliserver.cpp new file mode 100644 index 0000000..2ba76d8 --- /dev/null +++ b/cliserver.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include "cliserver.h" +#include "logger.h" +CLIServer::CLIServer(CLIHandler &handler) +{ + this->handler = &handler; +} + +bool CLIServer::detachServer(std::string socketpath) +{ + struct sockaddr_un name; + const int max_socket_length = sizeof(name.sun_path) - 1; + if(socketpath.size() > max_socket_length) + { + perror("socket path too long"); + return false; + } + int s = socket(AF_UNIX, SOCK_DGRAM, 0); + if(s == -1) + { + perror("socket"); + return false; + } + + memset(&name, 0, sizeof(name)); + name.sun_family = AF_UNIX; + memcpy(&name.sun_path, socketpath.c_str(), socketpath.size()); + + unlink(socketpath.c_str()); + int ret = bind(s, (const struct sockaddr *)&name, sizeof(name)); + if(ret == -1) + { + perror("bind"); + exit(EXIT_FAILURE); + } + + auto worker = [=] + { + while(true) + { + char buffer[1024] = {0}; + struct sockaddr_un peer; + socklen_t peerlen = sizeof(peer); + + int ret = recvfrom(s, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &peerlen); + if(ret == -1) + { + Logger::error() << "Error during recvfrom in CLI server: " << strerror(errno); + return false; + } + + std::string input{buffer}; + + auto pair = CLIHandler::splitCommand(input); + + auto result = handler->processCommand(pair.first, pair.second); + char resultCode = '0'; + if(result.first) + { + resultCode = '1'; + } + std::string resultString; + resultString += resultCode; + resultString += result.second; + ret = sendto(s, resultString.c_str(), resultString.size(), 0, (struct sockaddr *)&peer, peerlen); + if(ret == -1) + { + Logger::error() << "Error during sendto in CLI server: " << strerror(errno); + } + } + }; + std::thread t1{worker}; + t1.detach(); + return true; +} diff --git a/cliserver.h b/cliserver.h new file mode 100644 index 0000000..9745da8 --- /dev/null +++ b/cliserver.h @@ -0,0 +1,16 @@ +#ifndef CLISERVER_H +#define CLISERVER_H +#include +#include "cli.h" + +class CLIServer +{ + private: + CLIHandler *handler = nullptr; + + public: + CLIServer(CLIHandler &handler); + bool detachServer(std::string socketpath); +}; + +#endif // CLISERVER_H diff --git a/qswiki.cpp b/qswiki.cpp index 1f8f53f..9d5738e 100644 --- a/qswiki.cpp +++ b/qswiki.cpp @@ -25,6 +25,7 @@ SOFTWARE. #include #include #include +#include #include "gateway/gatewayinterface.h" #include "gateway/gatewayfactory.h" #include "handlers/handlerfactory.h" @@ -37,6 +38,10 @@ SOFTWARE. #include "requestworker.h" #include "cache/fscache.h" #include "sandbox/sandboxfactory.h" +#include "cli.h" +#include "cliconsole.h" +#include "cliserver.h" + void sigterm_handler(int arg) { // TODO: proper shutdown. @@ -56,6 +61,10 @@ void setup_signal_handlers() } } +#define OPT_PRINT_VERSION 23 + +static struct option long_options[] = {{"cli", no_argument, 0, 'c'}, {"version", no_argument, 0, OPT_PRINT_VERSION}}; + std::unique_ptr createCache(const ConfigVariableResolver &resolver) { @@ -63,13 +72,48 @@ std::unique_ptr createCache(const ConfigVariableResolver &resolver) return std::make_unique(path); } + +std::string get_version_string() +{ + return "master"; +} + int main(int argc, char **argv) { + + char *configfilepath = NULL; + int option; + int option_index; + bool cli_mode = false; + if(geteuid() == 0) { std::cerr << "Do not run this as root!" << std::endl; return 1; } + + while((option = getopt_long(argc, argv, "cv", long_options, &option_index)) != -1) + { + switch(option) + { + case 'c': + cli_mode = true; + break; + case OPT_PRINT_VERSION: + std::cout << get_version_string() << std::endl; + exit(EXIT_SUCCESS); + break; + } + } + + if(optind == argc) + { + std::cerr << "Missing config path" << std::endl; + return 1; + } + + configfilepath = argv[optind++]; + auto sandbox = createSandbox(); // TODO: do we want to keep it mandatory or configurable? if(!sandbox->supported()) @@ -82,7 +126,7 @@ int main(int argc, char **argv) std::cerr << "no path to config file provided" << std::endl; return 1; } - std::string configpath = std::filesystem::absolute(argv[1]).string(); + std::string configpath = std::filesystem::absolute(configfilepath).string(); if(!sandbox->enableForInit()) { Logger::error() << "Sandboxing for init mode could not be activated."; @@ -113,6 +157,21 @@ int main(int argc, char **argv) Logger::setStream(&logstream); auto database = createDatabase(config); + std::string socketPath = config.configVarResolver.getConfig("socketpath"); + CLIHandler cliHandler(config, *database); + + if(cli_mode) + { + CLIConsole console{cliHandler, socketPath}; + console.startInteractive(); + return 0; + } + CLIServer cliServer{cliHandler}; + if(!cliServer.detachServer(socketPath)) + { + Logger::error() << "Error: Failed to detach unix socket server"; + return 1; + } // TODO: quite ugly, anon-handling must be rethought auto userdao = database->createUserDao();