Index: ps/trunk/source/ps/ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/ConfigDB.cpp (revision 21866) +++ ps/trunk/source/ps/ConfigDB.cpp (revision 21867) @@ -1,464 +1,469 @@ /* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ConfigDB.h" #include #include "lib/allocators/shared_ptr.h" +#include "lib/file/vfs/vfs_path.h" #include "ps/CLogger.h" +#include "ps/CStr.h" #include "ps/Filesystem.h" #include "ps/ThreadUtil.h" +#include + typedef std::map TConfigMap; TConfigMap CConfigDB::m_Map[CFG_LAST]; VfsPath CConfigDB::m_ConfigFile[CFG_LAST]; bool CConfigDB::m_HasChanges[CFG_LAST]; static pthread_mutex_t cfgdb_mutex = PTHREAD_MUTEX_INITIALIZER; -// These entries will not be printed to logfiles -static const std::set g_UnloggedEntries = { +// These entries will not be printed to logfiles, so that logfiles can be shared without leaking personal or sensitive data +static const std::unordered_set g_UnloggedEntries = { "lobby.password", - "lobby.buddies" + "lobby.buddies", + "userreport.id" // authentication token for GDPR personal data requests }; CConfigDB::CConfigDB() { // Recursive mutex needed for WriteFile pthread_mutexattr_t attr; int err; err = pthread_mutexattr_init(&attr); ENSURE(err == 0); err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); ENSURE(err == 0); err = pthread_mutex_init(&cfgdb_mutex, &attr); ENSURE(err == 0); err = pthread_mutexattr_destroy(&attr); ENSURE(err == 0); } #define CHECK_NS(rval)\ do {\ if (ns < 0 || ns >= CFG_LAST)\ {\ debug_warn(L"CConfigDB: Invalid ns value");\ return rval;\ }\ } while (false) namespace { template void Get(const CStr& value, T& ret) { std::stringstream ss(value); ss >> ret; } template<> void Get<>(const CStr& value, bool& ret) { ret = value == "true"; } template<> void Get<>(const CStr& value, std::string& ret) { ret = value; } std::string EscapeString(const CStr& str) { std::string ret; for (size_t i = 0; i < str.length(); ++i) { if (str[i] == '\\') ret += "\\\\"; else if (str[i] == '"') ret += "\\\""; else ret += str[i]; } return ret; } } // namespace #define GETVAL(type)\ void CConfigDB::GetValue(EConfigNamespace ns, const CStr& name, type& value)\ {\ CHECK_NS(;);\ CScopeLock s(&cfgdb_mutex);\ TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name);\ if (it != m_Map[CFG_COMMAND].end())\ {\ Get(it->second[0], value);\ return;\ }\ for (int search_ns = ns; search_ns >= 0; --search_ns)\ {\ it = m_Map[search_ns].find(name);\ if (it != m_Map[search_ns].end())\ {\ Get(it->second[0], value);\ return;\ }\ }\ } GETVAL(bool) GETVAL(int) GETVAL(u32) GETVAL(float) GETVAL(double) GETVAL(std::string) #undef GETVAL bool CConfigDB::HasChanges(EConfigNamespace ns) const { CHECK_NS(false); CScopeLock s(&cfgdb_mutex); return m_HasChanges[ns]; } void CConfigDB::SetChanges(EConfigNamespace ns, bool value) { CHECK_NS(;); CScopeLock s(&cfgdb_mutex); m_HasChanges[ns] = value; } void CConfigDB::GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values) const { CHECK_NS(;); CScopeLock s(&cfgdb_mutex); TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); if (it != m_Map[CFG_COMMAND].end()) { values = it->second; return; } for (int search_ns = ns; search_ns >= 0; --search_ns) { it = m_Map[search_ns].find(name); if (it != m_Map[search_ns].end()) { values = it->second; return; } } } EConfigNamespace CConfigDB::GetValueNamespace(EConfigNamespace ns, const CStr& name) const { CHECK_NS(CFG_LAST); CScopeLock s(&cfgdb_mutex); TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); if (it != m_Map[CFG_COMMAND].end()) return CFG_COMMAND; for (int search_ns = ns; search_ns >= 0; --search_ns) { it = m_Map[search_ns].find(name); if (it != m_Map[search_ns].end()) return (EConfigNamespace)search_ns; } return CFG_LAST; } std::map CConfigDB::GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix) const { CScopeLock s(&cfgdb_mutex); std::map ret; CHECK_NS(ret); // Loop upwards so that values in later namespaces can override // values in earlier namespaces for (int search_ns = 0; search_ns <= ns; ++search_ns) for (const std::pair& p : m_Map[search_ns]) if (boost::algorithm::starts_with(p.first, prefix)) ret[p.first] = p.second; for (const std::pair& p : m_Map[CFG_COMMAND]) if (boost::algorithm::starts_with(p.first, prefix)) ret[p.first] = p.second; return ret; } void CConfigDB::SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value) { CHECK_NS(;); CScopeLock s(&cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1))); it->second[0] = value; } void CConfigDB::SetValueBool(EConfigNamespace ns, const CStr& name, const bool value) { CStr valueString = value ? "true" : "false"; SetValueString(ns, name, valueString); } void CConfigDB::RemoveValue(EConfigNamespace ns, const CStr& name) { CHECK_NS(;); CScopeLock s(&cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) return; m_Map[ns].erase(it); } void CConfigDB::SetConfigFile(EConfigNamespace ns, const VfsPath& path) { CHECK_NS(;); CScopeLock s(&cfgdb_mutex); m_ConfigFile[ns] = path; } bool CConfigDB::Reload(EConfigNamespace ns) { CHECK_NS(false); CScopeLock s(&cfgdb_mutex); shared_ptr buffer; size_t buflen; { // Handle missing files quietly if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0) { LOGMESSAGE("Cannot find config file \"%s\" - ignoring", m_ConfigFile[ns].string8()); return false; } LOGMESSAGE("Loading config file \"%s\"", m_ConfigFile[ns].string8()); Status ret = g_VFS->LoadFile(m_ConfigFile[ns], buffer, buflen); if (ret != INFO::OK) { LOGERROR("CConfigDB::Reload(): vfs_load for \"%s\" failed: return was %lld", m_ConfigFile[ns].string8(), (long long)ret); return false; } } TConfigMap newMap; char *filebuf = (char*)buffer.get(); char *filebufend = filebuf+buflen; bool quoted = false; CStr header; CStr name; CStr value; int line = 1; std::vector values; for (char* pos = filebuf; pos < filebufend; ++pos) { switch (*pos) { case '\n': case ';': break; // We finished parsing this line case ' ': case '\r': case '\t': continue; // ignore case '[': header.clear(); for (++pos; pos < filebufend && *pos != '\n' && *pos != ']'; ++pos) header.push_back(*pos); if (pos == filebufend || *pos == '\n') { LOGERROR("Config header with missing close tag encountered on line %d in '%s'", line, m_ConfigFile[ns].string8()); header.clear(); ++line; continue; } LOGMESSAGE("Found config header '%s'", header.c_str()); header.push_back('.'); while (++pos < filebufend && *pos != '\n' && *pos != ';') if (*pos != ' ' && *pos != '\r') { LOGERROR("Config settings on the same line as a header on line %d in '%s'", line, m_ConfigFile[ns].string8()); break; } while (pos < filebufend && *pos != '\n') ++pos; ++line; continue; case '=': // Parse parameters (comma separated, possibly quoted) for (++pos; pos < filebufend && *pos != '\n' && *pos != ';'; ++pos) { switch (*pos) { case '"': quoted = true; // parse until not quoted anymore for (++pos; pos < filebufend && *pos != '\n' && *pos != '"'; ++pos) { if (*pos == '\\' && ++pos == filebufend) { LOGERROR("Escape character at end of input (line %d in '%s')", line, m_ConfigFile[ns].string8()); break; } value.push_back(*pos); } if (pos < filebufend && *pos == '"') quoted = false; else --pos; // We should terminate the outer loop too break; case ' ': case '\r': case '\t': break; // ignore case ',': if (!value.empty()) values.push_back(value); value.clear(); break; default: value.push_back(*pos); break; } } if (quoted) // We ignore the invalid parameter LOGERROR("Unmatched quote while parsing config file '%s' on line %d", m_ConfigFile[ns].string8(), line); else if (!value.empty()) values.push_back(value); value.clear(); quoted = false; break; // We are either at the end of the line, or we still have a comment to parse default: name.push_back(*pos); continue; } // Consume the rest of the line while (pos < filebufend && *pos != '\n') ++pos; // Store the setting if (!name.empty() && !values.empty()) { CStr key(header + name); newMap[key] = values; if (g_UnloggedEntries.find(key) != g_UnloggedEntries.end()) LOGMESSAGE("Loaded config string \"%s\"", key); else { std::string vals; for (size_t i = 0; i < newMap[key].size() - 1; ++i) vals += "\"" + EscapeString(newMap[key][i]) + "\", "; vals += "\"" + EscapeString(newMap[key][values.size()-1]) + "\""; LOGMESSAGE("Loaded config string \"%s\" = %s", key, vals); } } else if (!name.empty()) LOGERROR("Encountered config setting '%s' without value while parsing '%s' on line %d", name, m_ConfigFile[ns].string8(), line); name.clear(); values.clear(); ++line; } if (!name.empty()) LOGERROR("Config file does not have a new line after the last config setting '%s'", name); m_Map[ns].swap(newMap); return true; } bool CConfigDB::WriteFile(EConfigNamespace ns) const { CHECK_NS(false); CScopeLock s(&cfgdb_mutex); return WriteFile(ns, m_ConfigFile[ns]); } bool CConfigDB::WriteFile(EConfigNamespace ns, const VfsPath& path) const { CHECK_NS(false); CScopeLock s(&cfgdb_mutex); shared_ptr buf; AllocateAligned(buf, 1*MiB, maxSectorSize); char* pos = (char*)buf.get(); for (const std::pair& p : m_Map[ns]) { size_t i; pos += sprintf(pos, "%s = ", p.first.c_str()); for (i = 0; i < p.second.size() - 1; ++i) pos += sprintf(pos, "\"%s\", ", EscapeString(p.second[i]).c_str()); pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str()); } const size_t len = pos - (char*)buf.get(); Status ret = g_VFS->CreateFile(path, buf, len); if (ret < 0) { LOGERROR("CConfigDB::WriteFile(): CreateFile \"%s\" failed (error: %d)", path.string8(), (int)ret); return false; } return true; } bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value) { CHECK_NS(false); CScopeLock s(&cfgdb_mutex); return WriteValueToFile(ns, name, value, m_ConfigFile[ns]); } bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value, const VfsPath& path) { CHECK_NS(false); CScopeLock s(&cfgdb_mutex); TConfigMap newMap; m_Map[ns].swap(newMap); Reload(ns); SetValueString(ns, name, value); bool ret = WriteFile(ns, path); m_Map[ns].swap(newMap); return ret; } #undef CHECK_NS Index: ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp (revision 21866) +++ ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp (revision 21867) @@ -1,147 +1,173 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_ConfigDB.h" #include "ps/ConfigDB.h" #include "ps/CLogger.h" -#include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" +#include +#include + +// These entries will not be readable nor writable for JS, so that malicious mods can't leak personal or sensitive data +static const std::unordered_set g_ProtectedConfigNames = { + "userreport.id" // authentication token for GDPR personal data requests +}; + +bool JSI_ConfigDB::IsProtectedConfigName(const std::string& name) +{ + if (g_ProtectedConfigNames.find(name) != g_ProtectedConfigNames.end()) + { + LOGERROR("Access denied (%s)", name.c_str()); + return true; + } + return false; +} + bool JSI_ConfigDB::GetConfigNamespace(const std::wstring& cfgNsString, EConfigNamespace& cfgNs) { if (cfgNsString == L"default") cfgNs = CFG_DEFAULT; else if (cfgNsString == L"system") cfgNs = CFG_SYSTEM; else if (cfgNsString == L"user") cfgNs = CFG_USER; else if (cfgNsString == L"mod") cfgNs = CFG_MOD; else { LOGERROR("Invalid namespace name passed to the ConfigDB!"); cfgNs = CFG_DEFAULT; return false; } return true; } bool JSI_ConfigDB::HasChanges(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; return g_ConfigDB.HasChanges(cfgNs); } bool JSI_ConfigDB::SetChanges(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, bool value) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetChanges(cfgNs, value); return true; } std::string JSI_ConfigDB::GetValue(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, const std::string& name) { + if (IsProtectedConfigName(name)) + return ""; + EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return std::string(); std::string value; g_ConfigDB.GetValue(cfgNs, name, value); return value; } bool JSI_ConfigDB::CreateValue(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, const std::string& name, const std::string& value) { + if (IsProtectedConfigName(name)) + return false; + EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetValueString(cfgNs, name, value); return true; } bool JSI_ConfigDB::RemoveValue(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, const std::string& name) { + if (IsProtectedConfigName(name)) + return false; + EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.RemoveValue(cfgNs, name); return true; } bool JSI_ConfigDB::WriteFile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, const Path& path) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; - bool ret = g_ConfigDB.WriteFile(cfgNs, path); - return ret; + return g_ConfigDB.WriteFile(cfgNs, path); } bool JSI_ConfigDB::WriteValueToFile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path) { + if (IsProtectedConfigName(name)) + return false; + EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; - bool ret = g_ConfigDB.WriteValueToFile(cfgNs, name, value, path); - return ret; + return g_ConfigDB.WriteValueToFile(cfgNs, name, value, path); } bool JSI_ConfigDB::Reload(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; - bool ret = g_ConfigDB.Reload(cfgNs); - return ret; + return g_ConfigDB.Reload(cfgNs); } bool JSI_ConfigDB::SetFile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& cfgNsString, const Path& path) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetConfigFile(cfgNs, path); return true; } void JSI_ConfigDB::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ConfigDB_HasChanges"); scriptInterface.RegisterFunction("ConfigDB_SetChanges"); scriptInterface.RegisterFunction("ConfigDB_GetValue"); scriptInterface.RegisterFunction("ConfigDB_CreateValue"); scriptInterface.RegisterFunction("ConfigDB_RemoveValue"); scriptInterface.RegisterFunction("ConfigDB_WriteFile"); scriptInterface.RegisterFunction("ConfigDB_WriteValueToFile"); scriptInterface.RegisterFunction("ConfigDB_SetFile"); scriptInterface.RegisterFunction("ConfigDB_Reload"); } Index: ps/trunk/source/ps/scripting/JSInterface_ConfigDB.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_ConfigDB.h (revision 21866) +++ ps/trunk/source/ps/scripting/JSInterface_ConfigDB.h (revision 21867) @@ -1,39 +1,42 @@ /* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_JSI_CONFIGDB #define INCLUDED_JSI_CONFIGDB #include "ps/ConfigDB.h" #include "scriptinterface/ScriptInterface.h" +#include + namespace JSI_ConfigDB { + bool IsProtectedConfigName(const std::string& name); bool GetConfigNamespace(const std::wstring& cfgNsString, EConfigNamespace& cfgNs); bool HasChanges(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString); bool SetChanges(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, bool value); std::string GetValue(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, const std::string& name); bool CreateValue(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value); bool RemoveValue(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, const std::string& name); bool WriteFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, const Path& path); bool WriteValueToFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path); bool Reload(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString); bool SetFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& cfgNsString, const Path& path); void RegisterScriptFunctions(const ScriptInterface& scriptInterface); } #endif // INCLUDED_JSI_CONFIGDB