Porównaj commity

12 Commity

Autor SHA1 Wiadomość Data
3d0fce590b Introduce CLI
main: Parse args using getopt_long() in main().

Begin implementation of a CLI. It can be launched
using ./qswiki config --cli.

Allow connecting to another instance using "attach" command.
This uses Unix domain sockets, and in the future can be used
to drop caches, reload template, etc.

Closes: #21
2021-10-03 17:05:46 +02:00
1082f8ac5a Permissions: Add toString()
Get a (reasonable) string representation of the permissions contained
in a Permissions object.
2021-10-03 17:01:48 +02:00
8b044d712b Authenticator: Introduce AUTH_DEFAULT_SALT_SIZE 2021-10-03 17:01:03 +02:00
5037a17fba utils: introduce trim() 2021-10-03 16:51:04 +02:00
164b2c19ee userDao: Implement list() 2021-10-03 16:51:04 +02:00
8d685dc581 Makefile: Remove -lseccomp as we don't need it anymore 2021-09-29 18:33:45 +02:00
ed43f5f700 submodules: update cpp-httplib 2021-09-29 18:28:18 +02:00
10f00aeb45 main: Pass absolute path of config file
As sandboxing code chroots and chdirs away,
2021-09-23 17:13:08 +02:00
67eb8b6428 sandbox: adjust to latest qssb.h 2021-09-23 17:13:08 +02:00
f26fd19fb4 submodules: sync with latest upstream 2021-09-23 17:13:08 +02:00
204a72da1f setup: Fix broken FTS DELETE op
Thie previous DELETE statement lead to strange
behaviours. It was pure luck this did not blow up
before all these years. It appears it may leave the index
in an undefined state, and the database recently started
to display strange behaviour in connection with newer sqlite
version.

Now, just remove the previous revision from the FTS index,
as for now, search only cares about the most recent revisions.

Also, remove redundant UPDATE trigger on revision table
We never update revisions, thus such trigger is simply
redundant.

Relevant: https://gitlab.gnome.org/GNOME/tracker/-/merge_requests/353
2021-09-23 17:13:08 +02:00
88816a4015 utils: html_xss(): Add ' and &
They REALLY should have been there from the beginning...
2021-06-15 18:37:52 +02:00
22 zmienionych plików z 786 dodań i 82 usunięć

Wyświetl plik

@ -2,7 +2,7 @@
CXXFLAGS=-std=c++17 -O0 -g -no-pie -pipe -MMD -Wall -Wextra CXXFLAGS=-std=c++17 -O0 -g -no-pie -pipe -MMD -Wall -Wextra
RELEASE_CXXFLAGS=-std=c++17 -O3 -pipe -MMD -Wall -Wextra RELEASE_CXXFLAGS=-std=c++17 -O3 -pipe -MMD -Wall -Wextra
LDFLAGS=-lsqlite3 -lpthread -lcrypto -lstdc++fs -lseccomp LDFLAGS=-lsqlite3 -lpthread -lcrypto -lstdc++fs
INCLUDEFLAGS=-I submodules/sqlitemoderncpp/hdr -I submodules/cpp-httplib -I submodules/qssb.h INCLUDEFLAGS=-I submodules/sqlitemoderncpp/hdr -I submodules/cpp-httplib -I submodules/qssb.h
CXX=g++ CXX=g++

Wyświetl plik

@ -42,11 +42,12 @@ std::vector<char> Authenticator::pbkdf5(std::string password, const std::vector<
unsigned char hash[32]; unsigned char hash[32];
const EVP_MD *sha256 = EVP_sha256(); const EVP_MD *sha256 = EVP_sha256();
const unsigned char *rawsalt = reinterpret_cast<const unsigned char *>(salt.data()); const unsigned char *rawsalt = reinterpret_cast<const unsigned char *>(salt.data());
int ret = PKCS5_PBKDF2_HMAC(password.c_str(), password.size(), rawsalt, salt.size(), 300000, sha256, sizeof(hash), hash); int ret =
PKCS5_PBKDF2_HMAC(password.c_str(), password.size(), rawsalt, salt.size(), 300000, sha256, sizeof(hash), hash);
if(ret != 1) if(ret != 1)
{ {
Logger::error() << "Authenticator: pbkdf5: Failed to create hash"; Logger::error() << "Authenticator: pbkdf5: Failed to create hash";
return { }; return {};
} }
std::vector<char> result; std::vector<char> result;

Wyświetl plik

@ -3,6 +3,7 @@
#include <variant> #include <variant>
#include "database/userdao.h" #include "database/userdao.h"
#define AUTH_DEFAULT_SALT_SIZE 32
enum AuthenticationError enum AuthenticationError
{ {
UserNotFound, UserNotFound,

220
cli.cpp Normal file
Wyświetl plik

@ -0,0 +1,220 @@
#include <map>
#include <functional>
#include <iomanip>
#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<bool, std::string> CLIHandler::user_add(const std::vector<std::string> &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<char> 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<bool, std::string> CLIHandler::user_change_pw(const std::vector<std::string> &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<bool, std::string> CLIHandler::user_rename(const std::vector<std::string> &args)
{
return {true, ""};
}
std::pair<bool, std::string> CLIHandler::user_set_perms(const std::vector<std::string> &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<bool, std::string> CLIHandler::user_list(const std::vector<std::string> &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<bool, std::string> CLIHandler::user_show(const std::vector<std::string> &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<bool, std::string> CLIHandler::attach(const std::vector<std::string> &args)
{
/* TODO: consider authentication */
pid_t pid = getpid();
return {true, "Hi, I am pid: " + std::to_string(pid)};
}
std::pair<bool, std::string> CLIHandler::cli_help(const std::vector<std::string> &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<bool, std::string> CLIHandler::processCommand(const std::vector<CLIHandler::cmd> &commands, std::string cmd,
const std::vector<std::string> &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<std::string> 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<bool, std::string> CLIHandler::processCommand(std::string cmd, const std::vector<std::string> &args)
{
return processCommand(this->cmds, cmd, args);
}
std::pair<std::string, std::vector<std::string>> CLIHandler::splitCommand(std::string input)
{
input = utils::trim(input);
std::vector<std::string> splitted = utils::split(input, "\\s+");
if(splitted.empty())
{
return {" ", splitted};
}
std::string cmd = splitted[0];
splitted.erase(splitted.begin());
return {cmd, splitted};
}

66
cli.h Normal file
Wyświetl plik

@ -0,0 +1,66 @@
#ifndef CLI_H
#define CLI_H
#include <iostream>
#include <string>
#include <vector>
#include "database/database.h"
#include "config.h"
class CLIHandler
{
struct cmd
{
std::string name;
std::string helptext;
unsigned int required_args;
std::vector<cmd> subCommands;
std::function<std::pair<bool, std::string>(CLIHandler *, const std::vector<std::string> &)> func;
};
private:
Database *db;
Config *conf;
protected:
std::pair<bool, std::string> attach(const std::vector<std::string> &args);
std::pair<bool, std::string> cli_help(const std::vector<std::string> &args);
std::pair<bool, std::string> user_add(const std::vector<std::string> &args);
std::pair<bool, std::string> user_change_pw(const std::vector<std::string> &args);
std::pair<bool, std::string> user_rename(const std::vector<std::string> &args);
std::pair<bool, std::string> user_set_perms(const std::vector<std::string> &args);
std::pair<bool, std::string> user_list(const std::vector<std::string> &args);
std::pair<bool, std::string> user_show(const std::vector<std::string> &args);
std::vector<struct cmd> 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<std::string> &args) -> std::pair<bool, std::string>
{
exit(EXIT_SUCCESS);
return {true, ""};
}},
{"help", "print this help", 0, {}, &CLIHandler::cli_help},
{"attach", "attach to running instance", 0, {}, &CLIHandler::attach}}};
std::pair<bool, std::string> processCommand(const std::vector<CLIHandler::cmd> &commands, std::string cmd,
const std::vector<std::string> &args);
public:
CLIHandler(Config &config, Database &d);
std::pair<bool, std::string> processCommand(std::string cmd, const std::vector<std::string> &args);
static std::pair<std::string, std::vector<std::string>> splitCommand(std::string input);
};
#endif // CLI_H

137
cliconsole.cpp Normal file
Wyświetl plik

@ -0,0 +1,137 @@
#include "cliconsole.h"
CLIConsole::CLIConsole(CLIHandler &cliHandler, std::string socketPath)
{
this->handler = &cliHandler;
this->socketPath = socketPath;
}
std::pair<bool, std::string> 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<bool, std::string> 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";
}

27
cliconsole.h Normal file
Wyświetl plik

@ -0,0 +1,27 @@
#ifndef CLICONSOLE_H
#define CLICONSOLE_H
#include <iostream>
#include <string>
#include <vector>
#include <sys/socket.h>
#include <sys/un.h>
#include "cli.h"
class CLIConsole
{
private:
struct sockaddr_un server;
int sock;
CLIHandler *handler;
std::string socketPath;
bool attached = false;
std::pair<bool, std::string> send(std::string input);
void attach();
public:
CLIConsole(CLIHandler &cliHandler, std::string socketPath);
void startInteractive();
};
#endif // CLICONSOLE_H

77
cliserver.cpp Normal file
Wyświetl plik

@ -0,0 +1,77 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#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;
}

16
cliserver.h Normal file
Wyświetl plik

@ -0,0 +1,16 @@
#ifndef CLISERVER_H
#define CLISERVER_H
#include <thread>
#include "cli.h"
class CLIServer
{
private:
CLIHandler *handler = nullptr;
public:
CLIServer(CLIHandler &handler);
bool detachServer(std::string socketpath);
};
#endif // CLISERVER_H

Wyświetl plik

@ -3,6 +3,7 @@
#include <string> #include <string>
#include <optional> #include <optional>
#include "../user.h" #include "../user.h"
#include "queryoption.h"
class UserDao class UserDao
{ {
public: public:
@ -10,6 +11,7 @@ class UserDao
virtual bool exists(std::string username) = 0; virtual bool exists(std::string username) = 0;
virtual std::optional<User> find(std::string username) = 0; virtual std::optional<User> find(std::string username) = 0;
virtual std::optional<User> find(int id) = 0; virtual std::optional<User> find(int id) = 0;
virtual std::vector<User> list(QueryOption o) = 0;
virtual void deleteUser(std::string username) = 0; virtual void deleteUser(std::string username) = 0;
virtual void save(const User &u) = 0; virtual void save(const User &u) = 0;
virtual ~UserDao(){}; virtual ~UserDao(){};

Wyświetl plik

@ -23,6 +23,7 @@ SOFTWARE.
#include <memory> #include <memory>
#include <cstring> #include <cstring>
#include "userdaosqlite.h" #include "userdaosqlite.h"
#include "sqlitequeryoption.h"
UserDaoSqlite::UserDaoSqlite() UserDaoSqlite::UserDaoSqlite()
{ {
@ -82,6 +83,39 @@ std::optional<User> UserDaoSqlite::find(int id)
} }
} }
std::vector<User> UserDaoSqlite::list(QueryOption o)
{
std::vector<User> result;
try
{
std::string queryOption = SqliteQueryOption(o).setOrderByColumn("username").setPrependWhere(true).build();
std::string query = "SELECT username, password, salt, permissions, enabled FROM user " + queryOption;
*db << query >>
[&](std::string username, std::vector<char> pw, std::vector<char> salt, int permisisons, bool enabled)
{
User tmp;
tmp.login = username;
tmp.password = pw;
tmp.salt = salt;
tmp.permissions = Permissions{permisisons};
tmp.enabled = enabled;
result.push_back(tmp);
};
}
catch(const sqlite::errors::no_rows &e)
{
return result;
}
catch(sqlite::sqlite_exception &e)
{
throwFrom(e);
}
return result;
}
void UserDaoSqlite::deleteUser(std::string username) void UserDaoSqlite::deleteUser(std::string username)
{ {
// What to do with the contributions of the user? // What to do with the contributions of the user?

Wyświetl plik

@ -13,6 +13,7 @@ class UserDaoSqlite : public UserDao, protected SqliteDao
std::optional<User> find(std::string username); std::optional<User> find(std::string username);
std::optional<User> find(int id); std::optional<User> find(int id);
std::vector<User> list(QueryOption o);
void deleteUser(std::string username); void deleteUser(std::string username);
void save(const User &u); void save(const User &u);
using SqliteDao::SqliteDao; using SqliteDao::SqliteDao;

Wyświetl plik

@ -15,19 +15,20 @@ Response HandlerUserSettings::handleRequest(const Request &r)
if(newpassword != newpasswordconfirm) if(newpassword != newpasswordconfirm)
{ {
//TODO: is not nice, users has to hit the back button... // TODO: is not nice, users has to hit the back button...
return this->errorResponse("Passwords don't match", "The entered new passwords don't match"); return this->errorResponse("Passwords don't match", "The entered new passwords don't match");
} }
auto userDao = this->database->createUserDao(); auto userDao = this->database->createUserDao();
Authenticator authenticator(*userDao); Authenticator authenticator(*userDao);
std::variant<User, AuthenticationError> authresult = authenticator.authenticate(this->userSession->user.login, oldpassword); std::variant<User, AuthenticationError> authresult =
authenticator.authenticate(this->userSession->user.login, oldpassword);
if(std::holds_alternative<AuthenticationError>(authresult)) if(std::holds_alternative<AuthenticationError>(authresult))
{ {
return this->errorResponse("Invalid current password", "The old password you entered is invalid"); return this->errorResponse("Invalid current password", "The old password you entered is invalid");
} }
Random r; Random r;
std::vector<char> salt = r.getRandom(23); std::vector<char> salt = r.getRandom(AUTH_DEFAULT_SALT_SIZE);
User user = std::get<User>(authresult); User user = std::get<User>(authresult);
user.salt = salt; user.salt = salt;
user.password = authenticator.hash(newpassword, user.salt); user.password = authenticator.hash(newpassword, user.salt);

Wyświetl plik

@ -20,6 +20,17 @@ SOFTWARE.
*/ */
#include "permissions.h" #include "permissions.h"
static const std::map<std::string, int> permmap = {{"can_read", PERM_CAN_READ},
{"can_edit", PERM_CAN_EDIT},
{"can_page_history", PERM_CAN_PAGE_HISTORY},
{"can_global_history", PERM_CAN_GLOBAL_HISTORY},
{"can_delete", PERM_CAN_DELETE},
{"can_see_page_list", PERM_CAN_SEE_PAGE_LIST},
{"can_create", PERM_CAN_CREATE},
{"can_see_category_list", PERM_CAN_SEE_CATEGORY_LIST},
{"can_see_links_here", PERM_CAN_SEE_LINKS_HERE},
{"can_search", PERM_CAN_SEARCH}};
Permissions::Permissions(int permissions) Permissions::Permissions(int permissions)
{ {
this->permissions = permissions; this->permissions = permissions;
@ -36,3 +47,20 @@ Permissions::Permissions(const std::string &str)
} }
} }
} }
std::string Permissions::toString(int perms)
{
std::string result;
for(auto pair : permmap)
{
if(pair.second & perms)
{
result += pair.first + ",";
}
}
if(result.size() > 0)
{
result.pop_back();
}
return result;
}

Wyświetl plik

@ -14,20 +14,12 @@
#include <string> #include <string>
#include <map> #include <map>
class Permissions class Permissions
{ {
private: private:
int permissions = 0; int permissions = 0;
const std::map<std::string, int> permmap = {{"can_read", PERM_CAN_READ},
{"can_edit", PERM_CAN_EDIT},
{"can_page_history", PERM_CAN_PAGE_HISTORY},
{"can_global_history", PERM_CAN_GLOBAL_HISTORY},
{"can_delete", PERM_CAN_DELETE},
{"can_see_page_list", PERM_CAN_SEE_PAGE_LIST},
{"can_create", PERM_CAN_CREATE},
{"can_see_category_list", PERM_CAN_SEE_CATEGORY_LIST},
{"can_see_links_here", PERM_CAN_SEE_LINKS_HERE},
{"can_search", PERM_CAN_SEARCH}};
public: public:
Permissions() Permissions()
@ -102,6 +94,13 @@ class Permissions
{ {
return this->permissions & PERM_CAN_SEE_PAGE_LIST; return this->permissions & PERM_CAN_SEE_PAGE_LIST;
} }
std::string toString() const
{
return Permissions::toString(this->permissions);
}
static std::string toString(int perms);
}; };
#endif // PERMISSIONS_H #endif // PERMISSIONS_H

Wyświetl plik

@ -25,6 +25,7 @@ SOFTWARE.
#include <unistd.h> #include <unistd.h>
#include <sys/types.h> #include <sys/types.h>
#include <filesystem> #include <filesystem>
#include <getopt.h>
#include "gateway/gatewayinterface.h" #include "gateway/gatewayinterface.h"
#include "gateway/gatewayfactory.h" #include "gateway/gatewayfactory.h"
#include "handlers/handlerfactory.h" #include "handlers/handlerfactory.h"
@ -37,6 +38,10 @@ SOFTWARE.
#include "requestworker.h" #include "requestworker.h"
#include "cache/fscache.h" #include "cache/fscache.h"
#include "sandbox/sandboxfactory.h" #include "sandbox/sandboxfactory.h"
#include "cli.h"
#include "cliconsole.h"
#include "cliserver.h"
void sigterm_handler(int arg) void sigterm_handler(int arg)
{ {
// TODO: proper shutdown. // 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<ICache> createCache(const ConfigVariableResolver &resolver) std::unique_ptr<ICache> createCache(const ConfigVariableResolver &resolver)
{ {
@ -63,13 +72,48 @@ std::unique_ptr<ICache> createCache(const ConfigVariableResolver &resolver)
return std::make_unique<FsCache>(path); return std::make_unique<FsCache>(path);
} }
std::string get_version_string()
{
return "master";
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
char *configfilepath = NULL;
int option;
int option_index;
bool cli_mode = false;
if(geteuid() == 0) if(geteuid() == 0)
{ {
std::cerr << "Do not run this as root!" << std::endl; std::cerr << "Do not run this as root!" << std::endl;
return 1; 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(); auto sandbox = createSandbox();
// TODO: do we want to keep it mandatory or configurable? // TODO: do we want to keep it mandatory or configurable?
if(!sandbox->supported()) if(!sandbox->supported())
@ -77,21 +121,21 @@ int main(int argc, char **argv)
Logger::error() << "Sandbox is not supported, exiting"; Logger::error() << "Sandbox is not supported, exiting";
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
if(argc < 2)
{
std::cerr << "no path to config file provided" << std::endl;
return 1;
}
std::string configpath = std::filesystem::absolute(configfilepath).string();
if(!sandbox->enableForInit()) if(!sandbox->enableForInit())
{ {
Logger::error() << "Sandboxing for init mode could not be activated."; Logger::error() << "Sandboxing for init mode could not be activated.";
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
if(argc < 2)
{
std::cerr << "no path to config file provided" << std::endl;
return 1;
}
try try
{ {
ConfigReader configreader(argv[1]); ConfigReader configreader(configpath);
Config config = configreader.readConfig(); Config config = configreader.readConfig();
// TODO: config.connectiontring only works as long as we only support sqlite of course // TODO: config.connectiontring only works as long as we only support sqlite of course
@ -113,6 +157,21 @@ int main(int argc, char **argv)
Logger::setStream(&logstream); Logger::setStream(&logstream);
auto database = createDatabase(config); 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 // TODO: quite ugly, anon-handling must be rethought
auto userdao = database->createUserDao(); auto userdao = database->createUserDao();

Wyświetl plik

@ -26,16 +26,26 @@
bool SandboxLinux::enableForInit() bool SandboxLinux::enableForInit()
{ {
umask(0027); umask(0027);
struct qssb_policy policy = {0}; struct qssb_policy *policy = qssb_init_policy();
int blacklisted_syscalls[] = {QSSB_SYS(execveat), QSSB_SYS(execve), -1}; if(policy == NULL)
policy.blacklisted_syscalls = blacklisted_syscalls;
policy.no_new_privs = 1;
int result = qssb_enable_policy(&policy);
if(result != 0)
{ {
Logger::error() << "Failed to install sandboxing policy (init): " << result; Logger::error() << "Failed to init sandboxing policy (init)";
return false; return false;
} }
policy->namespace_options = QSSB_UNSHARE_USER;
policy->drop_caps = 0;
qssb_append_syscall_policy(policy, QSSB_SYSCALL_DENY_KILL_PROCESS, QSSB_SYS(execveat));
qssb_append_syscall_policy(policy, QSSB_SYSCALL_DENY_KILL_PROCESS, QSSB_SYS(execve));
qssb_append_syscall_default_policy(policy, QSSB_SYSCALL_ALLOW);
int result = qssb_enable_policy(policy);
if(result != 0)
{
Logger::error() << "Failed to enable sandboxing policy (init): " << result;
qssb_free_policy(policy);
return false;
}
qssb_free_policy(policy);
return true; return true;
} }
@ -44,28 +54,34 @@ bool SandboxLinux::enablePreWorker(std::vector<std::string> fsPaths)
std::sort(fsPaths.begin(), fsPaths.end(), std::sort(fsPaths.begin(), fsPaths.end(),
[](const std::string &a, const std::string &b) { return a.length() < b.length(); }); [](const std::string &a, const std::string &b) { return a.length() < b.length(); });
struct qssb_path_policy *policies = new qssb_path_policy[fsPaths.size()]; struct qssb_policy *policy = qssb_init_policy();
if(policy == NULL)
{
Logger::error() << "Failed to init sandboxing policy (pre)";
return false;
}
for(unsigned int i = 0; i < fsPaths.size(); i++) for(unsigned int i = 0; i < fsPaths.size(); i++)
{ {
policies[i].next = policies + (i + 1); qssb_append_path_policy(policy, QSSB_FS_ALLOW_READ | QSSB_FS_ALLOW_WRITE, fsPaths[i].c_str());
policies[i].mountpoint = fsPaths[i].c_str();
policies[i].policy = QSSB_MOUNT_ALLOW_READ | QSSB_MOUNT_ALLOW_WRITE;
} }
policies[fsPaths.size() - 1].next = NULL;
struct qssb_policy policy = {0}; policy->namespace_options = QSSB_UNSHARE_MOUNT;
policy.path_policies = policies; policy->drop_caps = 0;
policy.namespace_options |= QSSB_UNSHARE_MOUNT; policy->mount_path_policies_to_chroot = 1;
policy.namespace_options |= QSSB_UNSHARE_USER;
int blacklisted_syscalls[] = {QSSB_SYS(execveat), QSSB_SYS(execve), -1}; qssb_append_syscall_policy(policy, QSSB_SYSCALL_DENY_KILL_PROCESS, QSSB_SYS(execveat));
policy.blacklisted_syscalls = blacklisted_syscalls; qssb_append_syscall_policy(policy, QSSB_SYSCALL_DENY_KILL_PROCESS, QSSB_SYS(execve));
int result = qssb_enable_policy(&policy); qssb_append_syscall_default_policy(policy, QSSB_SYSCALL_ALLOW);
int result = qssb_enable_policy(policy);
if(result != 0) if(result != 0)
{ {
Logger::error() << "Failed to install sandboxing policy (preworker): %i" << result; Logger::error() << "Failed to install sandboxing policy (preworker): %i" << result;
qssb_free_policy(policy);
return false; return false;
} }
delete[] policies; qssb_free_policy(policy);
return true; return true;
} }
@ -88,30 +104,35 @@ bool SandboxLinux::supported()
} }
bool SandboxLinux::enableForWorker() bool SandboxLinux::enableForWorker()
{ {
struct qssb_policy policy = {0}; struct qssb_policy *policy = qssb_init_policy();
policy.drop_caps = 1; if(policy == NULL)
policy.not_dumpable = 1; {
policy.no_new_privs = 1; Logger::error() << "Failed to init sandboxing policy (worker) ";
return false;
}
policy->drop_caps = 1;
policy->not_dumpable = 1;
policy->no_new_privs = 1;
policy->namespace_options = 0;
/* TODO: as said, a whitelist approach is better. As such, this list is bound to be incomplete in the /* TODO: as said, a whitelist approach is better. As such, this list is bound to be incomplete in the
* sense that more could be listed here and some critical ones are probably missing */ * sense that more could be listed here and some critical ones are probably missing */
int blacklisted_syscalls[] = {QSSB_SYS(setuid),
QSSB_SYS(connect), /* TODO: use qssb groups */
QSSB_SYS(chroot), long blacklisted_syscalls[] = {QSSB_SYS(setuid), QSSB_SYS(connect), QSSB_SYS(chroot), QSSB_SYS(pivot_root),
QSSB_SYS(pivot_root), QSSB_SYS(mount), QSSB_SYS(setns), QSSB_SYS(unshare), QSSB_SYS(ptrace),
QSSB_SYS(mount), QSSB_SYS(personality), QSSB_SYS(prctl)};
QSSB_SYS(setns),
QSSB_SYS(unshare), qssb_append_syscalls_policy(policy, QSSB_SYSCALL_DENY_KILL_PROCESS, blacklisted_syscalls,
QSSB_SYS(ptrace), sizeof(blacklisted_syscalls) / sizeof(blacklisted_syscalls[0]));
QSSB_SYS(personality), qssb_append_syscall_default_policy(policy, QSSB_SYSCALL_ALLOW);
QSSB_SYS(prctl),
-1}; if(qssb_enable_policy(policy) != 0)
policy.blacklisted_syscalls = blacklisted_syscalls;
if(qssb_enable_policy(&policy) != 0)
{ {
Logger::error() << "Sandbox: Activation of seccomp blacklist failed!"; Logger::error() << "Sandbox: Activation of seccomp blacklist failed!";
qssb_free_policy(policy);
return false; return false;
} }
qssb_free_policy(policy);
return true; return true;
} }

Wyświetl plik

@ -25,24 +25,13 @@ count integer
CREATE TABLE category(id INTEGER PRIMARY KEY, name varchar(255)); CREATE TABLE category(id INTEGER PRIMARY KEY, name varchar(255));
CREATE TABLE categorymember(id INTEGER PRIMARY KEY, category REFERENCES category(id), page REFERENCES page (id)); CREATE TABLE categorymember(id INTEGER PRIMARY KEY, category REFERENCES category(id), page REFERENCES page (id));
CREATE INDEX revisionid ON revision (revisionid DESC); CREATE INDEX revisionid ON revision (revisionid DESC);
CREATE INDEX pagename ON page (name) CREATE INDEX pagename ON page (name);
; CREATE INDEX token ON session (token);
CREATE INDEX token ON session (token) CREATE VIRTUAL TABLE search USING fts5(content, page UNINDEXED, content=revision,content_rowid=id);
;
CREATE TRIGGER search_ai AFTER INSERT ON revision BEGIN
DELETE FROM search WHERE page = new.page;
INSERT INTO search(rowid, content, page) VALUES (new.id, new.content, new.page);
END;
CREATE TRIGGER search_au AFTER UPDATE ON revision BEGIN
DELETE FROM search WHERE page = old.page;
INSERT INTO search(rowid, content, page) VALUES (new.id, new.content, new.page);
END;
CREATE VIRTUAL TABLE search USING fts5(content, page UNINDEXED, content=revision,content_rowid=id)
/* search(content,page) */;
CREATE TABLE IF NOT EXISTS 'search_data'(id INTEGER PRIMARY KEY, block BLOB);
CREATE TABLE IF NOT EXISTS 'search_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS 'search_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
CREATE TABLE IF NOT EXISTS 'search_config'(k PRIMARY KEY, v) WITHOUT ROWID;
CREATE TRIGGER search_ad AFTER DELETE ON revision BEGIN CREATE TRIGGER search_ad AFTER DELETE ON revision BEGIN
INSERT INTO search(search, rowid, content, page) VALUES('delete', old.id, old.content, old.page); INSERT INTO search(search, rowid, content, page) VALUES('delete', old.id, old.content, old.page);
END; END;
CREATE TRIGGER search_ai AFTER INSERT ON revision BEGIN
INSERT INTO search(search, rowid, content, page) SELECT 'delete', id, content, page FROM revision WHERE page = new.page AND revisionid = new.revisionid - 1;
INSERT INTO search(rowid, content, page) VALUES (new.id, new.content, new.page);
END;

Wyświetl plik

@ -46,6 +46,12 @@ std::string utils::html_xss(std::string_view str)
case '%': case '%':
result += "&#37;"; result += "&#37;";
break; break;
case '\'':
result += "&#x27;";
break;
case '&':
result += "&amp;";
break;
default: default:
result += c; result += c;
} }
@ -93,7 +99,7 @@ std::vector<std::string> utils::split(const std::string &str, char delim)
// TODO: can easily break if we pass a regex here // TODO: can easily break if we pass a regex here
std::vector<std::string> utils::split(const std::string &str, const std::string &delim) std::vector<std::string> utils::split(const std::string &str, const std::string &delim)
{ {
std::regex regex { delim + "+" }; std::regex regex{delim + "+"};
return split(str, regex); return split(str, regex);
} }
@ -175,3 +181,20 @@ std::string utils::toISODate(time_t t)
} }
return std::string{result}; return std::string{result};
} }
std::string utils::trim(const std::string &str)
{
std::string_view chars = " \t\n\r";
std::string_view view = str;
auto n = view.find_first_not_of(chars);
if(n != std::string_view::npos)
{
view.remove_prefix(n);
}
n = view.find_last_not_of(chars);
if(n != std::string_view::npos)
{
view.remove_suffix(view.size() - n - 1);
}
return std::string{view};
}

Wyświetl plik

@ -93,5 +93,7 @@ template <class T> inline std::string toString(const T &v)
return std::string(v.begin(), v.end()); return std::string(v.begin(), v.end());
} }
std::string trim(const std::string &str);
} // namespace utils } // namespace utils
#endif #endif