Replace auth.txt with SQLite auth database (#7279)
* Replace auth.txt with SQLite auth databasemaster
parent
1836882495
commit
153fb211ac
|
@ -251,6 +251,7 @@ LOCAL_SRC_FILES := \
|
|||
jni/src/util/srp.cpp \
|
||||
jni/src/util/timetaker.cpp \
|
||||
jni/src/unittest/test.cpp \
|
||||
jni/src/unittest/test_authdatabase.cpp \
|
||||
jni/src/unittest/test_collision.cpp \
|
||||
jni/src/unittest/test_compression.cpp \
|
||||
jni/src/unittest/test_connection.cpp \
|
||||
|
@ -331,6 +332,7 @@ LOCAL_SRC_FILES += \
|
|||
jni/src/script/cpp_api/s_security.cpp \
|
||||
jni/src/script/cpp_api/s_server.cpp \
|
||||
jni/src/script/lua_api/l_areastore.cpp \
|
||||
jni/src/script/lua_api/l_auth.cpp \
|
||||
jni/src/script/lua_api/l_base.cpp \
|
||||
jni/src/script/lua_api/l_camera.cpp \
|
||||
jni/src/script/lua_api/l_client.cpp \
|
||||
|
|
|
@ -4,72 +4,22 @@
|
|||
-- Builtin authentication handler
|
||||
--
|
||||
|
||||
local auth_file_path = core.get_worldpath().."/auth.txt"
|
||||
local auth_table = {}
|
||||
|
||||
local function read_auth_file()
|
||||
local newtable = {}
|
||||
local file, errmsg = io.open(auth_file_path, 'rb')
|
||||
if not file then
|
||||
core.log("info", auth_file_path.." could not be opened for reading ("..errmsg.."); assuming new world")
|
||||
return
|
||||
end
|
||||
for line in file:lines() do
|
||||
if line ~= "" then
|
||||
local fields = line:split(":", true)
|
||||
local name, password, privilege_string, last_login = unpack(fields)
|
||||
last_login = tonumber(last_login)
|
||||
if not (name and password and privilege_string) then
|
||||
error("Invalid line in auth.txt: "..dump(line))
|
||||
end
|
||||
local privileges = core.string_to_privs(privilege_string)
|
||||
newtable[name] = {password=password, privileges=privileges, last_login=last_login}
|
||||
end
|
||||
end
|
||||
io.close(file)
|
||||
auth_table = newtable
|
||||
core.notify_authentication_modified()
|
||||
end
|
||||
|
||||
local function save_auth_file()
|
||||
local newtable = {}
|
||||
-- Check table for validness before attempting to save
|
||||
for name, stuff in pairs(auth_table) do
|
||||
assert(type(name) == "string")
|
||||
assert(name ~= "")
|
||||
assert(type(stuff) == "table")
|
||||
assert(type(stuff.password) == "string")
|
||||
assert(type(stuff.privileges) == "table")
|
||||
assert(stuff.last_login == nil or type(stuff.last_login) == "number")
|
||||
end
|
||||
local content = {}
|
||||
for name, stuff in pairs(auth_table) do
|
||||
local priv_string = core.privs_to_string(stuff.privileges)
|
||||
local parts = {name, stuff.password, priv_string, stuff.last_login or ""}
|
||||
content[#content + 1] = table.concat(parts, ":")
|
||||
end
|
||||
if not core.safe_file_write(auth_file_path, table.concat(content, "\n")) then
|
||||
error(auth_file_path.." could not be written to")
|
||||
end
|
||||
end
|
||||
|
||||
read_auth_file()
|
||||
-- Make the auth object private, deny access to mods
|
||||
local core_auth = core.auth
|
||||
core.auth = nil
|
||||
|
||||
core.builtin_auth_handler = {
|
||||
get_auth = function(name)
|
||||
assert(type(name) == "string")
|
||||
-- Figure out what password to use for a new player (singleplayer
|
||||
-- always has an empty password, otherwise use default, which is
|
||||
-- usually empty too)
|
||||
local new_password_hash = ""
|
||||
-- If not in authentication table, return nil
|
||||
if not auth_table[name] then
|
||||
local auth_entry = core_auth.read(name)
|
||||
-- If no such auth found, return nil
|
||||
if not auth_entry then
|
||||
return nil
|
||||
end
|
||||
-- Figure out what privileges the player should have.
|
||||
-- Take a copy of the privilege table
|
||||
local privileges = {}
|
||||
for priv, _ in pairs(auth_table[name].privileges) do
|
||||
for priv, _ in pairs(auth_entry.privileges) do
|
||||
privileges[priv] = true
|
||||
end
|
||||
-- If singleplayer, give all privileges except those marked as give_to_singleplayer = false
|
||||
|
@ -89,85 +39,89 @@ core.builtin_auth_handler = {
|
|||
end
|
||||
-- All done
|
||||
return {
|
||||
password = auth_table[name].password,
|
||||
password = auth_entry.password,
|
||||
privileges = privileges,
|
||||
-- Is set to nil if unknown
|
||||
last_login = auth_table[name].last_login,
|
||||
last_login = auth_entry.last_login,
|
||||
}
|
||||
end,
|
||||
create_auth = function(name, password)
|
||||
assert(type(name) == "string")
|
||||
assert(type(password) == "string")
|
||||
core.log('info', "Built-in authentication handler adding player '"..name.."'")
|
||||
auth_table[name] = {
|
||||
return core_auth.create({
|
||||
name = name,
|
||||
password = password,
|
||||
privileges = core.string_to_privs(core.settings:get("default_privs")),
|
||||
last_login = os.time(),
|
||||
}
|
||||
save_auth_file()
|
||||
})
|
||||
end,
|
||||
delete_auth = function(name)
|
||||
assert(type(name) == "string")
|
||||
if not auth_table[name] then
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
return false
|
||||
end
|
||||
core.log('info', "Built-in authentication handler deleting player '"..name.."'")
|
||||
auth_table[name] = nil
|
||||
save_auth_file()
|
||||
return true
|
||||
return core_auth.delete(name)
|
||||
end,
|
||||
set_password = function(name, password)
|
||||
assert(type(name) == "string")
|
||||
assert(type(password) == "string")
|
||||
if not auth_table[name] then
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
core.builtin_auth_handler.create_auth(name, password)
|
||||
else
|
||||
core.log('info', "Built-in authentication handler setting password of player '"..name.."'")
|
||||
auth_table[name].password = password
|
||||
save_auth_file()
|
||||
auth_entry.password = password
|
||||
core_auth.save(auth_entry)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
set_privileges = function(name, privileges)
|
||||
assert(type(name) == "string")
|
||||
assert(type(privileges) == "table")
|
||||
if not auth_table[name] then
|
||||
core.builtin_auth_handler.create_auth(name,
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
auth_entry = core.builtin_auth_handler.create_auth(name,
|
||||
core.get_password_hash(name,
|
||||
core.settings:get("default_password")))
|
||||
end
|
||||
|
||||
-- Run grant callbacks
|
||||
for priv, _ in pairs(privileges) do
|
||||
if not auth_table[name].privileges[priv] then
|
||||
if not auth_entry.privileges[priv] then
|
||||
core.run_priv_callbacks(name, priv, nil, "grant")
|
||||
end
|
||||
end
|
||||
|
||||
-- Run revoke callbacks
|
||||
for priv, _ in pairs(auth_table[name].privileges) do
|
||||
for priv, _ in pairs(auth_entry.privileges) do
|
||||
if not privileges[priv] then
|
||||
core.run_priv_callbacks(name, priv, nil, "revoke")
|
||||
end
|
||||
end
|
||||
|
||||
auth_table[name].privileges = privileges
|
||||
auth_entry.privileges = privileges
|
||||
core_auth.save(auth_entry)
|
||||
core.notify_authentication_modified(name)
|
||||
save_auth_file()
|
||||
end,
|
||||
reload = function()
|
||||
read_auth_file()
|
||||
core_auth.reload()
|
||||
return true
|
||||
end,
|
||||
record_login = function(name)
|
||||
assert(type(name) == "string")
|
||||
assert(auth_table[name]).last_login = os.time()
|
||||
save_auth_file()
|
||||
local auth_entry = core_auth.read(name)
|
||||
assert(auth_entry)
|
||||
auth_entry.last_login = os.time()
|
||||
core_auth.save(auth_entry)
|
||||
end,
|
||||
iterate = function()
|
||||
local names = {}
|
||||
for k in pairs(auth_table) do
|
||||
names[k] = true
|
||||
local nameslist = core_auth.list_names()
|
||||
for k,v in pairs(nameslist) do
|
||||
names[v] = true
|
||||
end
|
||||
return pairs(names)
|
||||
end,
|
||||
|
@ -177,12 +131,13 @@ core.register_on_prejoinplayer(function(name, ip)
|
|||
if core.registered_auth_handler ~= nil then
|
||||
return -- Don't do anything if custom auth handler registered
|
||||
end
|
||||
if auth_table[name] ~= nil then
|
||||
local auth_entry = core_auth.read(name)
|
||||
if auth_entry ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local name_lower = name:lower()
|
||||
for k in pairs(auth_table) do
|
||||
for k in core.builtin_auth_handler.iterate() do
|
||||
if k:lower() == name_lower then
|
||||
return string.format("\nCannot create new player called '%s'. "..
|
||||
"Another account called '%s' is already registered. "..
|
||||
|
|
|
@ -29,6 +29,7 @@ It can be copied over from an old world to a newly created world.
|
|||
|
||||
World
|
||||
|-- auth.txt ----- Authentication data
|
||||
|-- auth.sqlite -- Authentication data (SQLite alternative)
|
||||
|-- env_meta.txt - Environment metadata
|
||||
|-- ipban.txt ---- Banned ips/users
|
||||
|-- map_meta.txt - Map metadata
|
||||
|
@ -62,6 +63,34 @@ Example lines:
|
|||
- Player "bar", no password, no privileges:
|
||||
bar::
|
||||
|
||||
auth.sqlite
|
||||
------------
|
||||
Contains authentification data as an SQLite database. This replaces auth.txt
|
||||
above when auth_backend is set to "sqlite3" in world.mt .
|
||||
|
||||
This database contains two tables "auth" and "user_privileges":
|
||||
|
||||
CREATE TABLE `auth` (
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`name` VARCHAR(32) UNIQUE,
|
||||
`password` VARCHAR(512),
|
||||
`last_login` INTEGER
|
||||
);
|
||||
CREATE TABLE `user_privileges` (
|
||||
`id` INTEGER,
|
||||
`privilege` VARCHAR(32),
|
||||
PRIMARY KEY (id, privilege)
|
||||
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES auth (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
The "name" and "password" fields of the auth table are the same as the auth.txt
|
||||
fields (with modern password hash). The "last_login" field is the last login
|
||||
time as a unix time stamp.
|
||||
|
||||
The "user_privileges" table contains one entry per privilege and player.
|
||||
A player with "interact" and "shout" privileges will have two entries, one
|
||||
with privilege="interact" and the second with privilege="shout".
|
||||
|
||||
env_meta.txt
|
||||
-------------
|
||||
Simple global environment variables.
|
||||
|
@ -107,6 +136,7 @@ Example content (added indentation and - explanations):
|
|||
readonly_backend = sqlite3 - optionally readonly seed DB (DB file _must_ be located in "readonly" subfolder)
|
||||
server_announce = false - whether the server is publicly announced or not
|
||||
load_mod_<mod> = false - whether <mod> is to be loaded in this world
|
||||
auth_backend = files - which DB backend to use for authentication data
|
||||
|
||||
Player File Format
|
||||
===================
|
||||
|
|
|
@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "settings.h"
|
||||
#include "porting.h"
|
||||
#include "filesys.h"
|
||||
#include "util/string.h"
|
||||
|
||||
// !!! WARNING !!!
|
||||
// This backend is intended to be used on Minetest 0.4.16 only for the transition backend
|
||||
|
@ -177,3 +178,105 @@ void PlayerDatabaseFiles::listPlayers(std::vector<std::string> &res)
|
|||
res.emplace_back(player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
AuthDatabaseFiles::AuthDatabaseFiles(const std::string &savedir) : m_savedir(savedir)
|
||||
{
|
||||
readAuthFile();
|
||||
}
|
||||
|
||||
bool AuthDatabaseFiles::getAuth(const std::string &name, AuthEntry &res)
|
||||
{
|
||||
const auto res_i = m_auth_list.find(name);
|
||||
if (res_i == m_auth_list.end()) {
|
||||
return false;
|
||||
}
|
||||
res = res_i->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuthDatabaseFiles::saveAuth(const AuthEntry &authEntry)
|
||||
{
|
||||
m_auth_list[authEntry.name] = authEntry;
|
||||
|
||||
// save entire file
|
||||
return writeAuthFile();
|
||||
}
|
||||
|
||||
bool AuthDatabaseFiles::createAuth(AuthEntry &authEntry)
|
||||
{
|
||||
m_auth_list[authEntry.name] = authEntry;
|
||||
|
||||
// save entire file
|
||||
return writeAuthFile();
|
||||
}
|
||||
|
||||
bool AuthDatabaseFiles::deleteAuth(const std::string &name)
|
||||
{
|
||||
if (!m_auth_list.erase(name)) {
|
||||
// did not delete anything -> hadn't existed
|
||||
return false;
|
||||
}
|
||||
return writeAuthFile();
|
||||
}
|
||||
|
||||
void AuthDatabaseFiles::listNames(std::vector<std::string> &res)
|
||||
{
|
||||
res.clear();
|
||||
res.reserve(m_auth_list.size());
|
||||
for (const auto &res_pair : m_auth_list) {
|
||||
res.push_back(res_pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthDatabaseFiles::reload()
|
||||
{
|
||||
readAuthFile();
|
||||
}
|
||||
|
||||
bool AuthDatabaseFiles::readAuthFile()
|
||||
{
|
||||
std::string path = m_savedir + DIR_DELIM + "auth.txt";
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) {
|
||||
return false;
|
||||
}
|
||||
m_auth_list.clear();
|
||||
while (file.good()) {
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
std::vector<std::string> parts = str_split(line, ':');
|
||||
if (parts.size() < 3) // also: empty line at end
|
||||
continue;
|
||||
const std::string &name = parts[0];
|
||||
const std::string &password = parts[1];
|
||||
std::vector<std::string> privileges = str_split(parts[2], ',');
|
||||
s64 last_login = parts.size() > 3 ? atol(parts[3].c_str()) : 0;
|
||||
|
||||
m_auth_list[name] = {
|
||||
1,
|
||||
name,
|
||||
password,
|
||||
privileges,
|
||||
last_login,
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuthDatabaseFiles::writeAuthFile()
|
||||
{
|
||||
std::string path = m_savedir + DIR_DELIM + "auth.txt";
|
||||
std::ostringstream output(std::ios_base::binary);
|
||||
for (const auto &auth_i : m_auth_list) {
|
||||
const AuthEntry &authEntry = auth_i.second;
|
||||
output << authEntry.name << ":" << authEntry.password << ":";
|
||||
output << str_join(authEntry.privileges, ",");
|
||||
output << ":" << authEntry.last_login;
|
||||
output << std::endl;
|
||||
}
|
||||
if (!fs::safeWriteToFile(path, output.str())) {
|
||||
infostream << "Failed to write " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
// for player files
|
||||
|
||||
#include "database.h"
|
||||
#include <unordered_map>
|
||||
|
||||
class PlayerDatabaseFiles : public PlayerDatabase
|
||||
{
|
||||
|
@ -41,3 +42,23 @@ private:
|
|||
|
||||
std::string m_savedir;
|
||||
};
|
||||
|
||||
class AuthDatabaseFiles : public AuthDatabase
|
||||
{
|
||||
public:
|
||||
AuthDatabaseFiles(const std::string &savedir);
|
||||
virtual ~AuthDatabaseFiles() = default;
|
||||
|
||||
virtual bool getAuth(const std::string &name, AuthEntry &res);
|
||||
virtual bool saveAuth(const AuthEntry &authEntry);
|
||||
virtual bool createAuth(AuthEntry &authEntry);
|
||||
virtual bool deleteAuth(const std::string &name);
|
||||
virtual void listNames(std::vector<std::string> &res);
|
||||
virtual void reload();
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, AuthEntry> m_auth_list;
|
||||
std::string m_savedir;
|
||||
bool readAuthFile();
|
||||
bool writeAuthFile();
|
||||
};
|
||||
|
|
|
@ -606,3 +606,170 @@ void PlayerDatabaseSQLite3::listPlayers(std::vector<std::string> &res)
|
|||
|
||||
sqlite3_reset(m_stmt_player_list);
|
||||
}
|
||||
|
||||
/*
|
||||
* Auth database
|
||||
*/
|
||||
|
||||
AuthDatabaseSQLite3::AuthDatabaseSQLite3(const std::string &savedir) :
|
||||
Database_SQLite3(savedir, "auth"), AuthDatabase()
|
||||
{
|
||||
}
|
||||
|
||||
AuthDatabaseSQLite3::~AuthDatabaseSQLite3()
|
||||
{
|
||||
FINALIZE_STATEMENT(m_stmt_read)
|
||||
FINALIZE_STATEMENT(m_stmt_write)
|
||||
FINALIZE_STATEMENT(m_stmt_create)
|
||||
FINALIZE_STATEMENT(m_stmt_delete)
|
||||
FINALIZE_STATEMENT(m_stmt_list_names)
|
||||
FINALIZE_STATEMENT(m_stmt_read_privs)
|
||||
FINALIZE_STATEMENT(m_stmt_write_privs)
|
||||
FINALIZE_STATEMENT(m_stmt_delete_privs)
|
||||
FINALIZE_STATEMENT(m_stmt_last_insert_rowid)
|
||||
}
|
||||
|
||||
void AuthDatabaseSQLite3::createDatabase()
|
||||
{
|
||||
assert(m_database); // Pre-condition
|
||||
|
||||
SQLOK(sqlite3_exec(m_database,
|
||||
"CREATE TABLE IF NOT EXISTS `auth` ("
|
||||
"`id` INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"`name` VARCHAR(32) UNIQUE,"
|
||||
"`password` VARCHAR(512),"
|
||||
"`last_login` INTEGER"
|
||||
");",
|
||||
NULL, NULL, NULL),
|
||||
"Failed to create auth table");
|
||||
|
||||
SQLOK(sqlite3_exec(m_database,
|
||||
"CREATE TABLE IF NOT EXISTS `user_privileges` ("
|
||||
"`id` INTEGER,"
|
||||
"`privilege` VARCHAR(32),"
|
||||
"PRIMARY KEY (id, privilege)"
|
||||
"CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES auth (id) ON DELETE CASCADE"
|
||||
");",
|
||||
NULL, NULL, NULL),
|
||||
"Failed to create auth privileges table");
|
||||
}
|
||||
|
||||
void AuthDatabaseSQLite3::initStatements()
|
||||
{
|
||||
PREPARE_STATEMENT(read, "SELECT id, name, password, last_login FROM auth WHERE name = ?");
|
||||
PREPARE_STATEMENT(write, "UPDATE auth set name = ?, password = ?, last_login = ? WHERE id = ?");
|
||||
PREPARE_STATEMENT(create, "INSERT INTO auth (name, password, last_login) VALUES (?, ?, ?)");
|
||||
PREPARE_STATEMENT(delete, "DELETE FROM auth WHERE name = ?");
|
||||
|
||||
PREPARE_STATEMENT(list_names, "SELECT name FROM auth ORDER BY name DESC");
|
||||
|
||||
PREPARE_STATEMENT(read_privs, "SELECT privilege FROM user_privileges WHERE id = ?");
|
||||
PREPARE_STATEMENT(write_privs, "INSERT OR IGNORE INTO user_privileges (id, privilege) VALUES (?, ?)");
|
||||
PREPARE_STATEMENT(delete_privs, "DELETE FROM user_privileges WHERE id = ?");
|
||||
|
||||
PREPARE_STATEMENT(last_insert_rowid, "SELECT last_insert_rowid()");
|
||||
}
|
||||
|
||||
bool AuthDatabaseSQLite3::getAuth(const std::string &name, AuthEntry &res)
|
||||
{
|
||||
verifyDatabase();
|
||||
str_to_sqlite(m_stmt_read, 1, name);
|
||||
if (sqlite3_step(m_stmt_read) != SQLITE_ROW) {
|
||||
sqlite3_reset(m_stmt_read);
|
||||
return false;
|
||||
}
|
||||
res.id = sqlite_to_uint(m_stmt_read, 0);
|
||||
res.name = sqlite_to_string(m_stmt_read, 1);
|
||||
res.password = sqlite_to_string(m_stmt_read, 2);
|
||||
res.last_login = sqlite_to_int64(m_stmt_read, 3);
|
||||
sqlite3_reset(m_stmt_read);
|
||||
|
||||
int64_to_sqlite(m_stmt_read_privs, 1, res.id);
|
||||
while (sqlite3_step(m_stmt_read_privs) == SQLITE_ROW) {
|
||||
res.privileges.emplace_back(sqlite_to_string(m_stmt_read_privs, 0));
|
||||
}
|
||||
sqlite3_reset(m_stmt_read_privs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuthDatabaseSQLite3::saveAuth(const AuthEntry &authEntry)
|
||||
{
|
||||
beginSave();
|
||||
|
||||
str_to_sqlite(m_stmt_write, 1, authEntry.name);
|
||||
str_to_sqlite(m_stmt_write, 2, authEntry.password);
|
||||
int64_to_sqlite(m_stmt_write, 3, authEntry.last_login);
|
||||
int64_to_sqlite(m_stmt_write, 4, authEntry.id);
|
||||
sqlite3_vrfy(sqlite3_step(m_stmt_write), SQLITE_DONE);
|
||||
sqlite3_reset(m_stmt_write);
|
||||
|
||||
writePrivileges(authEntry);
|
||||
|
||||
endSave();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuthDatabaseSQLite3::createAuth(AuthEntry &authEntry)
|
||||
{
|
||||
beginSave();
|
||||
|
||||
// id autoincrements
|
||||
str_to_sqlite(m_stmt_create, 1, authEntry.name);
|
||||
str_to_sqlite(m_stmt_create, 2, authEntry.password);
|
||||
int64_to_sqlite(m_stmt_create, 3, authEntry.last_login);
|
||||
sqlite3_vrfy(sqlite3_step(m_stmt_create), SQLITE_DONE);
|
||||
sqlite3_reset(m_stmt_create);
|
||||
|
||||
// obtain id and write back to original authEntry
|
||||
sqlite3_step(m_stmt_last_insert_rowid);
|
||||
authEntry.id = sqlite_to_uint(m_stmt_last_insert_rowid, 0);
|
||||
sqlite3_reset(m_stmt_last_insert_rowid);
|
||||
|
||||
writePrivileges(authEntry);
|
||||
|
||||
endSave();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuthDatabaseSQLite3::deleteAuth(const std::string &name)
|
||||
{
|
||||
verifyDatabase();
|
||||
|
||||
str_to_sqlite(m_stmt_delete, 1, name);
|
||||
sqlite3_vrfy(sqlite3_step(m_stmt_delete), SQLITE_DONE);
|
||||
int changes = sqlite3_changes(m_database);
|
||||
sqlite3_reset(m_stmt_delete);
|
||||
|
||||
// privileges deleted by foreign key on delete cascade
|
||||
|
||||
return changes > 0;
|
||||
}
|
||||
|
||||
void AuthDatabaseSQLite3::listNames(std::vector<std::string> &res)
|
||||
{
|
||||
verifyDatabase();
|
||||
|
||||
while (sqlite3_step(m_stmt_list_names) == SQLITE_ROW) {
|
||||
res.push_back(sqlite_to_string(m_stmt_list_names, 0));
|
||||
}
|
||||
sqlite3_reset(m_stmt_list_names);
|
||||
}
|
||||
|
||||
void AuthDatabaseSQLite3::reload()
|
||||
{
|
||||
// noop for SQLite
|
||||
}
|
||||
|
||||
void AuthDatabaseSQLite3::writePrivileges(const AuthEntry &authEntry)
|
||||
{
|
||||
int64_to_sqlite(m_stmt_delete_privs, 1, authEntry.id);
|
||||
sqlite3_vrfy(sqlite3_step(m_stmt_delete_privs), SQLITE_DONE);
|
||||
sqlite3_reset(m_stmt_delete_privs);
|
||||
for (const std::string &privilege : authEntry.privileges) {
|
||||
int64_to_sqlite(m_stmt_write_privs, 1, authEntry.id);
|
||||
str_to_sqlite(m_stmt_write_privs, 2, privilege);
|
||||
sqlite3_vrfy(sqlite3_step(m_stmt_write_privs), SQLITE_DONE);
|
||||
sqlite3_reset(m_stmt_write_privs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,16 @@ protected:
|
|||
return (u32) sqlite3_column_int(s, iCol);
|
||||
}
|
||||
|
||||
inline s64 sqlite_to_int64(sqlite3_stmt *s, int iCol)
|
||||
{
|
||||
return (s64) sqlite3_column_int64(s, iCol);
|
||||
}
|
||||
|
||||
inline u64 sqlite_to_uint64(sqlite3_stmt *s, int iCol)
|
||||
{
|
||||
return (u64) sqlite3_column_int64(s, iCol);
|
||||
}
|
||||
|
||||
inline float sqlite_to_float(sqlite3_stmt *s, int iCol)
|
||||
{
|
||||
return (float) sqlite3_column_double(s, iCol);
|
||||
|
@ -191,3 +201,34 @@ private:
|
|||
sqlite3_stmt *m_stmt_player_metadata_remove = nullptr;
|
||||
sqlite3_stmt *m_stmt_player_metadata_add = nullptr;
|
||||
};
|
||||
|
||||
class AuthDatabaseSQLite3 : private Database_SQLite3, public AuthDatabase
|
||||
{
|
||||
public:
|
||||
AuthDatabaseSQLite3(const std::string &savedir);
|
||||
virtual ~AuthDatabaseSQLite3();
|
||||
|
||||
virtual bool getAuth(const std::string &name, AuthEntry &res);
|
||||
virtual bool saveAuth(const AuthEntry &authEntry);
|
||||
virtual bool createAuth(AuthEntry &authEntry);
|
||||
virtual bool deleteAuth(const std::string &name);
|
||||
virtual void listNames(std::vector<std::string> &res);
|
||||
virtual void reload();
|
||||
|
||||
protected:
|
||||
virtual void createDatabase();
|
||||
virtual void initStatements();
|
||||
|
||||
private:
|
||||
virtual void writePrivileges(const AuthEntry &authEntry);
|
||||
|
||||
sqlite3_stmt *m_stmt_read = nullptr;
|
||||
sqlite3_stmt *m_stmt_write = nullptr;
|
||||
sqlite3_stmt *m_stmt_create = nullptr;
|
||||
sqlite3_stmt *m_stmt_delete = nullptr;
|
||||
sqlite3_stmt *m_stmt_list_names = nullptr;
|
||||
sqlite3_stmt *m_stmt_read_privs = nullptr;
|
||||
sqlite3_stmt *m_stmt_write_privs = nullptr;
|
||||
sqlite3_stmt *m_stmt_delete_privs = nullptr;
|
||||
sqlite3_stmt *m_stmt_last_insert_rowid = nullptr;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "irr_v3d.h"
|
||||
|
@ -61,3 +62,25 @@ public:
|
|||
virtual bool removePlayer(const std::string &name) = 0;
|
||||
virtual void listPlayers(std::vector<std::string> &res) = 0;
|
||||
};
|
||||
|
||||
struct AuthEntry
|
||||
{
|
||||
u64 id;
|
||||
std::string name;
|
||||
std::string password;
|
||||
std::vector<std::string> privileges;
|
||||
s64 last_login;
|
||||
};
|
||||
|
||||
class AuthDatabase
|
||||
{
|
||||
public:
|
||||
virtual ~AuthDatabase() = default;
|
||||
|
||||
virtual bool getAuth(const std::string &name, AuthEntry &res) = 0;
|
||||
virtual bool saveAuth(const AuthEntry &authEntry) = 0;
|
||||
virtual bool createAuth(AuthEntry &authEntry) = 0;
|
||||
virtual bool deleteAuth(const std::string &name) = 0;
|
||||
virtual void listNames(std::vector<std::string> &res) = 0;
|
||||
virtual void reload() = 0;
|
||||
};
|
||||
|
|
|
@ -289,6 +289,8 @@ static void set_allowed_options(OptionList *allowed_options)
|
|||
_("Migrate from current map backend to another (Only works when using minetestserver or with --server)"))));
|
||||
allowed_options->insert(std::make_pair("migrate-players", ValueSpec(VALUETYPE_STRING,
|
||||
_("Migrate from current players backend to another (Only works when using minetestserver or with --server)"))));
|
||||
allowed_options->insert(std::make_pair("migrate-auth", ValueSpec(VALUETYPE_STRING,
|
||||
_("Migrate from current auth backend to another (Only works when using minetestserver or with --server)"))));
|
||||
allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
|
||||
_("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
|
||||
#ifndef SERVER
|
||||
|
@ -840,6 +842,9 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
|
|||
if (cmd_args.exists("migrate-players"))
|
||||
return ServerEnvironment::migratePlayersDatabase(game_params, cmd_args);
|
||||
|
||||
if (cmd_args.exists("migrate-auth"))
|
||||
return ServerEnvironment::migrateAuthDatabase(game_params, cmd_args);
|
||||
|
||||
if (cmd_args.exists("terminal")) {
|
||||
#if USE_CURSES
|
||||
bool name_ok = true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
set(common_SCRIPT_LUA_API_SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/l_areastore.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/l_auth.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/l_base.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/l_craft.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "lua_api/l_auth.h"
|
||||
#include "lua_api/l_internal.h"
|
||||
#include "common/c_converter.h"
|
||||
#include "common/c_content.h"
|
||||
#include "cpp_api/s_base.h"
|
||||
#include "server.h"
|
||||
#include "environment.h"
|
||||
#include "database/database.h"
|
||||
#include <algorithm>
|
||||
|
||||
// common start: ensure auth db
|
||||
AuthDatabase *ModApiAuth::getAuthDb(lua_State *L)
|
||||
{
|
||||
ServerEnvironment *server_environment =
|
||||
dynamic_cast<ServerEnvironment *>(getEnv(L));
|
||||
if (!server_environment)
|
||||
return nullptr;
|
||||
return server_environment->getAuthDatabase();
|
||||
}
|
||||
|
||||
void ModApiAuth::pushAuthEntry(lua_State *L, const AuthEntry &authEntry)
|
||||
{
|
||||
lua_newtable(L);
|
||||
int table = lua_gettop(L);
|
||||
// id
|
||||
lua_pushnumber(L, authEntry.id);
|
||||
lua_setfield(L, table, "id");
|
||||
// name
|
||||
lua_pushstring(L, authEntry.name.c_str());
|
||||
lua_setfield(L, table, "name");
|
||||
// password
|
||||
lua_pushstring(L, authEntry.password.c_str());
|
||||
lua_setfield(L, table, "password");
|
||||
// privileges
|
||||
lua_newtable(L);
|
||||
int privtable = lua_gettop(L);
|
||||
for (const std::string &privs : authEntry.privileges) {
|
||||
lua_pushboolean(L, true);
|
||||
lua_setfield(L, privtable, privs.c_str());
|
||||
}
|
||||
lua_setfield(L, table, "privileges");
|
||||
// last_login
|
||||
lua_pushnumber(L, authEntry.last_login);
|
||||
lua_setfield(L, table, "last_login");
|
||||
|
||||
lua_pushvalue(L, table);
|
||||
}
|
||||
|
||||
// auth_read(name)
|
||||
int ModApiAuth::l_auth_read(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
AuthDatabase *auth_db = getAuthDb(L);
|
||||
if (!auth_db)
|
||||
return 0;
|
||||
AuthEntry authEntry;
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
bool success = auth_db->getAuth(std::string(name), authEntry);
|
||||
if (!success)
|
||||
return 0;
|
||||
|
||||
pushAuthEntry(L, authEntry);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// auth_save(table)
|
||||
int ModApiAuth::l_auth_save(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
AuthDatabase *auth_db = getAuthDb(L);
|
||||
if (!auth_db)
|
||||
return 0;
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
int table = 1;
|
||||
AuthEntry authEntry;
|
||||
bool success;
|
||||
success = getintfield(L, table, "id", authEntry.id);
|
||||
success = success && getstringfield(L, table, "name", authEntry.name);
|
||||
success = success && getstringfield(L, table, "password", authEntry.password);
|
||||
lua_getfield(L, table, "privileges");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2)) {
|
||||
authEntry.privileges.emplace_back(
|
||||
lua_tostring(L, -2)); // the key, not the value
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
lua_pop(L, 1); // the table
|
||||
success = success && getintfield(L, table, "last_login", authEntry.last_login);
|
||||
|
||||
if (!success) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushboolean(L, auth_db->saveAuth(authEntry));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// auth_create(table)
|
||||
int ModApiAuth::l_auth_create(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
AuthDatabase *auth_db = getAuthDb(L);
|
||||
if (!auth_db)
|
||||
return 0;
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
int table = 1;
|
||||
AuthEntry authEntry;
|
||||
bool success;
|
||||
// no meaningful id field, we assume
|
||||
success = getstringfield(L, table, "name", authEntry.name);
|
||||
success = success && getstringfield(L, table, "password", authEntry.password);
|
||||
lua_getfield(L, table, "privileges");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2)) {
|
||||
authEntry.privileges.emplace_back(
|
||||
lua_tostring(L, -2)); // the key, not the value
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
lua_pop(L, 1); // the table
|
||||
success = success && getintfield(L, table, "last_login", authEntry.last_login);
|
||||
|
||||
if (!success)
|
||||
return 0;
|
||||
|
||||
if (auth_db->createAuth(authEntry)) {
|
||||
pushAuthEntry(L, authEntry);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// auth_delete(name)
|
||||
int ModApiAuth::l_auth_delete(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
AuthDatabase *auth_db = getAuthDb(L);
|
||||
if (!auth_db)
|
||||
return 0;
|
||||
std::string name(luaL_checkstring(L, 1));
|
||||
lua_pushboolean(L, auth_db->deleteAuth(name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// auth_list_names()
|
||||
int ModApiAuth::l_auth_list_names(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
AuthDatabase *auth_db = getAuthDb(L);
|
||||
if (!auth_db)
|
||||
return 0;
|
||||
std::vector<std::string> names;
|
||||
auth_db->listNames(names);
|
||||
lua_createtable(L, names.size(), 0);
|
||||
int table = lua_gettop(L);
|
||||
int i = 1;
|
||||
for (const std::string &name : names) {
|
||||
lua_pushstring(L, name.c_str());
|
||||
lua_rawseti(L, table, i++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// auth_reload()
|
||||
int ModApiAuth::l_auth_reload(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
AuthDatabase *auth_db = getAuthDb(L);
|
||||
if (auth_db)
|
||||
auth_db->reload();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ModApiAuth::Initialize(lua_State *L, int top)
|
||||
{
|
||||
|
||||
lua_newtable(L);
|
||||
int auth_top = lua_gettop(L);
|
||||
|
||||
registerFunction(L, "read", l_auth_read, auth_top);
|
||||
registerFunction(L, "save", l_auth_save, auth_top);
|
||||
registerFunction(L, "create", l_auth_create, auth_top);
|
||||
registerFunction(L, "delete", l_auth_delete, auth_top);
|
||||
registerFunction(L, "list_names", l_auth_list_names, auth_top);
|
||||
registerFunction(L, "reload", l_auth_reload, auth_top);
|
||||
|
||||
lua_setfield(L, top, "auth");
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lua_api/l_base.h"
|
||||
|
||||
class AuthDatabase;
|
||||
struct AuthEntry;
|
||||
|
||||
class ModApiAuth : public ModApiBase
|
||||
{
|
||||
private:
|
||||
// auth_read(name)
|
||||
static int l_auth_read(lua_State *L);
|
||||
|
||||
// auth_save(table)
|
||||
static int l_auth_save(lua_State *L);
|
||||
|
||||
// auth_create(table)
|
||||
static int l_auth_create(lua_State *L);
|
||||
|
||||
// auth_delete(name)
|
||||
static int l_auth_delete(lua_State *L);
|
||||
|
||||
// auth_list_names()
|
||||
static int l_auth_list_names(lua_State *L);
|
||||
|
||||
// auth_reload()
|
||||
static int l_auth_reload(lua_State *L);
|
||||
|
||||
// helper for auth* methods
|
||||
static AuthDatabase *getAuthDb(lua_State *L);
|
||||
static void pushAuthEntry(lua_State *L, const AuthEntry &authEntry);
|
||||
|
||||
public:
|
||||
static void Initialize(lua_State *L, int top);
|
||||
};
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "settings.h"
|
||||
#include "cpp_api/s_internal.h"
|
||||
#include "lua_api/l_areastore.h"
|
||||
#include "lua_api/l_auth.h"
|
||||
#include "lua_api/l_base.h"
|
||||
#include "lua_api/l_craft.h"
|
||||
#include "lua_api/l_env.h"
|
||||
|
@ -106,6 +107,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top)
|
|||
ModChannelRef::Register(L);
|
||||
|
||||
// Initialize mod api modules
|
||||
ModApiAuth::Initialize(L, top);
|
||||
ModApiCraft::Initialize(L, top);
|
||||
ModApiEnvMod::Initialize(L, top);
|
||||
ModApiInventory::Initialize(L, top);
|
||||
|
|
|
@ -414,6 +414,18 @@ ServerEnvironment::ServerEnvironment(ServerMap *map,
|
|||
std::string name;
|
||||
conf.getNoEx("player_backend", name);
|
||||
m_player_database = openPlayerDatabase(name, path_world, conf);
|
||||
|
||||
std::string auth_name = "files";
|
||||
if (conf.exists("auth_backend")) {
|
||||
conf.getNoEx("auth_backend", auth_name);
|
||||
} else {
|
||||
conf.set("auth_backend", "files");
|
||||
if (!conf.updateConfigFile(conf_path.c_str())) {
|
||||
errorstream << "ServerEnvironment::ServerEnvironment(): "
|
||||
<< "Failed to update world.mt!" << std::endl;
|
||||
}
|
||||
}
|
||||
m_auth_database = openAuthDatabase(auth_name, path_world, conf);
|
||||
}
|
||||
|
||||
ServerEnvironment::~ServerEnvironment()
|
||||
|
@ -439,6 +451,7 @@ ServerEnvironment::~ServerEnvironment()
|
|||
}
|
||||
|
||||
delete m_player_database;
|
||||
delete m_auth_database;
|
||||
}
|
||||
|
||||
Map & ServerEnvironment::getMap()
|
||||
|
@ -2274,3 +2287,91 @@ bool ServerEnvironment::migratePlayersDatabase(const GameParams &game_params,
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
AuthDatabase *ServerEnvironment::openAuthDatabase(
|
||||
const std::string &name, const std::string &savedir, const Settings &conf)
|
||||
{
|
||||
if (name == "sqlite3")
|
||||
return new AuthDatabaseSQLite3(savedir);
|
||||
|
||||
if (name == "files")
|
||||
return new AuthDatabaseFiles(savedir);
|
||||
|
||||
throw BaseException(std::string("Database backend ") + name + " not supported.");
|
||||
}
|
||||
|
||||
bool ServerEnvironment::migrateAuthDatabase(
|
||||
const GameParams &game_params, const Settings &cmd_args)
|
||||
{
|
||||
std::string migrate_to = cmd_args.get("migrate-auth");
|
||||
Settings world_mt;
|
||||
std::string world_mt_path = game_params.world_path + DIR_DELIM + "world.mt";
|
||||
if (!world_mt.readConfigFile(world_mt_path.c_str())) {
|
||||
errorstream << "Cannot read world.mt!" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string backend = "files";
|
||||
if (world_mt.exists("auth_backend"))
|
||||
backend = world_mt.get("auth_backend");
|
||||
else
|
||||
warningstream << "No auth_backend found in world.mt, "
|
||||
"assuming \"files\"." << std::endl;
|
||||
|
||||
if (backend == migrate_to) {
|
||||
errorstream << "Cannot migrate: new backend is same"
|
||||
<< " as the old one" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const std::unique_ptr<AuthDatabase> srcdb(ServerEnvironment::openAuthDatabase(
|
||||
backend, game_params.world_path, world_mt));
|
||||
const std::unique_ptr<AuthDatabase> dstdb(ServerEnvironment::openAuthDatabase(
|
||||
migrate_to, game_params.world_path, world_mt));
|
||||
|
||||
std::vector<std::string> names_list;
|
||||
srcdb->listNames(names_list);
|
||||
for (const std::string &name : names_list) {
|
||||
actionstream << "Migrating auth entry for " << name << std::endl;
|
||||
bool success;
|
||||
AuthEntry authEntry;
|
||||
success = srcdb->getAuth(name, authEntry);
|
||||
success = success && dstdb->createAuth(authEntry);
|
||||
if (!success)
|
||||
errorstream << "Failed to migrate " << name << std::endl;
|
||||
}
|
||||
|
||||
actionstream << "Successfully migrated " << names_list.size()
|
||||
<< " auth entries" << std::endl;
|
||||
world_mt.set("auth_backend", migrate_to);
|
||||
if (!world_mt.updateConfigFile(world_mt_path.c_str()))
|
||||
errorstream << "Failed to update world.mt!" << std::endl;
|
||||
else
|
||||
actionstream << "world.mt updated" << std::endl;
|
||||
|
||||
if (backend == "files") {
|
||||
// special-case files migration:
|
||||
// move auth.txt to auth.txt.bak if possible
|
||||
std::string auth_txt_path =
|
||||
game_params.world_path + DIR_DELIM + "auth.txt";
|
||||
std::string auth_bak_path = auth_txt_path + ".bak";
|
||||
if (!fs::PathExists(auth_bak_path))
|
||||
if (fs::Rename(auth_txt_path, auth_bak_path))
|
||||
actionstream << "Renamed auth.txt to auth.txt.bak"
|
||||
<< std::endl;
|
||||
else
|
||||
errorstream << "Could not rename auth.txt to "
|
||||
"auth.txt.bak" << std::endl;
|
||||
else
|
||||
warningstream << "auth.txt.bak already exists, auth.txt "
|
||||
"not renamed" << std::endl;
|
||||
}
|
||||
|
||||
} catch (BaseException &e) {
|
||||
errorstream << "An error occured during migration: " << e.what()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ struct GameParams;
|
|||
class MapBlock;
|
||||
class RemotePlayer;
|
||||
class PlayerDatabase;
|
||||
class AuthDatabase;
|
||||
class PlayerSAO;
|
||||
class ServerEnvironment;
|
||||
class ActiveBlockModifier;
|
||||
|
@ -366,6 +367,10 @@ public:
|
|||
|
||||
static bool migratePlayersDatabase(const GameParams &game_params,
|
||||
const Settings &cmd_args);
|
||||
|
||||
AuthDatabase *getAuthDatabase() { return m_auth_database; }
|
||||
static bool migrateAuthDatabase(const GameParams &game_params,
|
||||
const Settings &cmd_args);
|
||||
private:
|
||||
|
||||
/**
|
||||
|
@ -375,6 +380,8 @@ private:
|
|||
|
||||
static PlayerDatabase *openPlayerDatabase(const std::string &name,
|
||||
const std::string &savedir, const Settings &conf);
|
||||
static AuthDatabase *openAuthDatabase(const std::string &name,
|
||||
const std::string &savedir, const Settings &conf);
|
||||
/*
|
||||
Internal ActiveObject interface
|
||||
-------------------------------------------
|
||||
|
@ -467,6 +474,7 @@ private:
|
|||
std::vector<RemotePlayer*> m_players;
|
||||
|
||||
PlayerDatabase *m_player_database = nullptr;
|
||||
AuthDatabase *m_auth_database = nullptr;
|
||||
|
||||
// Particles
|
||||
IntervalLimiter m_particle_management_interval;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
set (UNITTEST_SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_authdatabase.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_activeobject.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_areastore.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_ban.cpp
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "test.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "database/database-files.h"
|
||||
#include "database/database-sqlite3.h"
|
||||
#include "util/string.h"
|
||||
#include "filesys.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Anonymous namespace to create classes that are only
|
||||
// visible to this file
|
||||
//
|
||||
// These are helpers that return a *AuthDatabase and
|
||||
// allow us to run the same tests on different databases and
|
||||
// database acquisition strategies.
|
||||
|
||||
class AuthDatabaseProvider
|
||||
{
|
||||
public:
|
||||
virtual ~AuthDatabaseProvider() = default;
|
||||
virtual AuthDatabase *getAuthDatabase() = 0;
|
||||
};
|
||||
|
||||
class FixedProvider : public AuthDatabaseProvider
|
||||
{
|
||||
public:
|
||||
FixedProvider(AuthDatabase *auth_db) : auth_db(auth_db){};
|
||||
virtual ~FixedProvider(){};
|
||||
virtual AuthDatabase *getAuthDatabase() { return auth_db; };
|
||||
|
||||
private:
|
||||
AuthDatabase *auth_db;
|
||||
};
|
||||
|
||||
class FilesProvider : public AuthDatabaseProvider
|
||||
{
|
||||
public:
|
||||
FilesProvider(const std::string &dir) : dir(dir){};
|
||||
virtual ~FilesProvider() { delete auth_db; };
|
||||
virtual AuthDatabase *getAuthDatabase()
|
||||
{
|
||||
delete auth_db;
|
||||
auth_db = new AuthDatabaseFiles(dir);
|
||||
return auth_db;
|
||||
};
|
||||
|
||||
private:
|
||||
std::string dir;
|
||||
AuthDatabase *auth_db = nullptr;
|
||||
};
|
||||
|
||||
class SQLite3Provider : public AuthDatabaseProvider
|
||||
{
|
||||
public:
|
||||
SQLite3Provider(const std::string &dir) : dir(dir){};
|
||||
virtual ~SQLite3Provider() { delete auth_db; };
|
||||
virtual AuthDatabase *getAuthDatabase()
|
||||
{
|
||||
delete auth_db;
|
||||
auth_db = new AuthDatabaseSQLite3(dir);
|
||||
return auth_db;
|
||||
};
|
||||
|
||||
private:
|
||||
std::string dir;
|
||||
AuthDatabase *auth_db = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
class TestAuthDatabase : public TestBase
|
||||
{
|
||||
public:
|
||||
TestAuthDatabase()
|
||||
{
|
||||
TestManager::registerTestModule(this);
|
||||
// fixed directory, for persistence
|
||||
test_dir = getTestTempDirectory();
|
||||
}
|
||||
const char *getName() { return "TestAuthDatabase"; }
|
||||
|
||||
void runTests(IGameDef *gamedef);
|
||||
void runTestsForCurrentDB();
|
||||
|
||||
void testRecallFail();
|
||||
void testCreate();
|
||||
void testRecall();
|
||||
void testChange();
|
||||
void testRecallChanged();
|
||||
void testChangePrivileges();
|
||||
void testRecallChangedPrivileges();
|
||||
void testListNames();
|
||||
void testDelete();
|
||||
|
||||
private:
|
||||
std::string test_dir;
|
||||
AuthDatabaseProvider *auth_provider;
|
||||
};
|
||||
|
||||
static TestAuthDatabase g_test_instance;
|
||||
|
||||
void TestAuthDatabase::runTests(IGameDef *gamedef)
|
||||
{
|
||||
// Each set of tests is run twice for each database type:
|
||||
// one where we reuse the same AuthDatabase object (to test local caching),
|
||||
// and one where we create a new AuthDatabase object for each call
|
||||
// (to test actual persistence).
|
||||
|
||||
rawstream << "-------- Files database (same object)" << std::endl;
|
||||
|
||||
AuthDatabase *auth_db = new AuthDatabaseFiles(test_dir);
|
||||
auth_provider = new FixedProvider(auth_db);
|
||||
|
||||
runTestsForCurrentDB();
|
||||
|
||||
delete auth_db;
|
||||
delete auth_provider;
|
||||
|
||||
// reset database
|
||||
fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "auth.txt");
|
||||
|
||||
rawstream << "-------- Files database (new objects)" << std::endl;
|
||||
|
||||
auth_provider = new FilesProvider(test_dir);
|
||||
|
||||
runTestsForCurrentDB();
|
||||
|
||||
delete auth_provider;
|
||||
|
||||
rawstream << "-------- SQLite3 database (same object)" << std::endl;
|
||||
|
||||
auth_db = new AuthDatabaseSQLite3(test_dir);
|
||||
auth_provider = new FixedProvider(auth_db);
|
||||
|
||||
runTestsForCurrentDB();
|
||||
|
||||
delete auth_db;
|
||||
delete auth_provider;
|
||||
|
||||
// reset database
|
||||
fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "auth.sqlite");
|
||||
|
||||
rawstream << "-------- SQLite3 database (new objects)" << std::endl;
|
||||
|
||||
auth_provider = new SQLite3Provider(test_dir);
|
||||
|
||||
runTestsForCurrentDB();
|
||||
|
||||
delete auth_provider;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void TestAuthDatabase::runTestsForCurrentDB()
|
||||
{
|
||||
TEST(testRecallFail);
|
||||
TEST(testCreate);
|
||||
TEST(testRecall);
|
||||
TEST(testChange);
|
||||
TEST(testRecallChanged);
|
||||
TEST(testChangePrivileges);
|
||||
TEST(testRecallChangedPrivileges);
|
||||
TEST(testListNames);
|
||||
TEST(testDelete);
|
||||
TEST(testRecallFail);
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testRecallFail()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
// no such user yet
|
||||
UASSERT(!auth_db->getAuth("TestName", authEntry));
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testCreate()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
authEntry.name = "TestName";
|
||||
authEntry.password = "TestPassword";
|
||||
authEntry.privileges.emplace_back("shout");
|
||||
authEntry.privileges.emplace_back("interact");
|
||||
authEntry.last_login = 1000;
|
||||
UASSERT(auth_db->createAuth(authEntry));
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testRecall()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
UASSERT(auth_db->getAuth("TestName", authEntry));
|
||||
UASSERTEQ(std::string, authEntry.name, "TestName");
|
||||
UASSERTEQ(std::string, authEntry.password, "TestPassword");
|
||||
// the order of privileges is unimportant
|
||||
std::sort(authEntry.privileges.begin(), authEntry.privileges.end());
|
||||
UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "interact,shout");
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testChange()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
UASSERT(auth_db->getAuth("TestName", authEntry));
|
||||
authEntry.password = "NewPassword";
|
||||
authEntry.last_login = 1002;
|
||||
UASSERT(auth_db->saveAuth(authEntry));
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testRecallChanged()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
UASSERT(auth_db->getAuth("TestName", authEntry));
|
||||
UASSERTEQ(std::string, authEntry.password, "NewPassword");
|
||||
// the order of privileges is unimportant
|
||||
std::sort(authEntry.privileges.begin(), authEntry.privileges.end());
|
||||
UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "interact,shout");
|
||||
UASSERTEQ(u64, authEntry.last_login, 1002);
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testChangePrivileges()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
UASSERT(auth_db->getAuth("TestName", authEntry));
|
||||
authEntry.privileges.clear();
|
||||
authEntry.privileges.emplace_back("interact");
|
||||
authEntry.privileges.emplace_back("fly");
|
||||
authEntry.privileges.emplace_back("dig");
|
||||
UASSERT(auth_db->saveAuth(authEntry));
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testRecallChangedPrivileges()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
AuthEntry authEntry;
|
||||
|
||||
UASSERT(auth_db->getAuth("TestName", authEntry));
|
||||
// the order of privileges is unimportant
|
||||
std::sort(authEntry.privileges.begin(), authEntry.privileges.end());
|
||||
UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "dig,fly,interact");
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testListNames()
|
||||
{
|
||||
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
std::vector<std::string> list;
|
||||
|
||||
AuthEntry authEntry;
|
||||
|
||||
authEntry.name = "SecondName";
|
||||
authEntry.password = "SecondPassword";
|
||||
authEntry.privileges.emplace_back("shout");
|
||||
authEntry.privileges.emplace_back("interact");
|
||||
authEntry.last_login = 1003;
|
||||
auth_db->createAuth(authEntry);
|
||||
|
||||
auth_db->listNames(list);
|
||||
// not necessarily sorted, so sort before comparing
|
||||
std::sort(list.begin(), list.end());
|
||||
UASSERTEQ(std::string, str_join(list, ","), "SecondName,TestName");
|
||||
}
|
||||
|
||||
void TestAuthDatabase::testDelete()
|
||||
{
|
||||
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
|
||||
|
||||
UASSERT(!auth_db->deleteAuth("NoSuchName"));
|
||||
UASSERT(auth_db->deleteAuth("TestName"));
|
||||
// second try, expect failure
|
||||
UASSERT(!auth_db->deleteAuth("TestName"));
|
||||
}
|
|
@ -51,6 +51,7 @@ public:
|
|||
void testIsNumber();
|
||||
void testIsPowerOfTwo();
|
||||
void testMyround();
|
||||
void testStringJoin();
|
||||
};
|
||||
|
||||
static TestUtilities g_test_instance;
|
||||
|
@ -78,6 +79,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
|
|||
TEST(testIsNumber);
|
||||
TEST(testIsPowerOfTwo);
|
||||
TEST(testMyround);
|
||||
TEST(testStringJoin);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -328,3 +330,24 @@ void TestUtilities::testMyround()
|
|||
UASSERT(myround(-6.5f) == -7);
|
||||
}
|
||||
|
||||
void TestUtilities::testStringJoin()
|
||||
{
|
||||
std::vector<std::string> input;
|
||||
UASSERT(str_join(input, ",") == "");
|
||||
|
||||
input.emplace_back("one");
|
||||
UASSERT(str_join(input, ",") == "one");
|
||||
|
||||
input.emplace_back("two");
|
||||
UASSERT(str_join(input, ",") == "one,two");
|
||||
|
||||
input.emplace_back("three");
|
||||
UASSERT(str_join(input, ",") == "one,two,three");
|
||||
|
||||
input[1] = "";
|
||||
UASSERT(str_join(input, ",") == "one,,three");
|
||||
|
||||
input[1] = "two";
|
||||
UASSERT(str_join(input, " and ") == "one and two and three");
|
||||
}
|
||||
|
||||
|
|
|
@ -704,3 +704,22 @@ inline const std::string duration_to_string(int sec)
|
|||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a vector of strings by the string \p delimiter.
|
||||
*
|
||||
* @return A std::string
|
||||
*/
|
||||
inline std::string str_join(const std::vector<std::string> &list,
|
||||
const std::string &delimiter)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
bool first = true;
|
||||
for (const auto &part : list) {
|
||||
if (!first)
|
||||
oss << delimiter;
|
||||
oss << part;
|
||||
first = false;
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue