Skip to content

[ZH] Prevent hang in network lobby with long player names #1119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 130 additions & 21 deletions GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
#include "GameNetwork/LANAPI.h" // for testing packet size
#include "GameNetwork/LANAPICallbacks.h" // for testing packet size
#include "strtok_r.h"
#include <algorithm>
#include <utility>



Expand Down Expand Up @@ -892,7 +894,100 @@ Bool GameInfo::isSandbox(void)

static const char slotListID = 'S';

AsciiString GameInfoToAsciiString( const GameInfo *game )
struct LengthIndexPair
{
Int Length;
size_t Index;
friend bool operator<(const LengthIndexPair& lhs, const LengthIndexPair& rhs)
{
if (lhs.Length == rhs.Length)
return lhs.Index < rhs.Index;
return lhs.Length < rhs.Length;
}
friend bool operator>(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return rhs < lhs; }
friend bool operator<=(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return !(lhs > rhs); }
friend bool operator>=(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return !(lhs < rhs); }
};

static AsciiStringVec BuildPlayerNames(const GameInfo& game)
{
AsciiStringVec playerNames;
playerNames.resize(MAX_SLOTS);

for (Int i = 0; i < MAX_SLOTS; ++i)
{
const GameSlot* slot = game.getConstSlot(i);
if (slot->isHuman())
{
playerNames[i] = WideCharStringToMultiByte(slot->getName().str()).c_str();
}
}

return playerNames;
}

static Bool TruncatePlayerNames(AsciiStringVec& playerNames, UnsignedInt truncateAmount)
{
// wont truncate any name to below this length
CONSTEXPR const Int MinimumNameLength = 2;
UnsignedInt availableForTruncation = 0;

// make length+index pairs for the player names
std::vector<LengthIndexPair> lengthIndex;
lengthIndex.resize(playerNames.size());
for (size_t pi = 0; pi < playerNames.size(); ++pi)
{
Int playerNameLength = playerNames[pi].getLength();
lengthIndex[pi].Length = playerNameLength;
lengthIndex[pi].Index = pi;
availableForTruncation += std::max(0, playerNameLength - MinimumNameLength);
}

if (truncateAmount > availableForTruncation)
{
DEBUG_LOG(("TruncatePlayerNames - Requested to truncate %u chars from player names, but only %u were available for truncation.", truncateAmount, availableForTruncation));
return false;
}

// sort based on length in descending order
std::sort(lengthIndex.begin(), lengthIndex.end(), std::greater<LengthIndexPair>());

for (size_t i = 0; i < lengthIndex.size(); ++i)
{
size_t remainingEntries = lengthIndex.size() - i;
// determine average length based on the total amount of characters available for truncation and how many are remaining to be removed
int avgLengthForRemaining = ((availableForTruncation - truncateAmount) + (remainingEntries * MinimumNameLength)) / remainingEntries;
if (lengthIndex[i].Length > avgLengthForRemaining)
{
int truncateCurrent = lengthIndex[i].Length - avgLengthForRemaining;
playerNames[lengthIndex[i].Index].truncateBy(truncateCurrent);
truncateAmount -= truncateCurrent;
}

availableForTruncation -= lengthIndex[i].Length - MinimumNameLength;

if (truncateAmount == 0)
{
break;
}
}

// ensure there are no duplicates in the truncated names
std::set<AsciiString> uniqueNames;
for (size_t ni = 0; ni < playerNames.size(); ++ni)
{
while (!uniqueNames.insert(playerNames[ni]).second)
{
// the name already exists, change the last char to a random between a and z to ensure differentiation
playerNames[ni].removeLastChar();
playerNames[ni].concat(GameClientRandomValue('a', 'z'));
}
}

return true;
}

AsciiString GameInfoToAsciiString(const GameInfo *game, const AsciiStringVec& playerNames)
{
if (!game)
return AsciiString::TheEmptyString;
Expand All @@ -918,7 +1013,7 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
newMapName.concat(token);
mapName.nextToken(&token, "\\/");
}
DEBUG_LOG(("Map name is %s", mapName.str()));
DEBUG_LOG(("Map name is %s", newMapName.str()));
}

AsciiString optionsString;
Expand All @@ -936,23 +1031,13 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
AsciiString str;
if (slot && slot->isHuman())
{
AsciiString tmp; //all this data goes after name
tmp.format( ",%X,%d,%c%c,%d,%d,%d,%d,%d:",
slot->getIP(), slot->getPort(),
(slot->isAccepted()?'T':'F'),
(slot->hasMap()?'T':'F'),
str.format( "H%s,%X,%d,%c%c,%d,%d,%d,%d,%d:",
playerNames[i].str(), slot->getIP(),
slot->getPort(), (slot->isAccepted() ? 'T' : 'F'),
(slot->hasMap() ? 'T' : 'F'),
slot->getColor(), slot->getPlayerTemplate(),
slot->getStartPos(), slot->getTeamNumber(),
slot->getNATBehavior() );
//make sure name doesn't cause overflow of m_lanMaxOptionsLength
int lenCur = tmp.getLength() + optionsString.getLength() + 2; //+2 for H and trailing ;
int lenRem = m_lanMaxOptionsLength - lenCur; //length remaining before overflowing
int lenMax = lenRem / (MAX_SLOTS-i); //share lenRem with all remaining slots
AsciiString name = WideCharStringToMultiByte(slot->getName().str()).c_str();
while( name.getLength() > lenMax )
name.removeLastChar(); //what a horrible way to truncate. I hate AsciiString.

str.format( "H%s%s", name.str(), tmp.str() );
slot->getNATBehavior());
}
else if (slot && slot->isAI())
{
Expand Down Expand Up @@ -984,13 +1069,37 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
}
optionsString.concat(';');

DEBUG_ASSERTCRASH(!TheLAN || (optionsString.getLength() < m_lanMaxOptionsLength),
("WARNING: options string is longer than expected! Length is %d, but max is %d!",
optionsString.getLength(), m_lanMaxOptionsLength));

return optionsString;
}

AsciiString GameInfoToAsciiString(const GameInfo* game)
{
if (!game)
{
return AsciiString::TheEmptyString;
}

AsciiStringVec playerNames = BuildPlayerNames(*game);
AsciiString infoString = GameInfoToAsciiString(game, playerNames);

// TheSuperHackers @bugfix Safely truncate the game info string by
// stripping characters off of player names if the overall length is too large.
if (TheLAN && (infoString.getLength() > m_lanMaxOptionsLength))
{
const UnsignedInt truncateAmount = infoString.getLength() - m_lanMaxOptionsLength;
if (!TruncatePlayerNames(playerNames, truncateAmount))
{
DEBUG_CRASH(("WARNING: options string is longer than expected! Length is %d, but max is %d. Attempted to truncate player names by %u characters, but was unsuccessful!",
infoString.getLength(), m_lanMaxOptionsLength, truncateAmount));
return AsciiString::TheEmptyString;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So originally it would have returned the long string if it was unable to (theoretically) truncate. What happens if we return an empty string or a string that is longer than the cap?

}

infoString = GameInfoToAsciiString(game, playerNames);
}

return infoString;
}

static Int grabHexInt(const char *s)
{
char tmp[5] = "0xff";
Expand Down
2 changes: 1 addition & 1 deletion GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,7 @@ void LANAPI::RequestGameStartTimer( Int seconds )

void LANAPI::RequestGameOptions( AsciiString gameOptions, Bool isPublic, UnsignedInt ip /* = 0 */ )
{
DEBUG_ASSERTCRASH(gameOptions.getLength() < m_lanMaxOptionsLength, ("Game options string is too long!"));
DEBUG_ASSERTCRASH(gameOptions.getLength() <= m_lanMaxOptionsLength, ("Game options string is too long!"));

if (!m_currentGame)
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ void LANAPI::handleRequestGameInfo( LANMessage *msg, UnsignedInt senderIP )

AsciiString gameOpts = GameInfoToAsciiString(m_currentGame);
strncpy(reply.GameInfo.options,gameOpts.str(),m_lanMaxOptionsLength);
reply.GameInfo.options[m_lanMaxOptionsLength] = 0;
wcsncpy(reply.GameInfo.gameName, m_currentGame->getName().str(), g_lanGameNameLength);
reply.GameInfo.gameName[g_lanGameNameLength] = 0;
reply.GameInfo.inProgress = m_currentGame->isGameInProgress();
Expand Down
Loading