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