From 21f52676f3848d58ff1e4eb511c691d4a4ed824b Mon Sep 17 00:00:00 2001 From: madmaxoft Date: Sun, 3 Aug 2014 21:32:20 +0200 Subject: cMojangAPI: Added UUID-to-Name lookup. Also fixed the bindings, now all functions are static-like. --- MCServer/Plugins/APIDump/APIDesc.lua | 17 +- src/Bindings/ManualBindings.cpp | 98 ++++++++- src/Protocol/Authenticator.cpp | 4 +- src/Protocol/MojangAPI.cpp | 383 +++++++++++++++++++++++++++++++---- src/Protocol/MojangAPI.h | 97 +++++++-- src/Root.h | 2 +- 6 files changed, 530 insertions(+), 71 deletions(-) diff --git a/MCServer/Plugins/APIDump/APIDesc.lua b/MCServer/Plugins/APIDump/APIDesc.lua index 4f8567530..ad3b24ca5 100644 --- a/MCServer/Plugins/APIDump/APIDesc.lua +++ b/MCServer/Plugins/APIDump/APIDesc.lua @@ -1613,8 +1613,7 @@ a_Player:OpenWindow(Window); 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.

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

+ All the functions are static, call them using the cMojangAPI:Function() convention.

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() @@ -1629,11 +1628,12 @@ a_Player:OpenWindow(Window); ]], Functions = { - AddPlayerNameToUUIDMapping = { Params = "PlayerName, UUID", Return = "", Notes = "Adds the specified PlayerName-to-UUID mapping into the cache, with current timestamp." }, - GetUUIDFromPlayerName = { Params = "PlayerName, [UseOnlyCached]", Return = "UUID", Notes = "Returns the UUID that corresponds to the given playername, or an empty string on error. If UseOnlyCached is false (the default), queries the Mojang servers if the playername is not in the cache.
WARNING: 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." }, - 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.
WARNING: 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." }, + AddPlayerNameToUUIDMapping = { Params = "PlayerName, UUID", Return = "", Notes = "(STATIC) Adds the specified PlayerName-to-UUID mapping into the cache, with current timestamp. Accepts both short or dashed UUIDs. " }, + GetPlayerNameFromUUID = { Params = "UUID, [UseOnlyCached]", Return = "PlayerName", Notes = "(STATIC) Returns the playername that corresponds to the given UUID, or an empty string on error. If UseOnlyCached is false (the default), queries the Mojang servers if the UUID is not in the cache. The UUID can be either short or dashed.
WARNING: 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." }, + GetUUIDFromPlayerName = { Params = "PlayerName, [UseOnlyCached]", Return = "UUID", Notes = "(STATIC) Returns the (short) UUID that corresponds to the given playername, or an empty string on error. If UseOnlyCached is false (the default), queries the Mojang servers if the playername is not in the cache.
WARNING: 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." }, + GetUUIDsFromPlayerNames = { Params = "PlayerNames, [UseOnlyCached]", Return = "table", Notes = "(STATIC) Returns a table that contains the map, 'PlayerName' -> '(short) 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.
WARNING: 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 or 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 or short UUIDs. Logs a warning and returns an empty string if UUID format not recognized." }, }, }, @@ -2017,7 +2017,6 @@ cPluginManager.AddHook(cPluginManager.HOOK_CHAT, OnChatMessage); ]], Functions = { - Get = { Params = "", Return = "Root object", Notes = "(STATIC)This function returns the cRoot object." }, BroadcastChat = { Params = "Message", Return = "", Notes = "Broadcasts a message to every player in the server. No formatting is done by the server." }, BroadcastChatFailure = { Params = "Message", Return = "", Notes = "Prepends Rose [INFO] / colours entire text (depending on ShouldUseChatPrefixes()) and broadcasts message. For a command that failed to run because of insufficient permissions, etc." }, BroadcastChatFatal = { Params = "Message", Return = "", Notes = "Prepends Red [FATAL] / colours entire text (depending on ShouldUseChatPrefixes()) and broadcasts message. For a plugin that crashed, or similar." }, @@ -2028,12 +2027,12 @@ cPluginManager.AddHook(cPluginManager.HOOK_CHAT, OnChatMessage); FindAndDoWithPlayer = { Params = "PlayerName, CallbackFunction", Return = "", Notes = "Calls the given callback function for all players with names partially (or fully) matching the name string provided." }, ForEachPlayer = { Params = "CallbackFunction", Return = "", Notes = "Calls the given callback function for each player. The callback function has the following signature:

function Callback({{cPlayer|cPlayer}})
" }, ForEachWorld = { Params = "CallbackFunction", Return = "", Notes = "Calls the given callback function for each world. The callback function has the following signature:
function Callback({{cWorld|cWorld}})
" }, + Get = { Params = "", Return = "Root object", Notes = "(STATIC)This function returns the cRoot object." }, GetCraftingRecipes = { Params = "", Return = "{{cCraftingRecipe|cCraftingRecipe}}", Notes = "Returns the CraftingRecipes object" }, GetDefaultWorld = { Params = "", Return = "{{cWorld|cWorld}}", Notes = "Returns the world object from the default world." }, 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." }, diff --git a/src/Bindings/ManualBindings.cpp b/src/Bindings/ManualBindings.cpp index 34f0d7e30..042ffb19e 100644 --- a/src/Bindings/ManualBindings.cpp +++ b/src/Bindings/ManualBindings.cpp @@ -2158,6 +2158,99 @@ static int tolua_cClientHandle_SendPluginMessage(lua_State * L) +static int tolua_cMojangAPI_AddPlayerNameToUUIDMapping(lua_State * L) +{ + cLuaState S(L); + if ( + !S.CheckParamUserTable(1, "cMojangAPI") || + !S.CheckParamString(2) || + !S.CheckParamString(3) || + !S.CheckParamEnd(4) + ) + { + return 0; + } + + // Retrieve the parameters: + AString UUID, PlayerName; + S.GetStackValue(2, PlayerName); + S.GetStackValue(3, UUID); + + // Store in the cache: + cRoot::Get()->GetMojangAPI().AddPlayerNameToUUIDMapping(PlayerName, UUID); + return 0; +} + + + + + +static int tolua_cMojangAPI_GetPlayerNameFromUUID(lua_State * L) +{ + cLuaState S(L); + if ( + !S.CheckParamUserTable(1, "cMojangAPI") || + !S.CheckParamString(2) || + !S.CheckParamEnd(4) + ) + { + return 0; + } + + AString UUID; + S.GetStackValue(2, UUID); + + // 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); + } + + // Return the PlayerName: + AString PlayerName = cRoot::Get()->GetMojangAPI().GetPlayerNameFromUUID(UUID, ShouldUseCacheOnly); + S.Push(PlayerName); + return 1; +} + + + + + +static int tolua_cMojangAPI_GetUUIDFromPlayerName(lua_State * L) +{ + cLuaState S(L); + if ( + !S.CheckParamUserTable(1, "cMojangAPI") || + !S.CheckParamString(2) || + !S.CheckParamEnd(4) + ) + { + return 0; + } + + AString PlayerName; + S.GetStackValue(2, PlayerName); + + // 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); + } + + // Return the UUID: + AString UUID = cRoot::Get()->GetMojangAPI().GetUUIDFromPlayerName(PlayerName, ShouldUseCacheOnly); + S.Push(UUID); + return 1; +} + + + + + static int tolua_cMojangAPI_GetUUIDsFromPlayerNames(lua_State * L) { cLuaState S(L); @@ -3158,7 +3251,10 @@ void ManualBindings::Bind(lua_State * tolua_S) tolua_endmodule(tolua_S); tolua_beginmodule(tolua_S, "cMojangAPI"); - tolua_function(tolua_S, "GetUUIDsFromPlayerNames", tolua_cMojangAPI_GetUUIDsFromPlayerNames); + tolua_function(tolua_S, "AddPlayerNameToUUIDMapping", tolua_cMojangAPI_AddPlayerNameToUUIDMapping); + tolua_function(tolua_S, "GetPlayerNameFromUUID", tolua_cMojangAPI_GetPlayerNameFromUUID); + tolua_function(tolua_S, "GetUUIDFromPlayerName", tolua_cMojangAPI_GetUUIDFromPlayerName); + tolua_function(tolua_S, "GetUUIDsFromPlayerNames", tolua_cMojangAPI_GetUUIDsFromPlayerNames); tolua_endmodule(tolua_S); tolua_beginmodule(tolua_S, "cItemGrid"); diff --git a/src/Protocol/Authenticator.cpp b/src/Protocol/Authenticator.cpp index 160564d51..984000795 100644 --- a/src/Protocol/Authenticator.cpp +++ b/src/Protocol/Authenticator.cpp @@ -191,8 +191,8 @@ bool cAuthenticator::AuthWithYggdrasil(AString & a_UserName, const AString & a_S a_UUID = cMojangAPI::MakeUUIDShort(root.get("id", "").asString()); a_Properties = root["properties"]; - // Store the player's UUID in the NameToUUID map in MojangAPI: - cRoot::Get()->GetMojangAPI().AddPlayerNameToUUIDMapping(a_UserName, a_UUID); + // Store the player's profile in the MojangAPI caches: + cRoot::Get()->GetMojangAPI().AddPlayerProfile(a_UserName, a_UUID, a_Properties); return true; } diff --git a/src/Protocol/MojangAPI.cpp b/src/Protocol/MojangAPI.cpp index f53df1cba..9fdf07380 100644 --- a/src/Protocol/MojangAPI.cpp +++ b/src/Protocol/MojangAPI.cpp @@ -25,8 +25,10 @@ const int MAX_PER_QUERY = 100; -#define DEFAULT_NAME_TO_UUID_SERVER "api.mojang.com" -#define DEFAULT_NAME_TO_UUID_ADDRESS "/profiles/minecraft" +#define DEFAULT_NAME_TO_UUID_SERVER "api.mojang.com" +#define DEFAULT_NAME_TO_UUID_ADDRESS "/profiles/minecraft" +#define DEFAULT_UUID_TO_PROFILE_SERVER "sessionserver.mojang.com" +#define DEFAULT_UUID_TO_PROFILE_ADDRESS "/session/minecraft/profile/%UUID%?unsigned=false" @@ -96,12 +98,66 @@ static const AString & StarfieldCACert(void) +//////////////////////////////////////////////////////////////////////////////// +// cMojangAPI::sProfile: + +cMojangAPI::sProfile::sProfile( + const AString & a_PlayerName, + const AString & a_UUID, + const Json::Value & a_Properties, + Int64 a_DateTime +) : + m_PlayerName(a_PlayerName), + m_UUID(a_UUID), + m_Textures(), + m_TexturesSignature(), + m_DateTime(a_DateTime) +{ + /* + Example a_Profile contents: + "properties": + [ + { + "name": "textures", + "value": "eyJ0aW1lc3RhbXAiOjE0MDcwNzAzMjEyNzEsInByb2ZpbGVJZCI6ImIxY2FmMjQyMDJhODQxYTc4MDU1YTA3OWM0NjBlZWU3IiwicHJvZmlsZU5hbWUiOiJ4b2Z0IiwiaXNQdWJsaWMiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9iNzc5YmFiZjVhNTg3Zjk0OGFkNjc0N2VhOTEyNzU0MjliNjg4Mjk1YWUzYzA3YmQwZTJmNWJmNGQwNTIifX19", + "signature": "XCty+jGEF39hEPrPhYNnCX087kPaoCjYruzYI/DS4nkL5hbjnkSM5Rh15hnUyv/FHhC8OF5rif3D1tQjtMI19KSVaXoUFXpbJM8/+PB8GDgEbX8Fc3u9nYkzOcM/xfxdYsFAdFhLQMkvase/BZLSuPhdy9DdI+TCrO7xuSTZfYmmwVuWo3w5gCY+mSIAnqltnOzaOOTcly75xvO0WYpVk7nJdnR2tvSi0wfrQPDrIg/uzhX7p0SnDqijmBU4QaNez/TNKiFxy69dAzt0RSotlQzqkDbyVKhhv9a4eY8h3pXi4UMftKEj4FAKczxLImkukJXuOn5NN15/Q+le0rJVBC60/xjKIVzltEsMN6qjWD0lQjey7WEL+4pGhCVuWY5KzuZjFvgqszuJTFz7lo+bcHiceldJtea8/fa02eTRObZvdLxbWC9ZfFY0IhpOVKfcLdno/ddDMNMQMi5kMrJ8MZZ/PcW1w5n7MMGWPGCla1kOaC55AL0QYSMGRVEZqgU9wXI5M7sHGZKGM4mWxkbEJYBkpI/p3GyxWgV6v33ZWlsz65TqlNrR1gCLaoFCm7Sif8NqPBZUAONHYon0roXhin/DyEanS93WV6i6FC1Wisscjq2AcvnOlgTo/5nN/1QsMbjNumuMGo37sqjRqlXoPb8zEUbAhhztYuJjEfQ2Rd8=" + } + ] + */ + + // Parse the Textures and TexturesSignature from the Profile: + if (!a_Properties.isArray()) + { + // Properties is not a valid array, bail out + return; + } + Json::UInt Size = a_Properties.size(); + for (Json::UInt i = 0; i < Size; i++) + { + const Json::Value & Prop = a_Properties[i]; + AString PropName = Prop.get("name", "").asString(); + if (PropName != "textures") + { + continue; + } + m_Textures = Prop.get("value", "").asString(); + m_TexturesSignature = Prop.get("signature", "").asString(); + break; + } // for i - Properties[] +} + + + + + //////////////////////////////////////////////////////////////////////////////// // cMojangAPI: cMojangAPI::cMojangAPI(void) : m_NameToUUIDServer(DEFAULT_NAME_TO_UUID_SERVER), - m_NameToUUIDAddress(DEFAULT_NAME_TO_UUID_ADDRESS) + m_NameToUUIDAddress(DEFAULT_NAME_TO_UUID_ADDRESS), + m_UUIDToProfileServer(DEFAULT_UUID_TO_PROFILE_SERVER), + m_UUIDToProfileAddress(DEFAULT_UUID_TO_PROFILE_ADDRESS) { } @@ -120,8 +176,10 @@ cMojangAPI::~cMojangAPI() 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); + m_NameToUUIDServer = a_SettingsIni.GetValueSet("MojangAPI", "NameToUUIDServer", DEFAULT_NAME_TO_UUID_SERVER); + m_NameToUUIDAddress = a_SettingsIni.GetValueSet("MojangAPI", "NameToUUIDAddress", DEFAULT_NAME_TO_UUID_ADDRESS); + m_UUIDToProfileServer = a_SettingsIni.GetValueSet("MojangAPI", "UUIDToProfileServer", DEFAULT_UUID_TO_PROFILE_SERVER); + m_UUIDToProfileAddress = a_SettingsIni.GetValueSet("MojangAPI", "UUIDToProfileAddress", DEFAULT_UUID_TO_PROFILE_ADDRESS); LoadCachesFromDisk(); } @@ -135,7 +193,7 @@ AString cMojangAPI::GetUUIDFromPlayerName(const AString & a_PlayerName, bool a_U AString lcPlayerName(a_PlayerName); StrToLower(lcPlayerName); - // Request the cache to populate any names not yet contained: + // Request the cache to query the name if not yet cached: if (!a_UseOnlyCached) { AStringVector PlayerNames; @@ -144,7 +202,8 @@ AString cMojangAPI::GetUUIDFromPlayerName(const AString & a_PlayerName, bool a_U } // Retrieve from cache: - cNameToUUIDMap::const_iterator itr = m_NameToUUID.find(lcPlayerName); + cCSLock Lock(m_CSNameToUUID); + cProfileMap::const_iterator itr = m_NameToUUID.find(lcPlayerName); if (itr == m_NameToUUID.end()) { // No UUID found @@ -157,6 +216,44 @@ AString cMojangAPI::GetUUIDFromPlayerName(const AString & a_PlayerName, bool a_U +AString cMojangAPI::GetPlayerNameFromUUID(const AString & a_UUID, bool a_UseOnlyCached) +{ + // Normalize the UUID to lowercase short format that is used as the map key: + AString UUID = StrToLower(MakeUUIDShort(a_UUID)); + + // Retrieve from caches: + { + cCSLock Lock(m_CSUUIDToProfile); + cProfileMap::const_iterator itr = m_UUIDToProfile.find(UUID); + if (itr != m_UUIDToProfile.end()) + { + return itr->second.m_PlayerName; + } + } + { + cCSLock Lock(m_CSUUIDToName); + cProfileMap::const_iterator itr = m_UUIDToName.find(UUID); + if (itr != m_UUIDToName.end()) + { + return itr->second.m_PlayerName; + } + } + + // Name not yet cached, request cache and retry: + if (!a_UseOnlyCached) + { + CacheUUIDToProfile(UUID); + return GetPlayerNameFromUUID(a_UUID, true); + } + + // No value found, none queried. Return an error: + return ""; +} + + + + + AStringVector cMojangAPI::GetUUIDsFromPlayerNames(const AStringVector & a_PlayerNames, bool a_UseOnlyCached) { // Convert all playernames to lowercase: @@ -180,7 +277,7 @@ AStringVector cMojangAPI::GetUUIDsFromPlayerNames(const AStringVector & a_Player 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); + cProfileMap::const_iterator itrN = m_NameToUUID.find(*itr); if (itrN != m_NameToUUID.end()) { res[idx] = itrN->second.m_UUID; @@ -196,10 +293,39 @@ AStringVector cMojangAPI::GetUUIDsFromPlayerNames(const AStringVector & a_Player void cMojangAPI::AddPlayerNameToUUIDMapping(const AString & a_PlayerName, const AString & a_UUID) { AString lcName(a_PlayerName); - AString UUID = MakeUUIDShort(a_UUID); + AString UUID = StrToLower(MakeUUIDShort(a_UUID)); Int64 Now = time(NULL); - cCSLock Lock(m_CSNameToUUID); - m_NameToUUID[StrToLower(lcName)] = sUUIDRecord(a_PlayerName, UUID, Now); + { + cCSLock Lock(m_CSNameToUUID); + m_NameToUUID[StrToLower(lcName)] = sProfile(a_PlayerName, UUID, "", "", Now); + } + { + cCSLock Lock(m_CSUUIDToName); + m_UUIDToName[UUID] = sProfile(a_PlayerName, UUID, "", "", Now); + } +} + + + + + +void cMojangAPI::AddPlayerProfile(const AString & a_PlayerName, const AString & a_UUID, const Json::Value & a_Properties) +{ + AString lcName(a_PlayerName); + AString UUID = StrToLower(MakeUUIDShort(a_UUID)); + Int64 Now = time(NULL); + { + cCSLock Lock(m_CSNameToUUID); + m_NameToUUID[StrToLower(lcName)] = sProfile(a_PlayerName, UUID, "", "", Now); + } + { + cCSLock Lock(m_CSUUIDToName); + m_UUIDToName[UUID] = sProfile(a_PlayerName, UUID, "", "", Now); + } + { + cCSLock Lock(m_CSUUIDToProfile); + m_UUIDToProfile[UUID] = sProfile(a_PlayerName, UUID, a_Properties, Now); + } } @@ -337,6 +463,7 @@ void cMojangAPI::LoadCachesFromDisk(void) // 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)"); + db.exec("CREATE TABLE IF NOT EXISTS UUIDToProfile (UUID, PlayerName, Textures, TexturesSignature, DateTime)"); // Clean up old entries: { @@ -345,16 +472,40 @@ void cMojangAPI::LoadCachesFromDisk(void) stmt.bind(1, LimitDateTime); stmt.exec(); } + { + SQLite::Statement stmt(db, "DELETE FROM UUIDToProfile 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()) + // 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; + UUID = StrToLower(MakeUUIDShort(UUID)); + m_NameToUUID[StrToLower(lcPlayerName)] = sProfile(PlayerName, UUID, "", "", DateTime); + m_UUIDToName[UUID] = sProfile(PlayerName, UUID, "", "", DateTime); + } + } { - 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); + SQLite::Statement stmt(db, "SELECT PlayerName, UUID, Textures, TexturesSignature, DateTime FROM UUIDToProfile"); + while (stmt.executeStep()) + { + AString PlayerName = stmt.getColumn(0); + AString UUID = stmt.getColumn(1); + AString Textures = stmt.getColumn(2); + AString TexturesSignature = stmt.getColumn(2); + Int64 DateTime = stmt.getColumn(4); + AString lcPlayerName = PlayerName; + UUID = StrToLower(MakeUUIDShort(UUID)); + m_UUIDToProfile[StrToLower(lcPlayerName)] = sProfile(PlayerName, UUID, Textures, TexturesSignature, DateTime); + } } } catch (const SQLite::Exception & ex) @@ -374,26 +525,51 @@ void cMojangAPI::SaveCachesToDisk(void) // 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)"); + db.exec("CREATE TABLE IF NOT EXISTS UUIDToProfile (UUID, PlayerName, Textures, TexturesSignature, DateTime)"); // Remove all entries: db.exec("DELETE FROM PlayerNameToUUID"); + db.exec("DELETE FROM UUIDToProfile"); - // Save all cache entries: - SQLite::Statement stmt(db, "INSERT INTO PlayerNameToUUID(PlayerName, UUID, DateTime) VALUES (?, ?, ?)"); + // Save all cache entries - m_PlayerNameToUUID: 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) + SQLite::Statement stmt(db, "INSERT INTO PlayerNameToUUID(PlayerName, UUID, DateTime) VALUES (?, ?, ?)"); + cCSLock Lock(m_CSNameToUUID); + for (cProfileMap::const_iterator itr = m_NameToUUID.begin(), end = m_NameToUUID.end(); itr != end; ++itr) { - // This item is too old, do not save - continue; + 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(); + } + } + + // Save all cache entries - m_UUIDToProfile: + { + SQLite::Statement stmt(db, "INSERT INTO UUIDToProfile(UUID, PlayerName, Textures, TexturesSignature, DateTime) VALUES (?, ?, ?, ?, ?)"); + cCSLock Lock(m_CSUUIDToProfile); + for (cProfileMap::const_iterator itr = m_UUIDToProfile.begin(), end = m_UUIDToProfile.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_UUID); + stmt.bind(2, itr->second.m_PlayerName); + stmt.bind(3, itr->second.m_Textures); + stmt.bind(4, itr->second.m_TexturesSignature); + stmt.bind(5, itr->second.m_DateTime); + stmt.exec(); + stmt.reset(); } - 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) @@ -487,22 +663,145 @@ void cMojangAPI::CacheNamesToUUIDs(const AStringVector & a_PlayerNames) // 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()) + cCSLock Lock(m_CSNameToUUID); + for (size_t idx = 0; idx < JsonCount; ++idx) { - continue; - } - AString lcName = JsonName; - m_NameToUUID[StrToLower(lcName)] = sUUIDRecord(JsonName, JsonUUID, Now); - } // for idx - root[] + Json::Value & Val = root[idx]; + AString JsonName = Val.get("name", "").asString(); + AString JsonUUID = StrToLower(MakeUUIDShort(Val.get("id", "").asString())); + if (JsonUUID.empty()) + { + continue; + } + AString lcName = JsonName; + m_NameToUUID[StrToLower(lcName)] = sProfile(JsonName, JsonUUID, "", "", Now); + } // for idx - root[] + } // cCSLock (m_CSNameToUUID) + + // Also cache the UUIDToName: + { + cCSLock Lock(m_CSUUIDToName); + for (size_t idx = 0; idx < JsonCount; ++idx) + { + Json::Value & Val = root[idx]; + AString JsonName = Val.get("name", "").asString(); + AString JsonUUID = StrToLower(MakeUUIDShort(Val.get("id", "").asString())); + if (JsonUUID.empty()) + { + continue; + } + m_UUIDToName[JsonUUID] = sProfile(JsonName, JsonUUID, "", "", Now); + } // for idx - root[] + } } // while (!NamesToQuery.empty()) } + +void cMojangAPI::CacheUUIDToProfile(const AString & a_UUID) +{ + ASSERT(a_UUID.size() == 32); + + // Check if already present: + { + if (m_UUIDToProfile.find(a_UUID) != m_UUIDToProfile.end()) + { + return; + } + } + + // Create the request address: + AString Address = m_UUIDToProfileAddress; + ReplaceString(Address, "%UUID%", a_UUID); + + // Create the HTTP request: + AString Request; + Request += "GET " + Address + " HTTP/1.0\r\n"; // We need to use HTTP 1.0 because we don't handle Chunked transfer encoding + Request += "Host: " + m_UUIDToProfileServer + "\r\n"; + Request += "User-Agent: MCServer\r\n"; + Request += "Connection: close\r\n"; + Request += "Content-Length: 0\r\n"; + Request += "\r\n"; + + // Get the response from the server: + AString Response; + if (!SecureRequest(m_UUIDToProfileServer, Request, Response)) + { + return; + } + + // 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()); + return; + } + + // 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()); + return; + } + Response.erase(0, idxHeadersEnd + 4); + + // Parse the returned string into Json: + Json::Reader reader; + Json::Value root; + if (!reader.parse(Response, root, false) || !root.isObject()) + { + 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()); + return; + } + + /* Example response: + { + "id": "b1caf24202a841a78055a079c460eee7", + "name": "xoft", + "properties": + [ + { + "name": "textures", + "value": "eyJ0aW1lc3RhbXAiOjE0MDcwNzAzMjEyNzEsInByb2ZpbGVJZCI6ImIxY2FmMjQyMDJhODQxYTc4MDU1YTA3OWM0NjBlZWU3IiwicHJvZmlsZU5hbWUiOiJ4b2Z0IiwiaXNQdWJsaWMiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9iNzc5YmFiZjVhNTg3Zjk0OGFkNjc0N2VhOTEyNzU0MjliNjg4Mjk1YWUzYzA3YmQwZTJmNWJmNGQwNTIifX19", + "signature": "XCty+jGEF39hEPrPhYNnCX087kPaoCjYruzYI/DS4nkL5hbjnkSM5Rh15hnUyv/FHhC8OF5rif3D1tQjtMI19KSVaXoUFXpbJM8/+PB8GDgEbX8Fc3u9nYkzOcM/xfxdYsFAdFhLQMkvase/BZLSuPhdy9DdI+TCrO7xuSTZfYmmwVuWo3w5gCY+mSIAnqltnOzaOOTcly75xvO0WYpVk7nJdnR2tvSi0wfrQPDrIg/uzhX7p0SnDqijmBU4QaNez/TNKiFxy69dAzt0RSotlQzqkDbyVKhhv9a4eY8h3pXi4UMftKEj4FAKczxLImkukJXuOn5NN15/Q+le0rJVBC60/xjKIVzltEsMN6qjWD0lQjey7WEL+4pGhCVuWY5KzuZjFvgqszuJTFz7lo+bcHiceldJtea8/fa02eTRObZvdLxbWC9ZfFY0IhpOVKfcLdno/ddDMNMQMi5kMrJ8MZZ/PcW1w5n7MMGWPGCla1kOaC55AL0QYSMGRVEZqgU9wXI5M7sHGZKGM4mWxkbEJYBkpI/p3GyxWgV6v33ZWlsz65TqlNrR1gCLaoFCm7Sif8NqPBZUAONHYon0roXhin/DyEanS93WV6i6FC1Wisscjq2AcvnOlgTo/5nN/1QsMbjNumuMGo37sqjRqlXoPb8zEUbAhhztYuJjEfQ2Rd8=" + } + ] + } + */ + + // Store the returned result into caches: + AString PlayerName = root.get("name", "").asString(); + if (PlayerName.empty()) + { + // No valid playername, bail out + return; + } + Json::Value Properties = root.get("properties", ""); + Int64 Now = time(NULL); + { + cCSLock Lock(m_CSUUIDToProfile); + m_UUIDToProfile[a_UUID] = sProfile(PlayerName, a_UUID, Properties, Now); + } + { + cCSLock Lock(m_CSUUIDToName); + m_UUIDToName[a_UUID] = sProfile(PlayerName, a_UUID, Properties, Now); + } + { + AString lcPlayerName(PlayerName); + cCSLock Lock(m_CSNameToUUID); + m_NameToUUID[StrToLower(lcPlayerName)] = sProfile(PlayerName, a_UUID, Properties, Now); + } +} + + + + diff --git a/src/Protocol/MojangAPI.h b/src/Protocol/MojangAPI.h index 7f3ef4e39..08e799c73 100644 --- a/src/Protocol/MojangAPI.h +++ b/src/Protocol/MojangAPI.h @@ -11,6 +11,11 @@ #include +namespace Json +{ + class Value; +} + @@ -45,14 +50,24 @@ public: Note: only checks the string's length, not the actual content. */ static AString MakeUUIDDashed(const AString & a_UUID); + // tolua_end + /** Converts a player name into a UUID. The UUID will be empty on error. If a_UseOnlyCached is true, the function only consults the cached values. If a_UseOnlyCached is false and the name is not found in the cache, it is looked up online, which is a blocking - operation, do not use this in world-tick thread! */ + operation, do not use this in world-tick thread! + If you have multiple names to resolve, use the GetUUIDsFromPlayerNames() function, it uses a single request for multiple names. */ AString GetUUIDFromPlayerName(const AString & a_PlayerName, bool a_UseOnlyCached = false); - // tolua_end + /** Converts a UUID into a playername. + The returned playername will be empty on error. + Both short and dashed UUID formats are accepted. + Uses both m_UUIDToName and m_UUIDToProfile to search for the value. Uses m_UUIDToProfile for cache. + If a_UseOnlyCached is true, the function only consults the cached values. + If a_UseOnlyCached is false and the name is not found in the cache, it is looked up online, which is a blocking + operation, do not use this in world-tick thread! */ + AString GetPlayerNameFromUUID(const AString & a_UUID, bool a_UseOnlyCached = false); /** Converts the player names into UUIDs. a_PlayerName[idx] will be converted to UUID and returned as idx-th value @@ -62,36 +77,61 @@ public: 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 - + /** Called by the Authenticator to add a profile that it has received from authenticating a user. Adds + the profile to the respective mapping caches and updtes their datetime stamp to now. */ + void AddPlayerProfile(const AString & a_PlayerName, const AString & a_UUID, const Json::Value & a_Properties); + protected: - struct sUUIDRecord + /** Holds data for a single player profile. */ + struct sProfile { - AString m_PlayerName; // Case-correct playername - AString m_UUID; - Int64 m_DateTime; // UNIXtime of the UUID lookup + AString m_PlayerName; // Case-correct playername + AString m_UUID; // Short lowercased UUID + AString m_Textures; // The Textures field of the profile properties + AString m_TexturesSignature; // The signature of the Textures field of the profile properties + Int64 m_DateTime; // UNIXtime of the profile lookup - sUUIDRecord(void) : + /** Default constructor for the container's sake. */ + sProfile(void) : + m_PlayerName(), m_UUID(), + m_Textures(), + m_TexturesSignature(), m_DateTime(time(NULL)) { } - sUUIDRecord(const AString & a_PlayerName, const AString & a_UUID, Int64 a_DateTime) : + /** Constructor for the storage creation. */ + sProfile( + const AString & a_PlayerName, + const AString & a_UUID, + const AString & a_Textures, + const AString & a_TexturesSignature, + Int64 a_DateTime + ) : m_PlayerName(a_PlayerName), m_UUID(a_UUID), + m_Textures(a_Textures), + m_TexturesSignature(a_TexturesSignature), m_DateTime(a_DateTime) { } + + /** Constructor that parses the values from the Json profile. */ + sProfile( + const AString & a_PlayerName, + const AString & a_UUID, + const Json::Value & a_Properties, + Int64 a_DateTime + ); }; - typedef std::map cNameToUUIDMap; // maps Lowercased PlayerName to sUUIDRecord + typedef std::map cProfileMap; + /** The server to connect to when converting player names to UUIDs. For example "api.mojang.com". */ AString m_NameToUUIDServer; @@ -100,12 +140,32 @@ protected: 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; + /** The server to connect to when converting UUID to profile. For example "sessionserver.mojang.com". */ + AString m_UUIDToProfileServer; + + /** The URL to use for converting UUID to profile, without the server part. + Will replace %UUID% with the actual UUID. For example "session/minecraft/profile/%UUID%?unsigned=false". */ + AString m_UUIDToProfileAddress; + + /** Cache for the Name-to-UUID lookups. The map key is lowercased PlayerName. Protected by m_CSNameToUUID. */ + cProfileMap m_NameToUUID; /** Protects m_NameToUUID against simultaneous multi-threaded access. */ cCriticalSection m_CSNameToUUID; + /** Cache for the Name-to-UUID lookups. The map key is lowercased short UUID. Protected by m_CSUUIDToName. */ + cProfileMap m_UUIDToName; + + /** Protects m_UUIDToName against simultaneous multi-threaded access. */ + cCriticalSection m_CSUUIDToName; + + /** Cache for the UUID-to-profile lookups. The map key is lowercased short UUID. + Protected by m_CSUUIDToProfile. */ + cProfileMap m_UUIDToProfile; + + /** Protects m_UUIDToProfile against simultaneous multi-threaded access. */ + cCriticalSection m_CSUUIDToProfile; + /** Loads the caches from a disk storage. */ void LoadCachesFromDisk(void); @@ -113,10 +173,15 @@ protected: /** 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. + /** Makes sure all specified names are in the m_PlayerNameToUUID 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); + + /** Makes sure the specified UUID is in the m_UUIDToProfile cache. If missing, downloads it from Mojang API servers. + UUIDs that are not valid will not be added into the cache. + ASSUMEs that a_UUID is a lowercased short UUID. */ + void CacheUUIDToProfile(const AString & a_UUID); } ; // tolua_export diff --git a/src/Root.h b/src/Root.h index 5cb82edda..1cd175ab4 100644 --- a/src/Root.h +++ b/src/Root.h @@ -88,7 +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 + cMojangAPI & GetMojangAPI (void) { return m_MojangAPI; } /** Queues a console command for execution through the cServer class. The command will be executed in the tick thread -- cgit v1.2.3