Files
UnrealEngine/Engine/Source/Runtime/Online/Stomp/Private/StompFrame.cpp
2025-05-18 13:04:45 +08:00

237 lines
6.2 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "StompFrame.h"
#include "GenericPlatform/GenericPlatformStackWalk.h"
#include "StompLog.h"
#if WITH_STOMP
// Anonymous name space for some frame parsing helpers
namespace
{
const FLazyName ContentLengthHeader(TEXT("content-length"));
const uint8* MatchDelimiter(uint8 Element, const char* Delimiters)
{
const uint8* Found = (const uint8*)FCStringAnsi::Strchr((const char*)Delimiters, Element);
return Found;
}
static uint8 ReadValue(const uint8* In, SIZE_T Length, SIZE_T& Index, FStompBuffer& Buffer, const char* Delimiters="\n", bool bAllowEscaping = true)
{
bool bEscapeNext = false;
uint8 Retval = '\0';
for(; Index < Length; Index++)
{
if (!bEscapeNext)
{
if (bAllowEscaping && In[Index] == '\\')
{
bEscapeNext = true;
continue;
}
const uint8* Found = MatchDelimiter(In[Index], Delimiters);
if (Found != nullptr)
{
Retval = *Found;
Index++;
break;
}
}
Buffer.Add(In[Index]);
bEscapeNext = false;
}
// The stomp protocol also allows \r\n in addition to \n as line delimiter -- simply trim the \r off the end if present
// (Unhandled edge case: In case the buffer contains an escaped CR followed by a terminating newline, the CR will be stripped off in any case)
if (Retval == '\n' && Buffer.Num() > 0 && Buffer[Buffer.Num()-1] == '\r')
{
Buffer.RemoveAt(Buffer.Num()-1);
}
// Add string terminator at the end of the buffer
Buffer.Add('\0');
return Retval;
}
void SkipNewlines(const uint8* In, SIZE_T Length, SIZE_T& Index)
{
while (Index < Length && MatchDelimiter(In[Index], "\r\n") != nullptr)
{
Index++;
}
}
void AppendArray(FStompBuffer& Out, uint8* In, SIZE_T Length, bool bShouldEscape)
{
if( bShouldEscape)
{
for (SIZE_T I=0; I<Length; I++)
{
if (In[I] == ':' || In[I] == '\\' || In[I] == '\n' || In[I] == '\r')
{
Out.Add('\\');
}
Out.Add(In[I]);
}
}
else
{
Out.Append(In, Length);
}
}
}
FStompFrame::FStompFrame(const FStompCommand& InCommand, const FStompHeader& InHeader, const FStompBuffer& InBody)
: Command(InCommand)
, Header(InHeader)
, Body(InBody)
{
if (Body.Num() > 0 && !Header.Contains(ContentLengthHeader))
{
Header.Add(ContentLengthHeader, FString::FromInt(Body.Num()));
}
}
FStompFrame::FStompFrame(const uint8* Data, SIZE_T Length)
: FStompFrame()
{
Decode(Data, Length);
}
void FStompFrame::Encode(FStompBuffer& Out) const
{
// A heartbeat is just a newline and can't contain any data nor is it terminated with a \0 byte
if (Command == HeartbeatCommand)
{
Out.Add('\n');
if(Header.Num() > 0)
{
UE_LOG(LogStomp, Warning, TEXT("Ignoring header fields for heartbeat frame."));
}
if(Body.Num() > 0)
{
UE_LOG(LogStomp, Warning, TEXT("Ignoring body for heartbeat frame."));
}
}
// Else output COMMAND\nHeaders\n\nBody\0
else
{
// According to the spec, the CONNECT command should not escape metacharacters for backwards compatibility.
bool bShouldEscapeFrameHeader = Command != ConnectCommand;
FString CommandString = Command.ToString();
CommandString.ToUpperInline();
FTCHARToUTF8 CommandEncoded(*CommandString);
AppendArray(Out, (uint8*)CommandEncoded.Get(), CommandEncoded.Length(), bShouldEscapeFrameHeader);
Out.Add('\n');
for (const TPair<FName, FString>& Element : Header)
{
FString ElementKeyString = Element.Key.ToString();
ElementKeyString.ToLowerInline();
FTCHARToUTF8 HeaderNameEncoded(*ElementKeyString);
FTCHARToUTF8 HeaderValueEncoded(*Element.Value);
AppendArray(Out, (uint8*)HeaderNameEncoded.Get(), HeaderNameEncoded.Length(), bShouldEscapeFrameHeader);
Out.Add(':');
AppendArray(Out, (uint8*)HeaderValueEncoded.Get(), HeaderValueEncoded.Length(), bShouldEscapeFrameHeader);
Out.Add('\n');
}
Out.Add('\n');
Out.Append(Body);
Out.Add('\0');
}
}
void FStompFrame::Decode(const uint8* In, SIZE_T Length)
{
// Ignore terminating 0 if present
if (Length > 0 && In[Length-1] == 0)
{
Length--;
}
FStompBuffer Buffer;
SIZE_T Index = 0;
// Trim off any initial newlines
SkipNewlines(In, Length, Index);
// Empty buffer after trimming newlines means this is a heartbeat packet
if (Index >= Length)
{
Command = HeartbeatCommand;
return;
}
// Read command
ReadValue(In, Length, Index, Buffer);
Command = UTF8_TO_TCHAR(Buffer.GetData());
if (Index >= Length)
{
UE_LOG(LogStomp, Warning, TEXT("Stomp command '%s' received without any headers"), *Command.ToString().ToUpper());
return;
}
while(Index < Length)
{
const uint8* Junk = In+Index;
Buffer.Empty();
uint8 Delimiter = ReadValue(In, Length, Index, Buffer, "\n:");
FName HeaderName = UTF8_TO_TCHAR(Buffer.GetData());
if (Delimiter == ':')
{
Buffer.Empty();
ReadValue(In, Length, Index, Buffer);
Header.Add(HeaderName, UTF8_TO_TCHAR(Buffer.GetData()));
}
else if (HeaderName == FName())
{
// Empty line marks the end of headers
break;
}
else
{
UE_LOG(LogStomp, Warning, TEXT("Encountered header line with no colons, '%s'."), *HeaderName.ToString())
Header.Add(HeaderName, TEXT(""));
}
}
// The remaining part, if any is the raw message body
if (Header.Contains(ContentLengthHeader))
{
SIZE_T ContentLength = FCString::Atoi(*Header[ContentLengthHeader]);
if (ContentLength > Length - Index)
{
ContentLength = Length - Index;
UE_LOG(LogStomp, Warning, TEXT("Warning truncating body. Content-length says %s but only %" SIZE_T_FMT " bytes remain"), *Header[ContentLengthHeader], ContentLength);
}
Body.Append(In+Index, ContentLength);
Index += ContentLength;
}
else
{
// When there is no content length header, we sould read body until the next zero byte in the stream
ReadValue(In, Length, Index, Body, "", false); // 0 byte is always a delimiter, don't allow using '\' as escape character
}
// Update content length header to match what was actually read from the frame
Header.Emplace(ContentLengthHeader, FString::FromInt(Body.Num()));
// Trim off any padding newlines
SkipNewlines(In, Length, Index);
// Check for junk data after end of body
if (Index < Length)
{
UE_LOG(LogStomp, Warning, TEXT("%" SIZE_T_FMT " bytes of junk data at end of frame. Was the content-length header missing or wrong?"), Length-Index);
}
}
#endif // #if WITH_STOMP