Browse Source

Replace "auth.txt" with "auth.sqlite" (SQLite3 "auth" database)

Port of Old MT commit 153fb211ac
From: Ben Deutsch <ben@bendeutsch.de>
Date: Sun, 5 Aug 2018 13:13:38 +0200

This commit both  includes and extends the Old MT commit indicated
above.

One new feature is added:

Both  Old MT and Final MT check  "world.mt" for an  "auth_backend"
setting.

If there is none:

(a) Old MT adds "auth_backend = files" to "world.mt" and uses that
mode.

(b) Final MT  checks  to see if  "auth.txt" exists.  If "auth.txt"
exists,  Final MT adds  "auth_backend = files"  to  "world.mt" and
uses that mode.  However,  if "auth.txt" doesn't  exist,  Final MT
adds  "auth_backend = sqlite3" instead to "world.mt" and uses that
mode.

In short, by default, Final MT uses  "auth.txt" for old worlds and
"auth.sqlite" for new worlds.
master
OldCoder 8 months ago
parent
commit
bac05b1b72

+ 2
- 0
build/android/jni/Android.mk View File

@@ -245,6 +245,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 \
@@ -307,6 +308,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 \

+ 74
- 94
builtin/game/auth.lua View File

@@ -1,75 +1,25 @@
-- Minetest: builtin/auth.lua

--
-- Builtin authentication handler
--

core.auth_file_path = core.get_worldpath().."/auth.txt"
core.auth_table = {}

local function read_auth_file()
local newtable = {}
local file, errmsg = io.open(core.auth_file_path, 'rb')
if not file then
core.log("info", core.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)
core.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(core.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(core.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(core.auth_file_path, table.concat(content, "\n")) then
error(core.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 core.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(core.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
@@ -82,78 +32,125 @@ core.builtin_auth_handler = {
-- For the admin, give everything
elseif name == core.settings:get("name") then
for priv, def in pairs(core.registered_privileges) do
privileges[priv] = true
if def.give_to_admin then
privileges[priv] = true
end
end
end
-- All done
return {
password = core.auth_table[name].password,
password = auth_entry.password,
privileges = privileges,
-- Is set to nil if unknown
last_login = core.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.."'")
core.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")
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.."'")
return core_auth.delete(name)
end,
set_password = function(name, password)
assert(type(name) == "string")
assert(type(password) == "string")
if not core.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.."'")
core.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 core.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 core.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(core.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

core.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(core.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 = {}
local nameslist = core_auth.list_names()
for k,v in pairs(nameslist) do
names[v] = true
end
return pairs(names)
end,
}

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
local auth_entry = core_auth.read(name)
if auth_entry ~= nil then
return
end

local name_lower = name:lower()
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. "..
"Please check the spelling if it's your account "..
"or use a different nickname.", name, k)
end
end
end)

--
-- Authentication API
--

function core.register_authentication_handler(handler)
if core.registered_auth_handler then
error("Add-on authentication handler already registered by "..core.registered_auth_handler_modname)
@@ -179,28 +176,10 @@ end

core.set_player_password = auth_pass("set_password")
core.set_player_privs = auth_pass("set_privileges")
core.remove_player_auth = auth_pass("delete_auth")
core.auth_reload = auth_pass("reload")


local record_login = auth_pass("record_login")

core.register_on_joinplayer(function(player)
record_login(player:get_player_name())
end)

core.register_on_prejoinplayer(function(name, ip)
local auth = core.auth_table
if auth[name] ~= nil then
return
end

local name_lower = name:lower()
for k in pairs(auth) do
if k:lower() == name_lower then
return string.format("\nCannot create new player called '%s'. "..
"Another account called '%s' is already registered. "..
"Please check the spelling if it's your account "..
"or use a different nickname.", name, k)
end
end
end)

+ 92
- 92
clientmods/preview/init.lua View File

@@ -5,7 +5,7 @@ local mod_channel
dofile("preview:example.lua")
-- This is an example function to ensure it's working properly, should be removed before merge
core.register_on_shutdown(function()
print("[PREVIEW] shutdown client")
print("[PREVIEW] shutdown client")
end)
local id = nil

@@ -16,158 +16,158 @@ print("Server address: " .. server_info.address)
print("Server port: " .. server_info.port)

core.register_on_inventory_open(function(inventory)
print("INVENTORY OPEN")
print(dump(inventory))
return false
print("INVENTORY OPEN")
print(dump(inventory))
return false
end)

core.register_on_placenode(function(pointed_thing, node)
print("The local player place a node!")
print("pointed_thing :" .. dump(pointed_thing))
print("node placed :" .. dump(node))
return false
print("The local player place a node!")
print("pointed_thing :" .. dump(pointed_thing))
print("node placed :" .. dump(node))
return false
end)

core.register_on_item_use(function(itemstack, pointed_thing)
print("The local player used an item!")
print("pointed_thing :" .. dump(pointed_thing))
print("item = " .. itemstack:get_name())
return false
print("The local player used an item!")
print("pointed_thing :" .. dump(pointed_thing))
print("item = " .. itemstack:get_name())
return false
end)

-- This is an example function to ensure it's working properly, should be removed before merge
core.register_on_receiving_chat_message(function(message)
print("[PREVIEW] Received message " .. message)
return false
print("[PREVIEW] Received message " .. message)
return false
end)

-- This is an example function to ensure it's working properly, should be removed before merge
core.register_on_sending_chat_message(function(message)
print("[PREVIEW] Sending message " .. message)
return false
print("[PREVIEW] Sending message " .. message)
return false
end)

-- This is an example function to ensure it's working properly, should be removed before merge
core.register_on_hp_modification(function(hp)
print("[PREVIEW] HP modified " .. hp)
print("[PREVIEW] HP modified " .. hp)
end)

-- This is an example function to ensure it's working properly, should be removed before merge
core.register_on_damage_taken(function(hp)
print("[PREVIEW] Damage taken " .. hp)
print("[PREVIEW] Damage taken " .. hp)
end)

-- This is an example function to ensure it's working properly, should be removed before merge
core.register_globalstep(function(dtime)
-- print("[PREVIEW] globalstep " .. dtime)
-- print("[PREVIEW] globalstep " .. dtime)
end)

-- This is an example function to ensure it's working properly, should be removed before merge
core.register_chatcommand("dump", {
func = function(param)
return true, dump(_G)
end,
func = function(param)
return true, dump(_G)
end,
})

core.register_chatcommand("colorize_test", {
func = function(param)
return true, core.colorize("red", param)
end,
func = function(param)
return true, core.colorize("red", param)
end,
})

core.register_chatcommand("test_node", {
func = function(param)
core.display_chat_message(dump(core.get_node({x=0, y=0, z=0})))
core.display_chat_message(dump(core.get_node_or_nil({x=0, y=0, z=0})))
end,
func = function(param)
core.display_chat_message(dump(core.get_node({x=0, y=0, z=0})))
core.display_chat_message(dump(core.get_node_or_nil({x=0, y=0, z=0})))
end,
})

local function preview_minimap()
local minimap = core.ui.minimap
if not minimap then
print("[PREVIEW] Minimap is disabled. Skipping.")
return
end
minimap:set_mode(4)
minimap:show()
minimap:set_pos({x=5, y=50, z=5})
minimap:set_shape(math.random(0, 1))
print("[PREVIEW] Minimap: mode => " .. dump(minimap:get_mode()) ..
" position => " .. dump(minimap:get_pos()) ..
" angle => " .. dump(minimap:get_angle()))
local minimap = core.ui.minimap
if not minimap then
print("[PREVIEW] Minimap is disabled. Skipping.")
return
end
minimap:set_mode(4)
minimap:show()
minimap:set_pos({x=5, y=50, z=5})
minimap:set_shape(math.random(0, 1))
print("[PREVIEW] Minimap: mode => " .. dump(minimap:get_mode()) ..
" position => " .. dump(minimap:get_pos()) ..
" angle => " .. dump(minimap:get_angle()))
end

core.after(2, function()
print("[PREVIEW] loaded " .. modname .. " mod")
modstorage:set_string("current_mod", modname)
print(modstorage:get_string("current_mod"))
preview_minimap()
print("[PREVIEW] loaded " .. modname .. " mod")
modstorage:set_string("current_mod", modname)
print(modstorage:get_string("current_mod"))
preview_minimap()
end)

core.after(5, function()
if core.ui.minimap then
core.ui.minimap:show()
end
if core.ui.minimap then
core.ui.minimap:show()
end

print("[PREVIEW] Day count: " .. core.get_day_count() ..
" time of day " .. core.get_timeofday())
print("[PREVIEW] Day count: " .. core.get_day_count() ..
" time of day " .. core.get_timeofday())

print("[PREVIEW] Node level: " .. core.get_node_level({x=0, y=20, z=0}) ..
" max level " .. core.get_node_max_level({x=0, y=20, z=0}))
print("[PREVIEW] Node level: " .. core.get_node_level({x=0, y=20, z=0}) ..
" max level " .. core.get_node_max_level({x=0, y=20, z=0}))

print("[PREVIEW] Find node near: " .. dump(core.find_node_near({x=0, y=20, z=0}, 10,
{"group:tree", "default:dirt", "default:stone"})))
print("[PREVIEW] Find node near: " .. dump(core.find_node_near({x=0, y=20, z=0}, 10,
{"group:tree", "default:dirt", "default:stone"})))
end)

core.register_on_dignode(function(pos, node)
print("The local player dug a node!")
print("pos:" .. dump(pos))
print("node:" .. dump(node))
return false
print("The local player dug a node!")
print("pos:" .. dump(pos))
print("node:" .. dump(node))
return false
end)

core.register_on_punchnode(function(pos, node)
print("The local player punched a node!")
local itemstack = core.get_wielded_item()
--[[
-- getters
print(dump(itemstack:is_empty()))
print(dump(itemstack:get_name()))
print(dump(itemstack:get_count()))
print(dump(itemstack:get_wear()))
print(dump(itemstack:get_meta()))
print(dump(itemstack:get_metadata()
print(dump(itemstack:is_known()))
--print(dump(itemstack:get_definition()))
print(dump(itemstack:get_tool_capabilities()))
print(dump(itemstack:to_string()))
print(dump(itemstack:to_table()))
-- setters
print(dump(itemstack:set_name("default:dirt")))
print(dump(itemstack:set_count("95")))
print(dump(itemstack:set_wear(934)))
print(dump(itemstack:get_meta()))
print(dump(itemstack:get_metadata()))
--]]
print(dump(itemstack:to_table()))
print("pos:" .. dump(pos))
print("node:" .. dump(node))
return false
print("The local player punched a node!")
local itemstack = core.get_wielded_item()
--[[
-- getters
print(dump(itemstack:is_empty()))
print(dump(itemstack:get_name()))
print(dump(itemstack:get_count()))
print(dump(itemstack:get_wear()))
print(dump(itemstack:get_meta()))
print(dump(itemstack:get_metadata()
print(dump(itemstack:is_known()))
--print(dump(itemstack:get_definition()))
print(dump(itemstack:get_tool_capabilities()))
print(dump(itemstack:to_string()))
print(dump(itemstack:to_table()))
-- setters
print(dump(itemstack:set_name("default:dirt")))
print(dump(itemstack:set_count("95")))
print(dump(itemstack:set_wear(934)))
print(dump(itemstack:get_meta()))
print(dump(itemstack:get_metadata()))
--]]
print(dump(itemstack:to_table()))
print("pos:" .. dump(pos))
print("node:" .. dump(node))
return false
end)

core.register_chatcommand("privs", {
func = function(param)
return true, core.privs_to_string(minetest.get_privilege_list())
end,
func = function(param)
return true, core.privs_to_string(minetest.get_privilege_list())
end,
})

core.register_chatcommand("text", {
func = function(param)
return core.localplayer:hud_change(id, "text", param)
end,
func = function(param)
return core.localplayer:hud_change(id, "text", param)
end,
})

core.register_on_mods_loaded(function()
core.log("Yeah preview mod is loaded with other CSM mods.")
core.log("Yeah preview mod is loaded with other CSM mods.")
end)

+ 30
- 0
doc/world_format.txt View File

@@ -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
===================

+ 103
- 11
src/database-files.cpp View File

@@ -25,15 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h"
#include "porting.h"
#include "filesys.h"

// !!! WARNING !!!
// This backend is intended to be used on Minetest 0.4.16 only for the transition backend
// for player files

PlayerDatabaseFiles::PlayerDatabaseFiles(const std::string &savedir) : m_savedir(savedir)
{
fs::CreateDir(m_savedir);
}
#include "util/string.h"

void PlayerDatabaseFiles::serialize(std::ostringstream &os, RemotePlayer *player)
{
@@ -62,8 +54,6 @@ void PlayerDatabaseFiles::serialize(std::ostringstream &os, RemotePlayer *player

void PlayerDatabaseFiles::savePlayer(RemotePlayer *player)
{
fs::CreateDir(m_savedir);

std::string savedir = m_savedir + DIR_DELIM;
std::string path = savedir + player->getName();
bool path_found = false;
@@ -184,3 +174,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;
}

+ 28
- 12
src/database-files.h View File

@@ -20,27 +20,43 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#ifndef DATABASE_FILES_HEADER
#define DATABASE_FILES_HEADER

// !!! WARNING !!!
// This backend is intended to be used on Minetest 0.4.16 only for the transition backend
// for player files

#include "database.h"
#include <unordered_map>

class PlayerDatabaseFiles : public PlayerDatabase
{
public:
PlayerDatabaseFiles(const std::string &savedir);
virtual ~PlayerDatabaseFiles() = default;
PlayerDatabaseFiles(const std::string &savedir) : m_savedir(savedir) {}
virtual ~PlayerDatabaseFiles() = default;

void savePlayer(RemotePlayer *player);
bool loadPlayer(RemotePlayer *player, PlayerSAO *sao);
bool removePlayer(const std::string &name);
void listPlayers(std::vector<std::string> &res);
void savePlayer(RemotePlayer *player);
bool loadPlayer(RemotePlayer *player, PlayerSAO *sao);
bool removePlayer(const std::string &name);
void listPlayers(std::vector<std::string> &res);

private:
void serialize(std::ostringstream &os, RemotePlayer *player);
void serialize(std::ostringstream &os, RemotePlayer *player);

std::string m_savedir;
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();
};
#endif

+ 167
- 0
src/database-sqlite3.cpp View File

@@ -604,3 +604,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);
}
}

+ 40
- 0
src/database-sqlite3.h View File

@@ -86,6 +86,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);
@@ -193,4 +203,34 @@ private:
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;
};
#endif

+ 22
- 0
src/database.h View File

@@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#ifndef DATABASE_HEADER
#define DATABASE_HEADER

#include <set>
#include <string>
#include <vector>
#include "irr_v3d.h"
@@ -63,4 +64,25 @@ public:
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;
};
#endif

+ 2
- 2
src/game.cpp View File

@@ -4847,7 +4847,7 @@ void the_game(bool *kill,

void Game::showDeathFormspec()
{
static std::string formspec =
static std::string formspec =
std::string(FORMSPEC_VERSION_STRING) +
SIZE_TAG
"bgcolor[#320000b4;true]"
@@ -4857,7 +4857,7 @@ void Game::showDeathFormspec()

FormspecFormSource *fs_src = new FormspecFormSource (formspec);
LocalFormspecHandler *txt_dst = new LocalFormspecHandler
("MT_DEATH_SCREEN", client);
("MT_DEATH_SCREEN", client);

create_formspec_menu (&current_formspec,
client, &input->joystick, fs_src, txt_dst);

+ 5
- 0
src/main.cpp View File

@@ -283,6 +283,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
@@ -834,6 +836,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;

+ 26
- 0
src/script/common/c_converter.cpp View File

@@ -519,6 +519,32 @@ bool getintfield(lua_State *L, int table,
return got;
}

bool getintfield(lua_State *L, int table,
const char *fieldname, u64 &result)
{
lua_getfield(L, table, fieldname);
bool got = false;
if(lua_isnumber(L, -1)){
result = lua_tointeger(L, -1);
got = true;
}
lua_pop(L, 1);
return got;
}

bool getintfield(lua_State *L, int table,
const char *fieldname, s64 &result)
{
lua_getfield(L, table, fieldname);
bool got = false;
if(lua_isnumber(L, -1)){
result = lua_tointeger(L, -1);
got = true;
}
lua_pop(L, 1);
return got;
}

bool getfloatfield(lua_State *L, int table,
const char *fieldname, float &result)
{

+ 6
- 0
src/script/common/c_converter.h View File

@@ -51,6 +51,7 @@ bool getstringfield(lua_State *L, int table,
size_t getstringlistfield(lua_State *L, int table,
const char *fieldname,
std::vector<std::string> *result);

bool getintfield(lua_State *L, int table,
const char *fieldname, int &result);
bool getintfield(lua_State *L, int table,
@@ -59,6 +60,11 @@ bool getintfield(lua_State *L, int table,
const char *fieldname, u16 &result);
bool getintfield(lua_State *L, int table,
const char *fieldname, u32 &result);
bool getintfield(lua_State *L, int table,
const char *fieldname, u64 &result);
bool getintfield(lua_State *L, int table,
const char *fieldname, s64 &result);

void read_groups(lua_State *L, int index,
std::unordered_map<std::string, int> &result);
bool getboolfield(lua_State *L, int table,

+ 6
- 6
src/script/cpp_api/s_client.cpp View File

@@ -27,13 +27,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,

void ScriptApiClient::on_mods_loaded()
{
SCRIPTAPI_PRECHECKHEADER
SCRIPTAPI_PRECHECKHEADER

// Get registered shutdown hooks
lua_getglobal(L, "core");
lua_getfield(L, -1, "registered_on_mods_loaded");
// Call callbacks
runCallbacks(0, RUN_CALLBACKS_MODE_FIRST);
// Get registered shutdown hooks
lua_getglobal(L, "core");
lua_getfield(L, -1, "registered_on_mods_loaded");
// Call callbacks
runCallbacks(0, RUN_CALLBACKS_MODE_FIRST);
}

void ScriptApiClient::on_shutdown()

+ 2
- 2
src/script/cpp_api/s_client.h View File

@@ -38,8 +38,8 @@ class ClientEnvironment;
class ScriptApiClient : virtual public ScriptApiBase
{
public:
// Calls when mods are loaded
void on_mods_loaded();
// Calls when mods are loaded
void on_mods_loaded();

// Calls on_shutdown handlers
void on_shutdown();

+ 6
- 6
src/script/cpp_api/s_server.cpp View File

@@ -149,13 +149,13 @@ bool ScriptApiServer::on_chat_message(const std::string &name,

void ScriptApiServer::on_mods_loaded()
{
SCRIPTAPI_PRECHECKHEADER
SCRIPTAPI_PRECHECKHEADER

// Get registered shutdown hooks
lua_getglobal(L, "core");
lua_getfield(L, -1, "registered_on_mods_loaded");
// Call callbacks
runCallbacks(0, RUN_CALLBACKS_MODE_FIRST);
// Get registered shutdown hooks
lua_getglobal(L, "core");
lua_getfield(L, -1, "registered_on_mods_loaded");
// Call callbacks
runCallbacks(0, RUN_CALLBACKS_MODE_FIRST);
}

void ScriptApiServer::on_shutdown()

+ 2
- 2
src/script/cpp_api/s_server.h View File

@@ -31,8 +31,8 @@ public:
// Returns true if script handled message
bool on_chat_message(const std::string &name, const std::string &message);

// Calls when mods are loaded
void on_mods_loaded();
// Calls when mods are loaded
void on_mods_loaded();

// Calls on_shutdown handlers
void on_shutdown();

+ 1
- 0
src/script/lua_api/CMakeLists.txt View File

@@ -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

+ 216
- 0
src/script/lua_api/l_auth.cpp View File

@@ -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.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");
}

+ 54
- 0
src/script/lua_api/l_auth.h View File

@@ -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);
};

+ 2
- 0
src/script/scripting_server.cpp View File

@@ -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"
@@ -102,6 +103,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top)
StorageRef::Register(L);

// Initialize mod api modules
ModApiAuth::Initialize(L, top);
ModApiCraft::Initialize(L, top);
ModApiEnvMod::Initialize(L, top);
ModApiInventory::Initialize(L, top);

+ 122
- 4
src/serverenvironment.cpp View File

@@ -17,6 +17,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include <stdio.h>

#include "serverenvironment.h"
#include "content_sao.h"
#include "settings.h"
@@ -398,10 +400,6 @@ ServerEnvironment::ServerEnvironment(ServerMap *map,
if (!succeeded || !conf.exists("player_backend")) {
// fall back to files
conf.set("player_backend", "files");
warningstream << "/!\\ You are using old player file backend. "
<< "This backend is deprecated and will be removed in next release /!\\"
<< std::endl << "Switching to SQLite3 or PostgreSQL is advised, "
<< "please read http://wiki.minetest.net/Database_backends." << std::endl;

if (!conf.updateConfigFile(conf_path.c_str())) {
errorstream << "ServerEnvironment::ServerEnvironment(): "
@@ -412,6 +410,37 @@ 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
{
std::string txt_path = path_world + DIR_DELIM + "auth.txt";
FILE *ifp;

if ((ifp = fopen (txt_path.c_str(), "r")) != NULL)
{
fclose (ifp);
}
else
{
auth_name = "sqlite3";
}

conf.set ("auth_backend", auth_name.c_str());

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()
@@ -437,6 +466,7 @@ ServerEnvironment::~ServerEnvironment()
}

delete m_player_database;
delete m_auth_database;
}

Map & ServerEnvironment::getMap()
@@ -2269,3 +2299,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;
}

+ 8
- 0
src/serverenvironment.h View File

@@ -33,6 +33,7 @@ struct GameParams;
class MapBlock;
class RemotePlayer;
class PlayerDatabase;
class AuthDatabase;
class PlayerSAO;
class ServerEnvironment;
class ActiveBlockModifier;
@@ -354,6 +355,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:

/**
@@ -363,6 +368,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
-------------------------------------------
@@ -455,6 +462,7 @@ private:
std::vector<RemotePlayer*> m_players;

PlayerDatabase *m_player_database = nullptr;
AuthDatabase *m_auth_database = nullptr;

// Particles
IntervalLimiter m_particle_management_interval;

+ 1
- 0
src/unittest/CMakeLists.txt View File

@@ -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

+ 299
- 0
src/unittest/test_authdatabase.cpp View File

@@ -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-files.h"
#include "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"));
}

+ 23
- 0
src/unittest/test_utilities.cpp View File

@@ -50,6 +50,7 @@ public:
void testIsNumber();
void testIsPowerOfTwo();
void testMyround();
void testStringJoin();
};

static TestUtilities g_test_instance;
@@ -77,6 +78,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
TEST(testIsNumber);
TEST(testIsPowerOfTwo);
TEST(testMyround);
TEST(testStringJoin);
}

////////////////////////////////////////////////////////////////////////////////
@@ -325,3 +327,24 @@ void TestUtilities::testMyround()
UASSERT(myround(-3.1f) == -3);
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");
}

+ 18
- 1
src/util/string.h View File

@@ -650,5 +650,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();
}
#endif

Loading…
Cancel
Save