#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules #include "Protocol/Authenticator.h" #include "ClientHandle.h" #include "HTTP/UrlClient.h" #include "HTTP/UrlParser.h" #include "IniFile.h" #include "JsonUtils.h" #include "json/json.h" #include "Protocol/MojangAPI.h" #include "Root.h" #include "Server.h" #include "UUID.h" constexpr char DEFAULT_AUTH_SERVER[] = "sessionserver.mojang.com"; constexpr char DEFAULT_AUTH_ADDRESS[] = "/session/minecraft/hasJoined?username=%USERNAME%&serverId=%SERVERID%"; cAuthenticator::cAuthenticator(void) : Super("Authenticator"), m_Server(DEFAULT_AUTH_SERVER), m_Address(DEFAULT_AUTH_ADDRESS), m_ShouldAuthenticate(true) { } cAuthenticator::~cAuthenticator() { Stop(); } void cAuthenticator::ReadSettings(cSettingsRepositoryInterface & a_Settings) { m_Server = a_Settings.GetValueSet ("Authentication", "Server", DEFAULT_AUTH_SERVER); m_Address = a_Settings.GetValueSet ("Authentication", "Address", DEFAULT_AUTH_ADDRESS); m_ShouldAuthenticate = a_Settings.GetValueSetB("Authentication", "Authenticate", true); // prepend https:// if missing constexpr std::string_view HttpPrefix = "http://"; constexpr std::string_view HttpsPrefix = "https://"; if ( (std::string_view(m_Server).substr(0, HttpPrefix.size()) != HttpPrefix) && (std::string_view(m_Server).substr(0, HttpsPrefix.size()) != HttpsPrefix) ) { m_Server = "https://" + m_Server; } { auto [IsSuccessfull, ErrorMessage] = cUrlParser::Validate(m_Server); if (!IsSuccessfull) { LOGWARNING("%s %d: Supplied invalid URL for configuration value [Authentication: Server]: \"%s\", using default! Error: %s", __FUNCTION__, __LINE__, m_Server.c_str(), ErrorMessage.c_str()); m_Server = DEFAULT_AUTH_SERVER; } } { auto [IsSuccessfull, ErrorMessage] = cUrlParser::Validate(m_Server); if (!IsSuccessfull) { LOGWARNING("%s %d: Supplied invalid URL for configuration value [Authentication: Address]: \"%s\", using default! Error: %s", __FUNCTION__, __LINE__, m_Address.c_str(), ErrorMessage.c_str()); m_Address = DEFAULT_AUTH_ADDRESS; } } } void cAuthenticator::Authenticate(int a_ClientID, const std::string_view a_Username, const std::string_view a_ServerHash) { if (!m_ShouldAuthenticate) { // An "authenticated" username, which is just what the client sent since auth is off. std::string OfflineUsername(a_Username); // A specially constructed UUID based wholly on the username. const auto OfflineUUID = cClientHandle::GenerateOfflineUUID(OfflineUsername); // "Authenticate" the user based on what little information we have: cRoot::Get()->GetServer()->AuthenticateUser(a_ClientID, std::move(OfflineUsername), OfflineUUID, Json::Value()); return; } cCSLock Lock(m_CS); m_Queue.emplace_back(a_ClientID, a_Username, a_ServerHash); m_QueueNonempty.Set(); } void cAuthenticator::Start(cSettingsRepositoryInterface & a_Settings) { ReadSettings(a_Settings); Super::Start(); } void cAuthenticator::Stop(void) { m_ShouldTerminate = true; m_QueueNonempty.Set(); Super::Stop(); } void cAuthenticator::Execute(void) { for (;;) { cCSLock Lock(m_CS); while (!m_ShouldTerminate && (m_Queue.size() == 0)) { cCSUnlock Unlock(Lock); m_QueueNonempty.Wait(); } if (m_ShouldTerminate) { return; } ASSERT(!m_Queue.empty()); cAuthenticator::cUser User = std::move(m_Queue.front()); int & ClientID = User.m_ClientID; AString & Username = User.m_Name; AString & ServerID = User.m_ServerID; m_Queue.pop_front(); Lock.Unlock(); cUUID UUID; Json::Value Properties; if (AuthWithYggdrasil(Username, ServerID, UUID, Properties)) { LOGINFO("User %s authenticated with UUID %s", Username.c_str(), UUID.ToShortString().c_str()); cRoot::Get()->GetServer()->AuthenticateUser(ClientID, std::move(Username), UUID, std::move(Properties)); } else { cRoot::Get()->KickUser(ClientID, "Failed to authenticate account!"); } } // for (-ever) } bool cAuthenticator::AuthWithYggdrasil(AString & a_UserName, const AString & a_ServerId, cUUID & a_UUID, Json::Value & a_Properties) const { LOGD("Trying to authenticate user %s", a_UserName.c_str()); // Create the GET request: AString ActualAddress = m_Address; ReplaceURL(ActualAddress, "%USERNAME%", a_UserName); ReplaceURL(ActualAddress, "%SERVERID%", a_ServerId); // Create and send the HTTP request auto [IsSuccessfull, Response] = cUrlClient::BlockingGet(m_Server + ActualAddress); if (!IsSuccessfull) { return false; } // Parse the Json response: if (Response.empty()) { return false; } Json::Value root; if (!JsonUtils::ParseString(Response, root)) { LOGWARNING("%s: Cannot parse received data (authentication) to JSON!", __FUNCTION__); return false; } a_UserName = root.get("name", "Unknown").asString(); a_Properties = root["properties"]; if (!a_UUID.FromString(root.get("id", "").asString())) { LOGWARNING("%s: Received invalid UUID format", __FUNCTION__); return false; } // Store the player's profile in the MojangAPI caches: cRoot::Get()->GetMojangAPI().AddPlayerProfile(a_UserName, a_UUID, a_Properties); return true; } #ifdef ENABLE_PROPERTIES /* In case we want to export this function to the plugin API later - don't forget to add the relevant INI configuration lines for DEFAULT_PROPERTIES_ADDRESS */ #define DEFAULT_PROPERTIES_ADDRESS "/session/minecraft/profile/%UUID%" // Gets the properties, such as skin, of a player based on their UUID via Mojang's API bool GetPlayerProperties(const AString & a_UUID, Json::Value & a_Properties); bool cAuthenticator::GetPlayerProperties(const AString & a_UUID, Json::Value & a_Properties) { LOGD("Trying to get properties for user %s", a_UUID.c_str()); // Create and send the HTTP request auto [IsSuccessfull, Response] = cUrlClient::BlockingGet(m_Server + ActualAddress); if (!IsSuccessfull) { return false; } // Parse the Json response: if (Response.empty()) { return false; } Json::Value root; Json::Reader reader; if (!reader.parse(Response, root, false)) { LOGWARNING("cAuthenticator: Cannot parse received properties data to JSON!"); return false; } a_Properties = root["properties"]; return true; } #endif