diff --git a/Changelog.txt b/Changelog.txt index b87a946fc..f758cf120 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -4120,3 +4120,10 @@ When setting a property like MORE to the a spell or skill defname, trying to rea - Fixed: Hit chance to paralyzed targets with sphere custom (0) formula is now 80-100% (was 0-10%). - Added: Implemented Hit chance increase / decrease to sphere custom formula (0). - Fixed: Equiping armor and weapon with spelleffect could crash the SphereServer (#1538) + +24-03-2026, Mulambo +- Fixed: issue when web client sent a web identity packet and Sphere didn't let him log in +- Added: Classic UO Web Identity packet validation support (#1357): 2 new sphere.ini settings: `WebIdentity` and `WebIdentityForce`: + - If you add `WebIdentity` value from your Classic UO Web Management, received WebIdentity will be checked and validated + - If you add `WebIdentity` and set `WebIdentityForce` to 1, it will prohibit any client without web identity to log in + - If you don't add `WebIdentity`, behaviour will stay as it was diff --git a/src/game/CServerConfig.cpp b/src/game/CServerConfig.cpp index 170b45597..987512dc0 100644 --- a/src/game/CServerConfig.cpp +++ b/src/game/CServerConfig.cpp @@ -736,6 +736,8 @@ enum RC_TYPE RC_VERSION, RC_WALKBUFFER, RC_WALKREGEN, + RC_WEBIDENTITY, + RC_WEBIDENTITYFORCE, RC_WOOLGROWTHTIME, // m_iWoolGrowthTime RC_WOPCOLOR, RC_WOPFONT, @@ -1031,7 +1033,9 @@ const CAssocReg CServerConfig::sm_szLoadKeys[RC_QTY + 1] { "VERSION", { ELEM_VOID, 0 }}, { "WALKBUFFER", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWalkBuffer) }}, { "WALKREGEN", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWalkRegen) }}, - { "WOOLGROWTHTIME", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWoolGrowthTime) }}, + { "WEBIDENTITY", { ELEM_CSTRING, static_castOFFSETOF(CServerConfig,m_sWebIdentity) }}, + { "WEBIDENTITYFORCE", { ELEM_BOOL, static_castOFFSETOF(CServerConfig,m_sWebIdentityForce) }}, + { "WOOLGROWTHTIME", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWoolGrowthTime) }}, { "WOPCOLOR", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWordsOfPowerColor) }}, { "WOPFONT", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iWordsOfPowerFont) }}, { "WOPPLAYER", { ELEM_BOOL, static_castOFFSETOF(CServerConfig,m_fWordsOfPowerPlayer) }}, @@ -1518,7 +1522,13 @@ bool CServerConfig::r_LoadVal( CScript &s ) case RC_WALKBUFFER: m_iWalkBuffer = s.GetArgVal() * MSECS_PER_TENTH; break; - case RC_MEDITATIONMOVEMENTABORT: + case RC_WEBIDENTITY: + m_sWebIdentity = s.GetArgStr(); + break; + case RC_WEBIDENTITYFORCE: + m_sWebIdentityForce = s.GetArgVal() > 0; + break; + case RC_MEDITATIONMOVEMENTABORT: _fMeditationMovementAbort = s.GetArgVal() > 0 ? true : false; break; default: diff --git a/src/game/CServerConfig.h b/src/game/CServerConfig.h index 810d3045e..8a25ee82e 100644 --- a/src/game/CServerConfig.h +++ b/src/game/CServerConfig.h @@ -458,6 +458,11 @@ extern class CServerConfig : public CResourceHolder bool m_fCUOStatus; // Enable or disable the response to ConnectUO pings bool m_fUOGStatus; // Enable or disable the response to UOGateway pings + // Web identity secret from Classic UO Web Shard management. + CSString m_sWebIdentity = {false}; + // Force only clients with web identity. + bool m_sWebIdentityForce; + int64 m_iWalkBuffer; // Walk limiting code: buffer size (in tenths of second). int m_iWalkRegen; // Walk limiting code: regen speed (%) diff --git a/src/game/clients/CClient.h b/src/game/clients/CClient.h index 7b7a587be..265e7364a 100644 --- a/src/game/clients/CClient.h +++ b/src/game/clients/CClient.h @@ -150,6 +150,18 @@ class CClient : public CSObjListRec, public CScriptObj, public CChatChanMember, int64 m_timeLastEventWalk; // Last time we got a walk event from client int64 m_timeNextEventWalk; // Fastwalk prevention: only allow more walk requests after this timer + // Web Identity. + struct CWebIdentity + { + bool m_fReceived{false}; + CSString m_sUserId; + CSString m_sConnectingIp; + CSString m_sExternalAuthProvider; + CSString m_sExternalAuthUsername; + CSString m_sExternalAuthId; + CSString m_sRole; + } m_webIdentity; + // Context of the targetting setup. depends on CLIMODE_TYPE m_Targ_Mode union { diff --git a/src/game/clients/CClientLog.cpp b/src/game/clients/CClientLog.cpp index 24ae4eb17..e9e6723b1 100644 --- a/src/game/clients/CClientLog.cpp +++ b/src/game/clients/CClientLog.cpp @@ -112,7 +112,7 @@ bool CClient::addLoginErr(byte code) "Timeout / Wrong encryption / Unknown error", "Invalid client version. See the CLIENTVERSION setting in " SPHERE_FILE ".ini", "Invalid character selected (chosen character does not exist)", - "AuthID is not correct. This normally means that the client did not log in via the login server", + "AuthID is not correct. This means that the client did not log in via the login server or Web Identity secret doesn't match", "The account details entered are invalid (username or password is too short, too long or contains invalid characters). This can sometimes be caused by incorrect/missing encryption keys", "The account details entered are invalid (username or password is too short, too long or contains invalid characters). This can sometimes be caused by incorrect/missing encryption keys", "Encryption error: packet length does not match what was expected", @@ -848,6 +848,12 @@ bool CClient::xProcessClientSetup( CEvent * pEvent, uint uiLen ) ASSERT( pEvent != nullptr ); ASSERT( uiLen > 0 ); + // Web identity packet. Validation is handled in packet itself. + if (pEvent->Default.m_Cmd == XCMD_Spy && uiLen == 149) + { + return true; + } + // Try all client versions on the msg. if ( !m_Crypt.Init( m_net->m_seed, pEvent->m_Raw, uiLen, GetNetState()->isClientKR() ) ) { diff --git a/src/network/CNetworkInput.cpp b/src/network/CNetworkInput.cpp index 57443969e..fac98ddaa 100644 --- a/src/network/CNetworkInput.cpp +++ b/src/network/CNetworkInput.cpp @@ -539,10 +539,10 @@ bool CNetworkInput::processUnknownClientData(CNetState* state, Packet* buffer) fHTTPReq = (uiOrigRemainingLength >= 5 && memcmp(pOrigRemainingData, "GET /", 5) == 0) || (uiOrigRemainingLength >= 6 && memcmp(pOrigRemainingData, "POST /", 6) == 0); } - if (!fHTTPReq && (uiOrigRemainingLength > INT8_MAX)) + if (!fHTTPReq && (uiOrigRemainingLength > UINT8_MAX)) { g_Log.EventWarn("%x:Client connected with a seed length of %u exceeding max length limit of %d, disconnecting.\n", - state->id(), uiOrigRemainingLength, INT8_MAX); + state->id(), uiOrigRemainingLength, UINT8_MAX); return false; } diff --git a/src/network/receive.cpp b/src/network/receive.cpp index 1106e32a0..9fed8d992 100644 --- a/src/network/receive.cpp +++ b/src/network/receive.cpp @@ -1943,6 +1943,20 @@ bool PacketServerSelect::onReceive(CNetState* net) uint server = readInt16(); + // Web Identity validation. + if (g_Cfg.m_sWebIdentity.IsValid() && g_Cfg.m_sWebIdentityForce) + { + CClient* client = net->getClient(); + ASSERT(client); + + // We did not receive validation packet (0xa4). The packet itself is validated elsewhere. + if (!client->m_webIdentity.m_fReceived) + { + client->addLoginErr(PacketLoginError::BadAuthID); + return false; + } + } + net->getClient()->Login_Relay(server); return true; } @@ -1962,12 +1976,85 @@ PacketSystemInfo::PacketSystemInfo() : Packet(149) bool PacketSystemInfo::onReceive(CNetState* net) { ADDTOCALLSTACK("PacketSystemInfo::onReceive"); - UnreferencedParameter(net); - skip(148); - return true; -} + // Not using web identity. + if (!g_Cfg.m_sWebIdentity.IsValid()) + { + UnreferencedParameter(net); + skip(148); + return true; + } + + // Web identity check. + char clientType[7] = {}; + readStringASCII(clientType, 6, false); + const uint8 version = readByte(); + + // Type or version doesn't match, might not be Web Identity. + if (strcmp(clientType, "CUOWEB") != 0 || version != 1) { + skip(141); + + return true; + } + + // We are using web identity, and we passed the version check. Assign data we received from it. + CClient* client = net->getClient(); + ASSERT(client); + + // Skip timestamp. + skip(4); + + // Buffer for reading data from packet. + char dataBuffer[30]; + + // Remaining length of Web Identity packet (so we don't eat bytes from another packet). + int length = 137; + + // Read the received secret and compare it to our secret. + length -= readStringNullASCII(dataBuffer, length); + + // Secret is not valid. + if (strcmp(dataBuffer, g_Cfg.m_sWebIdentity) != 0) + { + skip(length); + client->addLoginErr(PacketLoginError::BadAuthID); + return false; + } + + // Client is validated, we can now populate Web Identity data. + CClient::CWebIdentity &Identity = client->m_webIdentity; + Identity.m_fReceived = true; + + // User ID. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sUserId = dataBuffer; + + // Connecting IP. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sConnectingIp = dataBuffer; + + // External Auth Provider. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sExternalAuthProvider = dataBuffer; + + // External Auth Username. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sExternalAuthUsername = dataBuffer; + + // External Auth ID. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sExternalAuthId = dataBuffer; + + // Role. + length -= readStringNullASCII(dataBuffer, length); + Identity.m_sRole = dataBuffer; + + // Skip the rest of the packet. + skip(length); + + return true; +} /*************************************************************************** * diff --git a/src/sphere.ini b/src/sphere.ini index 077cb5140..326bf01f6 100644 --- a/src/sphere.ini +++ b/src/sphere.ini @@ -1203,6 +1203,12 @@ CUOStatus=1 // If enabled, it returns: Name, Age, Clients, Items, Chars and Memory UOGStatus=1 +// Classic UO web identity. +// Enable by uncommenting and placing your generated secret in shard management. +//WebIdentity=YOUR_SHARD_SECRET +// Set to 1 to reject all clients without WebIdentity secret. +WebIdentityForce=0 + // Add these Resource Sections to the defname list, so that they can be accessible via DEFLIST.* [RESOURCELIST] ITEMDEF