1
0

Merge pull request #1255 from mc-server/NameToUUID

Name to UUID
This commit is contained in:
Mattes D 2014-08-01 22:35:12 +02:00
commit 941a182d8a
22 changed files with 860 additions and 158 deletions

3
.gitmodules vendored
View File

@ -10,3 +10,6 @@
[submodule "lib/polarssl"]
path = lib/polarssl
url = https://github.com/mc-server/polarssl
[submodule "lib/SQLiteCpp"]
path = lib/SQLiteCpp
url = https://github.com/mc-server/SQLiteCpp.git

View File

@ -53,6 +53,14 @@ endif()
project (MCServer)
# Set options for SQLiteCpp, disable all their tests and lints:
set(SQLITECPP_RUN_CPPLINT OFF CACHE BOOL "Run cpplint.py tool for Google C++ StyleGuide." FORCE)
set(SQLITECPP_RUN_CPPCHECK OFF CACHE BOOL "Run cppcheck C++ static analysis tool." FORCE)
set(SQLITECPP_RUN_DOXYGEN OFF CACHE BOOL "Run Doxygen C++ documentation tool." FORCE)
set(SQLITECPP_BUILD_EXAMPLES OFF CACHE BOOL "Build examples." FORCE)
set(SQLITECPP_BUILD_TESTS OFF CACHE BOOL "Build and run tests." FORCE)
set(SQLITECPP_INTERNAL_SQLITE OFF CACHE BOOL "Add the internal SQLite3 source to the project." FORCE)
# Include all the libraries:
add_subdirectory(lib/inifile/)
add_subdirectory(lib/jsoncpp/)
@ -60,9 +68,16 @@ add_subdirectory(lib/zlib/)
add_subdirectory(lib/lua/)
add_subdirectory(lib/tolua++/)
add_subdirectory(lib/sqlite/)
add_subdirectory(lib/SQLiteCpp/)
add_subdirectory(lib/expat/)
add_subdirectory(lib/luaexpat/)
# Add proper include directories so that SQLiteCpp can find SQLite3:
get_property(SQLITECPP_INCLUDES DIRECTORY "lib/SQLiteCpp/" PROPERTY INCLUDE_DIRECTORIES)
set(SQLITECPP_INCLUDES "${SQLITECPP_INCLUDES}" "${CMAKE_CURRENT_SOURCE_DIR}/lib/sqlite/")
set_property(DIRECTORY lib/SQLiteCpp/ PROPERTY INCLUDE_DIRECTORIES "${SQLITECPP_INCLUDES}")
set_property(TARGET SQLiteCpp PROPERTY INCLUDE_DIRECTORIES "${SQLITECPP_INCLUDES}")
if (WIN32)
add_subdirectory(lib/luaproxy/)
endif()

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2012-2014 Sebastien Rombauts (sebastien.rombauts@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -11,4 +11,5 @@ MCServer*debug.cmd
Lua-LICENSE.txt
LuaExpat-license.html
LuaSQLite3-LICENSE.txt
SQLiteCpp-LICENSE.txt
MersenneTwister-LICENSE.txt

View File

@ -523,12 +523,12 @@ end
Functions =
{
GenerateOfflineUUID = { Params = "Username", Return = "string", Notes = "(STATIC) Generates an UUID based on the player name provided. This is used for the offline (non-auth) mode, when there's no UUID source. Each username generates a unique and constant UUID, so that when the player reconnects with the same name, their UUID is the same. Returns a 36-char UUID (with dashes)." },
GenerateOfflineUUID = { Params = "Username", Return = "string", Notes = "(STATIC) Generates an UUID based on the player name provided. This is used for the offline (non-auth) mode, when there's no UUID source. Each username generates a unique and constant UUID, so that when the player reconnects with the same name, their UUID is the same. Returns a 32-char UUID (no dashes)." },
GetLocale = { Params = "", Return = "Locale", Notes = "Returns the locale string that the client sends as part of the protocol handshake. Can be used to provide localized strings." },
GetPing = { Params = "", Return = "number", Notes = "Returns the ping time, in ms" },
GetPlayer = { Params = "", Return = "{{cPlayer|cPlayer}}", Notes = "Returns the player object connected to this client. Note that this may be nil, for example if the player object is not yet spawned." },
GetUniqueID = { Params = "", Return = "number", Notes = "Returns the UniqueID of the client used to identify the client in the server" },
GetUUID = { Params = "", Return = "string", Notes = "Returns the authentication-based UUID of the client. This UUID should be used to identify the player when persisting any player-related data." },
GetUUID = { Params = "", Return = "string", Notes = "Returns the authentication-based UUID of the client. This UUID should be used to identify the player when persisting any player-related data. Returns a 32-char UUID (no dashes)" },
GetUsername = { Params = "", Return = "string", Notes = "Returns the username that the client has provided" },
GetViewDistance = { Params = "", Return = "number", Notes = "Returns the viewdistance (number of chunks loaded for the player in each direction)" },
HasPluginChannel = { Params = "ChannelName", Return = "bool", Notes = "Returns true if the client has registered to receive messages on the specified plugin channel." },
@ -1606,6 +1606,37 @@ a_Player:OpenWindow(Window);
}, -- cMapManager
cMojangAPI =
{
Desc = [[
Provides interface to various API functions that Mojang provides through their servers. Note that
some of these calls will wait for a response from the network, and so shouldn't be used while the
server is fully running (or at least when there are players connected) to avoid percepted lag.</p>
<p>
Some functions are static and do not require an instance to be called. For others, you need to get
the singleton instance of this class using {{cRoot}}'s GetMojangAPI() function.</p>
<p>
Mojang uses two formats for UUIDs, short and dashed. MCServer works with short UUIDs internally, but
will convert to dashed UUIDs where needed - in the protocol login for example. The MakeUUIDShort()
and MakeUUIDDashed() functions are provided for plugins to use for conversion between the two
formats.</p>
<p>
This class will cache values returned by the API service. The cache will hold the values for 7 days
by default, after that, they will no longer be available. This is in order to not let the server get
banned from using the API service, since they are rate-limited to 600 queries per 10 minutes. The
cache contents also gets updated whenever a player successfully joins, since that makes the server
contact the API service, too, and retrieve the relevant data.</p>
]],
Functions =
{
AddPlayerNameToUUIDMapping = { Params = "PlayerName, UUID", Return = "", Notes = "Adds the specified PlayerName-to-UUID mapping into the cache, with current timestamp." },
GetUUIDsFromPlayerNames = { Params = "PlayerNames, [UseOnlyCached]", Return = "table", Notes = "Returns a table that contains the map, 'PlayerName' -> 'UUID', for all valid playernames in the input array-table. PlayerNames not recognized will not be set in the returned map. If UseOnlyCached is false (the default), queries the Mojang servers for the results that are not in the cache. <br /><b>WARNING</b>: Do NOT use this function with UseOnlyCached set to false while the server is running. Only use it when the server is starting up (inside the Initialize() method), otherwise you will lag the server severely." },
MakeUUIDDashed = { Params = "UUID", Return = "DashedUUID", Notes = "(STATIC) Converts the UUID to a dashed format (\"01234567-8901-2345-6789-012345678901\"). Accepts both dashed and short UUIDs. Logs a warning and returns an empty string if UUID format not recognized." },
MakeUUIDShort = { Params = "UUID", Return = "ShortUUID", Notes = "(STATIC) Converts the UUID to a short format (without dashes, \"01234567890123456789012345678901\"). Accepts both dashed and short UUIDs. Logs a warning and returns an empty string if UUID format not recognized." },
},
},
cMonster =
{
Desc = [[
@ -2001,6 +2032,7 @@ cPluginManager.AddHook(cPluginManager.HOOK_CHAT, OnChatMessage);
GetFurnaceFuelBurnTime = { Params = "{{cItem|Fuel}}", Return = "number", Notes = "(STATIC) Returns the number of ticks for how long the item would fuel a furnace. Returns zero if not a fuel." },
GetFurnaceRecipe = { Params = "{{cItem|InItem}}", Return = "{{cItem|OutItem}}, NumTicks, {{cItem|InItem}}", Notes = "(STATIC) Returns the furnace recipe for smelting the specified input. If a recipe is found, returns the smelted result, the number of ticks required for the smelting operation, and the input consumed (note that MCServer supports smelting M items into N items and different smelting rates). If no recipe is found, returns no value." },
GetGroupManager = { Params = "", Return = "{{cGroupManager|cGroupManager}}", Notes = "Returns the cGroupManager object." },
GetMojangAPI = { Params = "", Return = "{{cMojangAPI}}", Notes = "Returns the {{cMojangAPI}} object." },
GetPhysicalRAMUsage = { Params = "", Return = "number", Notes = "Returns the amount of physical RAM that the entire MCServer process is using, in KiB. Negative if the OS doesn't support this query." },
GetPluginManager = { Params = "", Return = "{{cPluginManager|cPluginManager}}", Notes = "Returns the cPluginManager object." },
GetPrimaryServerVersion = { Params = "", Return = "number", Notes = "Returns the servers primary server version." },

View File

@ -80,6 +80,7 @@ function Initialize(Plugin)
TestBlockAreasString()
TestStringBase64()
TestUUIDFromName()
--[[
-- Test cCompositeChat usage in console-logging:
@ -275,6 +276,75 @@ end
function TestUUIDFromName()
LOG("Testing UUID-from-Name resolution...")
-- Test by querying a few existing names, along with a non-existent one:
local PlayerNames =
{
"xoft",
"aloe_vera",
"nonexistent_player",
}
-- WARNING: Blocking operation! DO NOT USE IN TICK THREAD!
local UUIDs = cMojangAPI:GetUUIDsFromPlayerNames(PlayerNames)
-- Log the results:
for _, name in ipairs(PlayerNames) do
local UUID = UUIDs[name]
if (UUID == nil) then
LOG(" UUID(" .. name .. ") not found.")
else
LOG(" UUID(" .. name .. ") = \"" .. UUID .. "\"")
end
end
-- Test once more with the same players, valid-only. This should go directly from cache, so fast.
LOG("Testing again with the same valid players...")
local ValidPlayerNames =
{
"xoft",
"aloe_vera",
}
UUIDs = cMojangAPI:GetUUIDsFromPlayerNames(ValidPlayerNames);
-- Log the results:
for _, name in ipairs(ValidPlayerNames) do
local UUID = UUIDs[name]
if (UUID == nil) then
LOG(" UUID(" .. name .. ") not found.")
else
LOG(" UUID(" .. name .. ") = \"" .. UUID .. "\"")
end
end
-- Test yet again, cache-only:
LOG("Testing once more, cache only...")
local PlayerNames3 =
{
"xoft",
"aloe_vera",
"notch", -- Valid player name, but not cached (most likely :)
}
UUIDs = cMojangAPI:GetUUIDsFromPlayerNames(PlayerNames3, true)
-- Log the results:
for _, name in ipairs(PlayerNames3) do
local UUID = UUIDs[name]
if (UUID == nil) then
LOG(" UUID(" .. name .. ") not found.")
else
LOG(" UUID(" .. name .. ") = \"" .. UUID .. "\"")
end
end
LOG("UUID-from-Name resolution tests finished.")
end
function TestSQLiteBindings()
LOG("Testing SQLite bindings...");

1
lib/SQLiteCpp Submodule

@ -0,0 +1 @@
Subproject commit 27b9d111818af3b05bcf4153bb6e380fe1dd6816

View File

@ -78,6 +78,7 @@ $cfile "../Map.h"
$cfile "../MapManager.h"
$cfile "../Scoreboard.h"
$cfile "../Statistics.h"
$cfile "../Protocol/MojangAPI.h"

View File

@ -27,6 +27,7 @@
#include "../BlockEntities/MobHeadEntity.h"
#include "../BlockEntities/FlowerPotEntity.h"
#include "../LineBlockTracer.h"
#include "../Protocol/Authenticator.h"
#include "../WorldStorage/SchematicFileSerializer.h"
#include "../CompositeChat.h"
@ -2157,6 +2158,72 @@ static int tolua_cClientHandle_SendPluginMessage(lua_State * L)
static int tolua_cMojangAPI_GetUUIDsFromPlayerNames(lua_State * L)
{
cLuaState S(L);
if (
!S.CheckParamUserTable(1, "cMojangAPI") ||
!S.CheckParamTable(2) ||
!S.CheckParamEnd(4)
)
{
return 0;
}
// Convert the input table into AStringVector:
AStringVector PlayerNames;
int NumNames = luaL_getn(L, 2);
PlayerNames.reserve(NumNames);
for (int i = 1; i <= NumNames; i++)
{
lua_rawgeti(L, 2, i);
AString Name;
S.GetStackValue(-1, Name);
if (!Name.empty())
{
PlayerNames.push_back(Name);
}
lua_pop(L, 1);
}
// If the UseOnlyCached param was given, read it; default to false
bool ShouldUseCacheOnly = false;
if (lua_gettop(L) == 3)
{
ShouldUseCacheOnly = (lua_toboolean(L, 3) != 0);
lua_pop(L, 1);
}
// Push the output table onto the stack:
lua_newtable(L);
// Get the UUIDs:
AStringVector UUIDs = cRoot::Get()->GetMojangAPI().GetUUIDsFromPlayerNames(PlayerNames, ShouldUseCacheOnly);
if (UUIDs.size() != PlayerNames.size())
{
// A hard error has occured while processing the request, no UUIDs were returned. Return an empty table:
return 1;
}
// Convert to output table, PlayerName -> UUID:
size_t len = UUIDs.size();
for (size_t i = 0; i < len; i++)
{
if (UUIDs[i].empty())
{
// No UUID was provided for PlayerName[i], skip it in the resulting table
continue;
}
lua_pushlstring(L, UUIDs[i].c_str(), UUIDs[i].length());
lua_setfield(L, 3, PlayerNames[i].c_str());
}
return 1;
}
static int Lua_ItemGrid_GetSlotCoords(lua_State * L)
{
tolua_Error tolua_err;
@ -3090,6 +3157,10 @@ void ManualBindings::Bind(lua_State * tolua_S)
tolua_function(tolua_S, "SendPluginMessage", tolua_cClientHandle_SendPluginMessage);
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cMojangAPI");
tolua_function(tolua_S, "GetUUIDsFromPlayerNames", tolua_cMojangAPI_GetUUIDsFromPlayerNames);
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cItemGrid");
tolua_function(tolua_S, "GetSlotCoords", Lua_ItemGrid_GetSlotCoords);
tolua_endmodule(tolua_S);

View File

@ -138,6 +138,8 @@ SET (HDRS
XMLParser.h)
include_directories(".")
include_directories ("${CMAKE_CURRENT_SOURCE_DIR}/../lib/sqlite")
include_directories ("${CMAKE_CURRENT_SOURCE_DIR}/../lib/SQLiteCpp/include")
if (NOT MSVC)
# Bindings need to reference other folders, so they are done here instead
@ -311,4 +313,4 @@ endif ()
if (WIN32)
target_link_libraries(${EXECUTABLE} expat tolualib ws2_32.lib Psapi.lib)
endif()
target_link_libraries(${EXECUTABLE} luaexpat iniFile jsoncpp polarssl zlib sqlite lua)
target_link_libraries(${EXECUTABLE} luaexpat iniFile jsoncpp polarssl zlib sqlite lua SQLiteCpp)

View File

@ -234,13 +234,14 @@ AString cClientHandle::GenerateOfflineUUID(const AString & a_Username)
// This guarantees that they will never collide with an online UUID and can be distinguished.
// Proper format for a version 3 UUID is:
// xxxxxxxx-xxxx-3xxx-yxxx-xxxxxxxxxxxx where x is any hexadecimal digit and y is one of 8, 9, A, or B
// Note that we generate a short UUID (without the dashes)
// Generate an md5 checksum, and use it as base for the ID:
unsigned char MD5[16];
md5((const unsigned char *)a_Username.c_str(), a_Username.length(), MD5);
MD5[6] &= 0x0f; // Need to trim to 4 bits only...
MD5[8] &= 0x0f; // ... otherwise %01x overflows into two chars
return Printf("%02x%02x%02x%02x-%02x%02x-3%01x%02x-8%01x%02x-%02x%02x%02x%02x%02x%02x",
return Printf("%02x%02x%02x%02x%02x%02x3%01x%02x8%01x%02x%02x%02x%02x%02x%02x%02x",
MD5[0], MD5[1], MD5[2], MD5[3],
MD5[4], MD5[5], MD5[6], MD5[7],
MD5[8], MD5[9], MD5[10], MD5[11],

View File

@ -66,7 +66,9 @@ public:
cPlayer * GetPlayer(void) { return m_Player; } // tolua_export
/** Returns the player's UUID, as used by the protocol, in the short form (no dashes) */
const AString & GetUUID(void) const { return m_UUID; } // tolua_export
void SetUUID(const AString & a_UUID) { m_UUID = a_UUID; }
const Json::Value & GetProperties(void) const { return m_Properties; }
@ -80,7 +82,7 @@ public:
/** Generates an UUID based on the player name provided.
This is used for the offline (non-auth) mode, when there's no UUID source.
Each username generates a unique and constant UUID, so that when the player reconnects with the same name, their UUID is the same.
Returns a 36-char UUID (with dashes). */
Returns a 32-char UUID (no dashes). */
static AString GenerateOfflineUUID(const AString & a_Username); // tolua_export
/** Returns true if the UUID is generated by online auth, false if it is an offline-generated UUID.
@ -360,7 +362,11 @@ private:
int m_NumBlockChangeInteractionsThisTick;
static int s_ClientCount;
/** ID used for identification during authenticating. Assigned sequentially for each new instance. */
int m_UniqueID;
/** Contains the UUID used by Mojang to identify the player's account. Short UUID stored here (without dashes) */
AString m_UUID;
/** Set to true when the chunk where the player is is sent to the client. Used for spawning the player */

View File

@ -1705,8 +1705,10 @@ bool cPlayer::LoadFromDisk(cWorldPtr & a_World)
// Load from the offline UUID file, if allowed:
AString OfflineUUID = cClientHandle::GenerateOfflineUUID(GetName());
const char * OfflineUsage = " (unused)";
if (cRoot::Get()->GetServer()->ShouldLoadOfflinePlayerData())
{
OfflineUsage = "";
if (LoadFromFile(GetUUIDFileName(OfflineUUID), a_World))
{
return true;
@ -1729,8 +1731,8 @@ bool cPlayer::LoadFromDisk(cWorldPtr & a_World)
}
// None of the files loaded successfully
LOG("Player data file not found for %s (%s, offline %s), will be reset to defaults.",
GetName().c_str(), m_UUID.c_str(), OfflineUUID.c_str()
LOG("Player data file not found for %s (%s, offline %s%s), will be reset to defaults.",
GetName().c_str(), m_UUID.c_str(), OfflineUUID.c_str(), OfflineUsage
);
if (a_World == NULL)
@ -2237,12 +2239,13 @@ void cPlayer::Detach()
AString cPlayer::GetUUIDFileName(const AString & a_UUID)
{
ASSERT(a_UUID.size() == 36);
AString UUID = cMojangAPI::MakeUUIDDashed(a_UUID);
ASSERT(UUID.length() == 36);
AString res("players/");
res.append(a_UUID, 0, 2);
res.append(UUID, 0, 2);
res.push_back('/');
res.append(a_UUID, 2, AString::npos);
res.append(UUID, 2, AString::npos);
res.append(".json");
return res;
}

View File

@ -551,7 +551,7 @@ protected:
*/
bool m_bIsTeleporting;
/** The UUID of the player, as read from the ClientHandle.
/** The short UUID (no dashes) of the player, as read from the ClientHandle.
If no ClientHandle is given, the UUID is initialized to empty. */
AString m_UUID;

View File

@ -2,6 +2,7 @@
#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
#include "Authenticator.h"
#include "MojangAPI.h"
#include "../Root.h"
#include "../Server.h"
#include "../ClientHandle.h"
@ -18,67 +19,6 @@
#define DEFAULT_AUTH_SERVER "sessionserver.mojang.com"
#define DEFAULT_AUTH_ADDRESS "/session/minecraft/hasJoined?username=%USERNAME%&serverId=%SERVERID%"
/** This is the data of the root certs for Starfield Technologies, the CA that signed sessionserver.mojang.com's cert:
Downloaded from http://certs.starfieldtech.com/repository/ */
static const AString StarfieldCACert()
{
return AString(
// G2 cert
"-----BEGIN CERTIFICATE-----\n"
"MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx\n"
"EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\n"
"HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs\n"
"ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw\n"
"MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\n"
"b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj\n"
"aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp\n"
"Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
"ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg\n"
"nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1\n"
"HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N\n"
"Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN\n"
"dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0\n"
"HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\n"
"BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G\n"
"CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU\n"
"sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3\n"
"4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg\n"
"8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K\n"
"pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1\n"
"mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0\n"
"-----END CERTIFICATE-----\n\n"
// Original (G1) cert:
"-----BEGIN CERTIFICATE-----\n"
"MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\n"
"MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\n"
"U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\n"
"NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\n"
"ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\n"
"ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\n"
"DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n"
"8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n"
"+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\n"
"X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\n"
"K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n"
"1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\n"
"A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\n"
"zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\n"
"YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\n"
"bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\n"
"DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\n"
"L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\n"
"eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\n"
"xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\n"
"VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\n"
"WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n"
"-----END CERTIFICATE-----\n"
);
}
cAuthenticator::cAuthenticator(void) :
super("cAuthenticator"),
m_Server(DEFAULT_AUTH_SERVER),
@ -102,8 +42,8 @@ cAuthenticator::~cAuthenticator()
void cAuthenticator::ReadINI(cIniFile & IniFile)
{
m_Server = IniFile.GetValueSet("Authentication", "Server", DEFAULT_AUTH_SERVER);
m_Address = IniFile.GetValueSet("Authentication", "Address", DEFAULT_AUTH_ADDRESS);
m_Server = IniFile.GetValueSet ("Authentication", "Server", DEFAULT_AUTH_SERVER);
m_Address = IniFile.GetValueSet ("Authentication", "Address", DEFAULT_AUTH_ADDRESS);
m_ShouldAuthenticate = IniFile.GetValueSetB("Authentication", "Authenticate", true);
}
@ -193,62 +133,6 @@ void cAuthenticator::Execute(void)
bool cAuthenticator::SecureGetFromAddress(const AString & a_CACerts, const AString & a_ExpectedPeerName, const AString & a_Data, AString & a_Response)
{
// Connect the socket:
cBlockingSslClientSocket Socket;
Socket.SetTrustedRootCertsFromString(a_CACerts, a_ExpectedPeerName);
if (!Socket.Connect(a_ExpectedPeerName, 443))
{
LOGWARNING("cAuthenticator: Can't connect to %s: %s", a_ExpectedPeerName.c_str(), Socket.GetLastErrorText().c_str());
return false;
}
if (!Socket.Send(a_Data.c_str(), a_Data.size()))
{
LOGWARNING("cAuthenticator: Writing SSL data failed: %s", Socket.GetLastErrorText().c_str());
return false;
}
// Read the HTTP response:
int ret;
unsigned char buf[1024];
for (;;)
{
ret = Socket.Receive(buf, sizeof(buf));
if ((ret == POLARSSL_ERR_NET_WANT_READ) || (ret == POLARSSL_ERR_NET_WANT_WRITE))
{
// This value should never be returned, it is handled internally by cBlockingSslClientSocket
LOGWARNING("cAuthenticator: SSL reading failed internally");
return false;
}
if (ret == POLARSSL_ERR_SSL_PEER_CLOSE_NOTIFY)
{
break;
}
if (ret < 0)
{
LOGWARNING("cAuthenticator: SSL reading failed: -0x%x", -ret);
return false;
}
if (ret == 0)
{
break;
}
a_Response.append((const char *)buf, (size_t)ret);
}
Socket.Disconnect();
return true;
}
bool cAuthenticator::AuthWithYggdrasil(AString & a_UserName, const AString & a_ServerId, AString & a_UUID, Json::Value & a_Properties)
{
LOGD("Trying to authenticate user %s", a_UserName.c_str());
@ -266,7 +150,7 @@ bool cAuthenticator::AuthWithYggdrasil(AString & a_UserName, const AString & a_S
Request += "\r\n";
AString Response;
if (!SecureGetFromAddress(StarfieldCACert(), m_Server, Request, Response))
if (!cMojangAPI::SecureRequest(m_Server, Request, Response))
{
return false;
}
@ -304,17 +188,11 @@ bool cAuthenticator::AuthWithYggdrasil(AString & a_UserName, const AString & a_S
return false;
}
a_UserName = root.get("name", "Unknown").asString();
a_UUID = root.get("id", "").asString();
a_UUID = cMojangAPI::MakeUUIDShort(root.get("id", "").asString());
a_Properties = root["properties"];
// If the UUID doesn't contain the hashes, insert them at the proper places:
if (a_UUID.size() == 32)
{
a_UUID.insert(8, "-");
a_UUID.insert(13, "-");
a_UUID.insert(18, "-");
a_UUID.insert(23, "-");
}
// Store the player's UUID in the NameToUUID map in MojangAPI:
cRoot::Get()->GetMojangAPI().AddPlayerNameToUUIDMapping(a_UserName, a_UUID);
return true;
}

View File

@ -11,8 +11,6 @@
#pragma once
#ifndef CAUTHENTICATOR_H_INCLUDED
#define CAUTHENTICATOR_H_INCLUDED
#include "../OSSupport/IsThread.h"
@ -52,7 +50,7 @@ public:
/** Stops the authenticator thread. The thread may be started and stopped repeatedly */
void Stop(void);
private:
class cUser
@ -76,29 +74,26 @@ private:
cUserList m_Queue;
cEvent m_QueueNonempty;
/** The server that is to be contacted for auth / UUID conversions */
AString m_Server;
/** The URL to use for auth, without server part.
%USERNAME% will be replaced with actual user name.
%SERVERID% will be replaced with server's ID.
For example "/session/minecraft/hasJoined?username=%USERNAME%&serverId=%SERVERID%". */
AString m_Address;
AString m_PropertiesAddress;
bool m_ShouldAuthenticate;
/** cIsThread override: */
virtual void Execute(void) override;
/** Connects to a hostname using SSL, sends given data, and sets the response, returning whether all was successful or not */
bool SecureGetFromAddress(const AString & a_CACerts, const AString & a_ExpectedPeerName, const AString & a_Request, AString & a_Response);
/** Returns true if the user authenticated okay, false on error
Sets the username, UUID, and properties (i.e. skin) fields
*/
Returns the case-corrected username, UUID, and properties (eg. skin). */
bool AuthWithYggdrasil(AString & a_UserName, const AString & a_ServerId, AString & a_UUID, Json::Value & a_Properties);
};
#endif // CAUTHENTICATOR_H_INCLUDED

View File

@ -7,6 +7,7 @@ include_directories ("${PROJECT_SOURCE_DIR}/../")
SET (SRCS
Authenticator.cpp
ChunkDataSerializer.cpp
MojangAPI.cpp
Protocol125.cpp
Protocol132.cpp
Protocol14x.cpp
@ -18,6 +19,7 @@ SET (SRCS
SET (HDRS
Authenticator.h
ChunkDataSerializer.h
MojangAPI.h
Protocol.h
Protocol125.h
Protocol132.h

480
src/Protocol/MojangAPI.cpp Normal file
View File

@ -0,0 +1,480 @@
// MojangAPI.cpp
// Implements the cMojangAPI class representing the various API points provided by Mojang's webservices, and a cache for their results
#include "Globals.h"
#include "MojangAPI.h"
#include "SQLiteCpp/Database.h"
#include "SQLiteCpp/Statement.h"
#include "inifile/iniFile.h"
#include "json/json.h"
#include "PolarSSL++/BlockingSslClientSocket.h"
/** The maximum age for items to be kept in the cache. Any item older than this will be removed. */
const Int64 MAX_AGE = 7 * 24 * 60 * 60; // 7 days ago
/** The maximum number of names to send in a single query */
const int MAX_PER_QUERY = 100;
#define DEFAULT_NAME_TO_UUID_SERVER "api.mojang.com"
#define DEFAULT_NAME_TO_UUID_ADDRESS "/profiles/minecraft"
/** This is the data of the root certs for Starfield Technologies, the CA that signed sessionserver.mojang.com's cert:
Downloaded from http://certs.starfieldtech.com/repository/ */
static const AString & StarfieldCACert(void)
{
static const AString Cert(
// G2 cert
"-----BEGIN CERTIFICATE-----\n"
"MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx\n"
"EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\n"
"HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs\n"
"ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw\n"
"MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\n"
"b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj\n"
"aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp\n"
"Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
"ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg\n"
"nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1\n"
"HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N\n"
"Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN\n"
"dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0\n"
"HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\n"
"BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G\n"
"CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU\n"
"sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3\n"
"4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg\n"
"8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K\n"
"pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1\n"
"mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0\n"
"-----END CERTIFICATE-----\n\n"
// Original (G1) cert:
"-----BEGIN CERTIFICATE-----\n"
"MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\n"
"MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\n"
"U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\n"
"NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\n"
"ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\n"
"ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\n"
"DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n"
"8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n"
"+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\n"
"X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\n"
"K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n"
"1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\n"
"A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\n"
"zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\n"
"YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\n"
"bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\n"
"DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\n"
"L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\n"
"eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\n"
"xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\n"
"VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\n"
"WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n"
"-----END CERTIFICATE-----\n"
);
return Cert;
}
////////////////////////////////////////////////////////////////////////////////
// cMojangAPI:
cMojangAPI::cMojangAPI(void) :
m_NameToUUIDServer(DEFAULT_NAME_TO_UUID_SERVER),
m_NameToUUIDAddress(DEFAULT_NAME_TO_UUID_ADDRESS)
{
}
cMojangAPI::~cMojangAPI()
{
SaveCachesToDisk();
}
void cMojangAPI::Start(cIniFile & a_SettingsIni)
{
m_NameToUUIDServer = a_SettingsIni.GetValueSet("MojangAPI", "NameToUUIDServer", DEFAULT_NAME_TO_UUID_SERVER);
m_NameToUUIDAddress = a_SettingsIni.GetValueSet("MojangAPI", "NameToUUIDAddress", DEFAULT_NAME_TO_UUID_ADDRESS);
LoadCachesFromDisk();
}
AStringVector cMojangAPI::GetUUIDsFromPlayerNames(const AStringVector & a_PlayerNames, bool a_UseOnlyCached)
{
// Convert all playernames to lowercase:
AStringVector PlayerNames;
for (AStringVector::const_iterator itr = a_PlayerNames.begin(), end = a_PlayerNames.end(); itr != end; ++itr)
{
AString Lower(*itr);
PlayerNames.push_back(StrToLower(Lower));
} // for itr - a_PlayerNames[]
// Request the cache to populate any names not yet contained:
if (!a_UseOnlyCached)
{
CacheNamesToUUIDs(PlayerNames);
}
// Retrieve from cache:
size_t idx = 0;
AStringVector res;
res.resize(PlayerNames.size());
cCSLock Lock(m_CSNameToUUID);
for (AStringVector::const_iterator itr = PlayerNames.begin(), end = PlayerNames.end(); itr != end; ++itr, ++idx)
{
cNameToUUIDMap::const_iterator itrN = m_NameToUUID.find(*itr);
if (itrN != m_NameToUUID.end())
{
res[idx] = itrN->second.m_UUID;
}
} // for itr - PlayerNames[]
return res;
}
void cMojangAPI::AddPlayerNameToUUIDMapping(const AString & a_PlayerName, const AString & a_UUID)
{
AString lcName(a_PlayerName);
AString UUID = MakeUUIDShort(a_UUID);
Int64 Now = time(NULL);
cCSLock Lock(m_CSNameToUUID);
m_NameToUUID[StrToLower(lcName)] = sUUIDRecord(a_PlayerName, UUID, Now);
}
bool cMojangAPI::SecureRequest(const AString & a_ServerName, const AString & a_Request, AString & a_Response)
{
// Connect the socket:
cBlockingSslClientSocket Socket;
Socket.SetTrustedRootCertsFromString(StarfieldCACert(), a_ServerName);
if (!Socket.Connect(a_ServerName, 443))
{
LOGWARNING("%s: Can't connect to %s: %s", __FUNCTION__, a_ServerName.c_str(), Socket.GetLastErrorText().c_str());
return false;
}
if (!Socket.Send(a_Request.c_str(), a_Request.size()))
{
LOGWARNING("%s: Writing SSL data failed: %s", __FUNCTION__, Socket.GetLastErrorText().c_str());
return false;
}
// Read the HTTP response:
int ret;
unsigned char buf[1024];
for (;;)
{
ret = Socket.Receive(buf, sizeof(buf));
if ((ret == POLARSSL_ERR_NET_WANT_READ) || (ret == POLARSSL_ERR_NET_WANT_WRITE))
{
// This value should never be returned, it is handled internally by cBlockingSslClientSocket
LOGWARNING("%s: SSL reading failed internally", __FUNCTION__);
return false;
}
if (ret == POLARSSL_ERR_SSL_PEER_CLOSE_NOTIFY)
{
break;
}
if (ret < 0)
{
LOGWARNING("%s: SSL reading failed: -0x%x", __FUNCTION__, -ret);
return false;
}
if (ret == 0)
{
break;
}
a_Response.append((const char *)buf, (size_t)ret);
}
Socket.Disconnect();
return true;
}
AString cMojangAPI::MakeUUIDShort(const AString & a_UUID)
{
// Note: we only check the string's length, not the actual content
switch (a_UUID.size())
{
case 32:
{
// Already is a short UUID
return a_UUID;
}
case 36:
{
// Remove the dashes from the string:
AString res;
res.reserve(32);
res.append(a_UUID, 0, 8);
res.append(a_UUID, 9, 4);
res.append(a_UUID, 14, 4);
res.append(a_UUID, 19, 4);
res.append(a_UUID, 24, 12);
return res;
}
}
LOGWARNING("%s: Not an UUID: \"%s\".", __FUNCTION__, a_UUID.c_str());
return "";
}
AString cMojangAPI::MakeUUIDDashed(const AString & a_UUID)
{
// Note: we only check the string's length, not the actual content
switch (a_UUID.size())
{
case 36:
{
// Already is a dashed UUID
return a_UUID;
}
case 32:
{
// Insert dashes at the proper positions:
AString res;
res.reserve(36);
res.append(a_UUID, 0, 8);
res.push_back('-');
res.append(a_UUID, 8, 4);
res.push_back('-');
res.append(a_UUID, 12, 4);
res.push_back('-');
res.append(a_UUID, 16, 4);
res.push_back('-');
res.append(a_UUID, 20, 12);
return res;
}
}
LOGWARNING("%s: Not an UUID: \"%s\".", __FUNCTION__, a_UUID.c_str());
return "";
}
void cMojangAPI::LoadCachesFromDisk(void)
{
try
{
// Open up the SQLite DB:
SQLite::Database db("MojangAPI.sqlite", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
db.exec("CREATE TABLE IF NOT EXISTS PlayerNameToUUID (PlayerName, UUID, DateTime)");
// Clean up old entries:
{
SQLite::Statement stmt(db, "DELETE FROM PlayerNameToUUID WHERE DateTime < ?");
Int64 LimitDateTime = time(NULL) - MAX_AGE;
stmt.bind(1, LimitDateTime);
stmt.exec();
}
// Retrieve all remaining entries::
SQLite::Statement stmt(db, "SELECT PlayerName, UUID, DateTime FROM PlayerNameToUUID");
while (stmt.executeStep())
{
AString PlayerName = stmt.getColumn(0);
AString UUID = stmt.getColumn(1);
Int64 DateTime = stmt.getColumn(2);
AString lcPlayerName = PlayerName;
m_NameToUUID[StrToLower(lcPlayerName)] = sUUIDRecord(PlayerName, UUID, DateTime);
}
}
catch (const SQLite::Exception & ex)
{
LOGINFO("Loading MojangAPI cache failed: %s", ex.what());
}
}
void cMojangAPI::SaveCachesToDisk(void)
{
try
{
// Open up the SQLite DB:
SQLite::Database db("MojangAPI.sqlite", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
db.exec("CREATE TABLE IF NOT EXISTS PlayerNameToUUID (PlayerName, UUID, DateTime)");
// Remove all entries:
db.exec("DELETE FROM PlayerNameToUUID");
// Save all cache entries:
SQLite::Statement stmt(db, "INSERT INTO PlayerNameToUUID(PlayerName, UUID, DateTime) VALUES (?, ?, ?)");
Int64 LimitDateTime = time(NULL) - MAX_AGE;
cCSLock Lock(m_CSNameToUUID);
for (cNameToUUIDMap::const_iterator itr = m_NameToUUID.begin(), end = m_NameToUUID.end(); itr != end; ++itr)
{
if (itr->second.m_DateTime < LimitDateTime)
{
// This item is too old, do not save
continue;
}
stmt.bind(1, itr->second.m_PlayerName);
stmt.bind(2, itr->second.m_UUID);
stmt.bind(3, itr->second.m_DateTime);
stmt.exec();
stmt.reset();
}
}
catch (const SQLite::Exception & ex)
{
LOGINFO("Saving MojangAPI cache failed: %s", ex.what());
}
}
void cMojangAPI::CacheNamesToUUIDs(const AStringVector & a_PlayerNames)
{
// Create a list of names to query, by removing those that are already cached:
AStringVector NamesToQuery;
NamesToQuery.reserve(a_PlayerNames.size());
{
cCSLock Lock(m_CSNameToUUID);
for (AStringVector::const_iterator itr = a_PlayerNames.begin(), end = a_PlayerNames.end(); itr != end; ++itr)
{
if (m_NameToUUID.find(*itr) == m_NameToUUID.end())
{
NamesToQuery.push_back(*itr);
}
} // for itr - a_PlayerNames[]
} // Lock(m_CSNameToUUID)
while (!NamesToQuery.empty())
{
// Create the request body - a JSON containing up to MAX_PER_QUERY playernames:
Json::Value root;
int Count = 0;
AStringVector::iterator itr = NamesToQuery.begin(), end = NamesToQuery.end();
for (; (itr != end) && (Count < MAX_PER_QUERY); ++itr, ++Count)
{
Json::Value req(*itr);
root.append(req);
} // for itr - a_PlayerNames[]
NamesToQuery.erase(NamesToQuery.begin(), itr);
Json::FastWriter Writer;
AString RequestBody = Writer.write(root);
// Create the HTTP request:
AString Request;
Request += "POST " + m_NameToUUIDAddress + " HTTP/1.0\r\n"; // We need to use HTTP 1.0 because we don't handle Chunked transfer encoding
Request += "Host: " + m_NameToUUIDServer + "\r\n";
Request += "User-Agent: MCServer\r\n";
Request += "Connection: close\r\n";
Request += "Content-Type: application/json\r\n";
Request += Printf("Content-Length: %u\r\n", (unsigned)RequestBody.length());
Request += "\r\n";
Request += RequestBody;
// Get the response from the server:
AString Response;
if (!SecureRequest(m_NameToUUIDServer, Request, Response))
{
continue;
}
// Check the HTTP status line:
const AString Prefix("HTTP/1.1 200 OK");
AString HexDump;
if (Response.compare(0, Prefix.size(), Prefix))
{
LOGINFO("%s failed: bad HTTP status line received", __FUNCTION__);
LOGD("Response: \n%s", CreateHexDump(HexDump, Response.data(), Response.size(), 16).c_str());
continue;
}
// Erase the HTTP headers from the response:
size_t idxHeadersEnd = Response.find("\r\n\r\n");
if (idxHeadersEnd == AString::npos)
{
LOGINFO("%s failed: bad HTTP response header received", __FUNCTION__);
LOGD("Response: \n%s", CreateHexDump(HexDump, Response.data(), Response.size(), 16).c_str());
continue;
}
Response.erase(0, idxHeadersEnd + 4);
// Parse the returned string into Json:
Json::Reader reader;
if (!reader.parse(Response, root, false) || !root.isArray())
{
LOGWARNING("%s failed: Cannot parse received data (NameToUUID) to JSON!", __FUNCTION__);
LOGD("Response body:\n%s", CreateHexDump(HexDump, Response.data(), Response.size(), 16).c_str());
continue;
}
// Store the returned results into cache:
size_t JsonCount = root.size();
Int64 Now = time(NULL);
cCSLock Lock(m_CSNameToUUID);
for (size_t idx = 0; idx < JsonCount; ++idx)
{
Json::Value & Val = root[idx];
AString JsonName = Val.get("name", "").asString();
AString JsonUUID = MakeUUIDShort(Val.get("id", "").asString());
if (JsonUUID.empty())
{
continue;
}
AString lcName = JsonName;
m_NameToUUID[StrToLower(lcName)] = sUUIDRecord(JsonName, JsonUUID, Now);
} // for idx - root[]
} // while (!NamesToQuery.empty())
}

117
src/Protocol/MojangAPI.h Normal file
View File

@ -0,0 +1,117 @@
// MojangAPI.h
// Declares the cMojangAPI class representing the various API points provided by Mojang's webservices, and a cache for their results
#pragma once
#include <time.h>
// tolua_begin
class cMojangAPI
{
public:
// tolua_end
cMojangAPI(void);
~cMojangAPI();
/** Initializes the API; reads the settings from the specified ini file.
Loads cached results from disk. */
void Start(cIniFile & a_SettingsIni);
/** Connects to the specified server using SSL, sends the given request and receives the response.
Checks Mojang certificates using the hard-coded Starfield root CA certificate.
Returns true if all was successful, false on failure. */
static bool SecureRequest(const AString & a_ServerName, const AString & a_Request, AString & a_Response);
// tolua_begin
/** Converts the given UUID to its short form (32 bytes, no dashes).
Logs a warning and returns empty string if not a UUID.
Note: only checks the string's length, not the actual content. */
static AString MakeUUIDShort(const AString & a_UUID);
/** Converts the given UUID to its dashed form (36 bytes, 4 dashes).
Logs a warning and returns empty string if not a UUID.
Note: only checks the string's length, not the actual content. */
static AString MakeUUIDDashed(const AString & a_UUID);
// tolua_end
/** Converts the player names into UUIDs.
a_PlayerName[idx] will be converted to UUID and returned as idx-th value
The UUID will be empty on error.
If a_UseOnlyCached is true, only the cached values are returned.
If a_UseOnlyCached is false, the names not found in the cache are looked up online, which is a blocking
operation, do not use this in world-tick thread! */
AStringVector GetUUIDsFromPlayerNames(const AStringVector & a_PlayerName, bool a_UseOnlyCached = false);
// tolua_begin
/** Called by the Authenticator to add a PlayerName -> UUID mapping that it has received from
authenticating a user. This adds the cache item and "refreshes" it if existing, adjusting its datetime
stamp to now. */
void AddPlayerNameToUUIDMapping(const AString & a_PlayerName, const AString & a_UUID);
// tolua_end
protected:
struct sUUIDRecord
{
AString m_PlayerName; // Case-correct playername
AString m_UUID;
Int64 m_DateTime; // UNIXtime of the UUID lookup
sUUIDRecord(void) :
m_UUID(),
m_DateTime(time(NULL))
{
}
sUUIDRecord(const AString & a_PlayerName, const AString & a_UUID, Int64 a_DateTime) :
m_PlayerName(a_PlayerName),
m_UUID(a_UUID),
m_DateTime(a_DateTime)
{
}
};
typedef std::map<AString, sUUIDRecord> cNameToUUIDMap; // maps Lowercased PlayerName to sUUIDRecord
/** The server to connect to when converting player names to UUIDs. For example "api.mojang.com". */
AString m_NameToUUIDServer;
/** The URL to use for converting player names to UUIDs, without server part.
For example "/profiles/page/1". */
AString m_NameToUUIDAddress;
/** Cache for the Name-to-UUID lookups. The map key is expected lowercased. Protected by m_CSNameToUUID. */
cNameToUUIDMap m_NameToUUID;
/** Protects m_NameToUUID against simultaneous multi-threaded access. */
cCriticalSection m_CSNameToUUID;
/** Loads the caches from a disk storage. */
void LoadCachesFromDisk(void);
/** Saves the caches to a disk storage. */
void SaveCachesToDisk(void);
/** Makes sure all specified names are in the cache. Downloads any missing ones from Mojang API servers.
Names that are not valid are not added into the cache.
ASSUMEs that a_PlayerNames contains lowercased player names. */
void CacheNamesToUUIDs(const AStringVector & a_PlayerNames);
} ; // tolua_export

View File

@ -681,7 +681,7 @@ void cProtocol172::SendLoginSuccess(void)
{
cPacketizer Pkt(*this, 0x02); // Login success packet
Pkt.WriteString(m_Client->GetUUID());
Pkt.WriteString(cMojangAPI::MakeUUIDDashed(m_Client->GetUUID()));
Pkt.WriteString(m_Client->GetUsername());
}
@ -942,7 +942,7 @@ void cProtocol172::SendPlayerSpawn(const cPlayer & a_Player)
// Called to spawn another player for the client
cPacketizer Pkt(*this, 0x0c); // Spawn Player packet
Pkt.WriteVarInt(a_Player.GetUniqueID());
Pkt.WriteString(a_Player.GetClientHandle()->GetUUID());
Pkt.WriteString(cMojangAPI::MakeUUIDDashed(a_Player.GetClientHandle()->GetUUID()));
Pkt.WriteString(a_Player.GetName());
Pkt.WriteFPInt(a_Player.GetPosX());
Pkt.WriteFPInt(a_Player.GetPosY());
@ -3029,7 +3029,7 @@ void cProtocol176::SendPlayerSpawn(const cPlayer & a_Player)
// Called to spawn another player for the client
cPacketizer Pkt(*this, 0x0c); // Spawn Player packet
Pkt.WriteVarInt(a_Player.GetUniqueID());
Pkt.WriteString(a_Player.GetClientHandle()->GetUUID());
Pkt.WriteString(cMojangAPI::MakeUUIDDashed(a_Player.GetClientHandle()->GetUUID()));
Pkt.WriteString(a_Player.GetName());
const Json::Value & Properties = m_Client->GetProperties();

View File

@ -145,6 +145,7 @@ void cRoot::Start(void)
}
LOG("Starting server...");
m_MojangAPI.Start(IniFile); // Mojang API needs to be started before plugins, so that plugins may use it for DB upgrades on server init
if (!m_Server->InitServer(IniFile))
{
LOGERROR("Failure starting server, aborting...");

View File

@ -2,6 +2,7 @@
#pragma once
#include "Protocol/Authenticator.h"
#include "Protocol/MojangAPI.h"
#include "HTTPServer/HTTPServer.h"
#include "Defines.h"
@ -87,6 +88,7 @@ public:
cWebAdmin * GetWebAdmin (void) { return m_WebAdmin; } // tolua_export
cPluginManager * GetPluginManager (void) { return m_PluginManager; } // tolua_export
cAuthenticator & GetAuthenticator (void) { return m_Authenticator; }
cMojangAPI & GetMojangAPI (void) { return m_MojangAPI; } // tolua_export
/** Queues a console command for execution through the cServer class.
The command will be executed in the tick thread
@ -191,6 +193,7 @@ private:
cWebAdmin * m_WebAdmin;
cPluginManager * m_PluginManager;
cAuthenticator m_Authenticator;
cMojangAPI m_MojangAPI;
cHTTPServer m_HTTPServer;
cMCLogger * m_Log;