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

827 lines
27 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LwsWebSocket.h"
#if WITH_WEBSOCKETS && WITH_LIBWEBSOCKETS
#include "LwsWebSocketsManager.h"
#include "WebSocketsModule.h"
#include "WebSocketsLog.h"
#if WITH_SSL
#include "Ssl.h"
#endif
#include "Misc/EmbeddedCommunication.h"
#include "Misc/ScopeLock.h"
#include "Misc/ConfigCacheIni.h"
#include "HttpModule.h"
#include "HttpManager.h"
#include "PlatformHttp.h"
// FLwsSendBuffer
FLwsSendBuffer::FLwsSendBuffer(const uint8* Data, SIZE_T Size, bool bInIsBinary)
: bIsBinary(bInIsBinary)
, BytesWritten(0)
, bHasError(false)
{
check((LWS_PRE + Size) < INT32_MAX);
check(Data);
Payload.Reserve(LWS_PRE + Size);
Payload.AddDefaulted(LWS_PRE); // Reserve space for WS header data
Payload.Append(Data, Size);
}
int32 FLwsSendBuffer::GetPayloadSize() const
{
return Payload.Num() - (int32)(LWS_PRE);
}
// FLwsReceiveBufferBinary
FLwsReceiveBufferBinary::FLwsReceiveBufferBinary(const uint8* Data, const int32 Size, const int32 InBytesRemaining)
: BytesRemaining(InBytesRemaining)
{
check(Data);
check(Size >= 0);
check(InBytesRemaining >= 0);
Payload.Append(Data, Size);
}
// FLwsReceiveBufferBinaryFragment
FLwsReceiveBufferBinaryFragment::FLwsReceiveBufferBinaryFragment(const uint8* Data, const int32 Size, const bool bInIsLastFragment)
: bIsLastFragment(bInIsLastFragment)
{
check(Data);
check(Size >= 0);
Payload.Append(Data, Size);
}
// FLwsReceiveBufferText
FLwsReceiveBufferText::FLwsReceiveBufferText(FString&& InText)
: Text(MoveTemp(InText))
{
}
// FLwsWebSocket
int32 FLwsWebSocket::IncrementingIdentifier = 0;
const TCHAR* FLwsWebSocket::ToString(const EState InState)
{
#define LWSWEBSOCKET_ESTATE_TOSTRING(InName) case EState::InName: return TEXT(#InName);
switch (InState)
{
LWSWEBSOCKET_ESTATE_TOSTRING(None);
LWSWEBSOCKET_ESTATE_TOSTRING(StartConnecting);
LWSWEBSOCKET_ESTATE_TOSTRING(Connecting);
LWSWEBSOCKET_ESTATE_TOSTRING(Connected);
LWSWEBSOCKET_ESTATE_TOSTRING(ClosingByRequest);
LWSWEBSOCKET_ESTATE_TOSTRING(Closed);
LWSWEBSOCKET_ESTATE_TOSTRING(Error);
}
#undef LWSWEBSOCKET_ESTATE_TOSTRING
return TEXT("Unknown");
}
FLwsWebSocket::FLwsWebSocket(FPrivateToken, const FString& InUrl, const TArray<FString>& InProtocols, const FString& InUpgradeHeader, uint64 TextMessageMemoryLimit)
: State(EState::None)
, LastGameThreadState(EState::None)
, bWasSendQueueEmpty(true)
, LwsConnection(nullptr)
, Url(InUrl)
, Protocols(InProtocols)
, UpgradeHeader(InUpgradeHeader)
, MaxTextMessageBufferSize(TextMessageMemoryLimit)
, Identifier(++IncrementingIdentifier)
{
UE_LOG(LogWebSockets, VeryVerbose, TEXT("FLwsWebSocket[%d]: Constructed url=%s protocols=%s"), Identifier, *InUrl, *FString::Join(Protocols, TEXT(",")));
}
FLwsWebSocket::~FLwsWebSocket()
{
UE_LOG(LogWebSockets, VeryVerbose, TEXT("FLwsWebSocket[%d]: Destroyed"), Identifier);
checkf(LwsConnection == nullptr, TEXT("FLwsWebSocket: Must have closed connection before destruction"));
ClearData();
}
void FLwsWebSocket::Connect()
{
if (LastGameThreadState != EState::None)
{
UE_LOG(LogWebSockets, Warning, TEXT("FLwsWebSocket[%d]::Connect: State is not None (%s), unable to start connecting!"), Identifier, ToString(LastGameThreadState));
return;
}
if (!FLwsWebSocketsManager::Get().bDisableDomainAllowlist)
{
FHttpManager& HttpManager = FHttpModule::Get().GetHttpManager();
if (!HttpManager.IsDomainAllowed(Url))
{
State = EState::Error;
UE_LOG(LogWebSockets, Warning, TEXT("FLwsWebSocket[%d]::Connect: %s is not allowlisted. Refusing to connect."), Identifier, *Url);
OnConnectionError().Broadcast(TEXT("Invalid Domain"));
return;
}
}
else
{
UE_LOG(LogWebSockets, Log, TEXT("FLwsWebSocket[%d]::Connect: Domain allowlisting has been disabled by config."), Identifier);
}
// No lock, we are not being processed on the websockets thread yet
State = EState::StartConnecting;
LastGameThreadState = State; // This is called on the game thread
bWantsMessageEvents = OnMessage().IsBound();
bWantsRawMessageEvents = OnRawMessage().IsBound();
bWantsBinaryMessageEvents = OnBinaryMessage().IsBound();
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::Connect: setting State=%s url=%s bWantsMessageEvents=%d bWantsRawMessageEvents=%d"), Identifier, ToString(State), *Url, (int32)bWantsMessageEvents, (int32)bWantsRawMessageEvents);
// Clear up any data from previous runs
ClearData();
FLwsWebSocketsManager& WebSocketsManager = FLwsWebSocketsManager::Get();
WebSocketsManager.StartProcessingWebSocket(this);
}
void FLwsWebSocket::Close(int32 Code, const FString& Reason)
{
if (CloseRequest.Code != 0)
{
UE_LOG(LogWebSockets, Warning, TEXT("FLwsWebSocket[%d]::Close: Already closing, ignoring subsequent attempt"), Identifier);
return;
}
// We are doing this conversion here so we don't have to do it on the ws thread
FTCHARToUTF8 Convert(*Reason);
ANSICHAR* ANSIReason = static_cast<ANSICHAR*>(FMemory::Malloc(Convert.Length() + 1));
FCStringAnsi::Strncpy(ANSIReason, (const ANSICHAR*)Convert.Get(), Convert.Length() + 1);
ANSIReason[Convert.Length()] = 0;
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::Close: Close queued with code=%d reason=%s"), Identifier, Code, *Reason);
{
UE::TScopeLock ScopeLock(StateLock);
CloseRequest.Code = Code;
CloseRequest.Reason = ANSIReason;
}
}
void FLwsWebSocket::Send(const void* Data, SIZE_T Size, bool bIsBinary)
{
SendQueue.Enqueue(new FLwsSendBuffer(static_cast<const uint8*>(Data), Size, bIsBinary));
FLwsWebSocketsManager& WebSocketsManager = FLwsWebSocketsManager::Get();
WebSocketsManager.WakeService();
}
void FLwsWebSocket::Send(const FString& Data)
{
FTCHARToUTF8 Converted(*Data);
Send((uint8*)Converted.Get(), Converted.Length(), false);
OnMessageSent().Broadcast(Data);
}
void FLwsWebSocket::SetTextMessageMemoryLimit(uint64 TextMessageMemoryLimit)
{
MaxTextMessageBufferSize = TextMessageMemoryLimit;
}
void FLwsWebSocket::SendFromQueue()
{
check(LwsConnection);
FLwsSendBuffer* CurrentBuffer;
// libwebsockets-3.0 only allows us to send once per LWS_CALLBACK_*_WRITABLE event
if (SendQueue.Peek(CurrentBuffer))
{
int32 LastBytesWritten = CurrentBuffer->BytesWritten;
const bool bWriteSuccessful = WriteBuffer(*CurrentBuffer);
const bool bIsDone = CurrentBuffer->IsDone();
if (!bWriteSuccessful)
{
// @TODO: bubble up error
UE_LOG(LogWebSockets, Warning, TEXT("FLwsWebSocket[%d]::SendFromQueue: Error writing buffer Size=%d BytesWritten=%d bIsBinary=%d"),
Identifier, CurrentBuffer->GetPayloadSize(), CurrentBuffer->BytesWritten, (int32)CurrentBuffer->bIsBinary);
}
else if (LastBytesWritten != CurrentBuffer->BytesWritten)
{
UE_LOG(LogWebSockets, VeryVerbose, TEXT("FLwsWebSocket[%d]::SendFromQueue: Wrote %d bytes, %d bytes remaining in this packet"),
Identifier, (CurrentBuffer->BytesWritten - LastBytesWritten), (CurrentBuffer->GetPayloadSize() - CurrentBuffer->BytesWritten));
}
const bool bFinishedSending = bIsDone || !bWriteSuccessful;
if (bFinishedSending)
{
SendQueue.Dequeue(CurrentBuffer);
delete CurrentBuffer;
}
}
// If we still have data to send, ask for a notification when ready to send more
bWasSendQueueEmpty = SendQueue.IsEmpty();
if (!bWasSendQueueEmpty)
{
lws_callback_on_writable(LwsConnection);
}
}
void FLwsWebSocket::ClearData()
{
check(State != EState::Connected);
ReceiveBinaryQueue.Empty();
ReceiveBinaryFragmentQueue.Empty();
ReceiveTextQueue.Empty();
FLwsSendBuffer* SendBuffer;
while (SendQueue.Dequeue(SendBuffer))
{
delete SendBuffer;
}
ReceiveBuffer.Empty(0); // Also clear temporary receive buffer
if (CloseRequest.Reason)
{
FMemory::Free(CloseRequest.Reason);
}
CloseRequest.Code = 0;
CloseRequest.Reason = nullptr;
}
bool FLwsWebSocket::WriteBuffer(FLwsSendBuffer& Buffer)
{
enum lws_write_protocol WriteProtocol;
if (Buffer.BytesWritten > 0)
{
WriteProtocol = LWS_WRITE_CONTINUATION;
}
else if (Buffer.bIsBinary)
{
WriteProtocol = LWS_WRITE_BINARY;
}
else
{
WriteProtocol = LWS_WRITE_TEXT;
}
// Payload is modified in the call to lws_write, for libwebsockets to be able to use already allocated memory instead of needing to allocate more
uint8* Payload = Buffer.Payload.GetData();
const int32 PayloadSize = Buffer.Payload.Num();
const int32 Offset = LWS_PRE + Buffer.BytesWritten;
const int32 CurrentBytesWritten = lws_write(LwsConnection, Payload + Offset, PayloadSize - Offset, WriteProtocol);
if (CurrentBytesWritten > 0)
{
Buffer.BytesWritten += CurrentBytesWritten;
}
// lws_write returns -1 on error
return CurrentBytesWritten >= 0;
}
int FLwsWebSocket::LwsCallback(lws* Instance, lws_callback_reasons Reason, void* Data, size_t Length)
{
switch (Reason)
{
case LWS_CALLBACK_CLIENT_ESTABLISHED:
{
// No lock because we are the only thread that writes to State
EState PreviousState = State;
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Connected;
}
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_CLIENT_ESTABLISHED, setting State=%s PreviousState=%s"), Identifier, ToString(State), ToString(PreviousState));
LwsConnection = Instance;
bWasSendQueueEmpty = SendQueue.IsEmpty();
if (!bWasSendQueueEmpty)
{
lws_callback_on_writable(LwsConnection);
}
break;
}
case LWS_CALLBACK_CLIENT_RECEIVE:
{
const SIZE_T BytesLeft = lws_remaining_packet_payload(Instance);
bool bIsFinalFragment = (bool)lws_is_final_fragment(Instance);
bool bIsBinary = (bool)lws_frame_is_binary(Instance);
UE_LOG(LogWebSockets, VeryVerbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_CLIENT_RECEIVE Length=%" SIZE_T_FMT " BytesLeftInFragment=%" SIZE_T_FMT " IsFinalFragment=%s"), Identifier, Length, BytesLeft, bIsFinalFragment ? TEXT("true") : TEXT("false"));
bool bWakeGameThread = false;
if (bWantsMessageEvents && !bIsBinary)
{
FUTF8ToTCHAR Convert((const ANSICHAR*)Data, Length);
if (ReceiveBuffer.Len() + Length > MaxTextMessageBufferSize)
{
FString ReasonString = FString::Printf(TEXT("Received text message exceeded memory limit of %" UINT64_FMT " bytes"), MaxTextMessageBufferSize);
Close(LWS_CLOSE_STATUS_MESSAGE_TOO_LARGE, ReasonString);
UE_LOG(LogWebSockets, Verbose, TEXT("Received text message too large - use SetTextMessageMemoryLimit() to increase buffer size. Current Size=%lu"), MaxTextMessageBufferSize);
}
else
{
ReceiveBuffer.Append(Convert.Get(), Convert.Length());
if (BytesLeft == 0 && bIsFinalFragment)
{
bWakeGameThread = true;
ReceiveTextQueue.Enqueue(MakeUnique<FLwsReceiveBufferText>(MoveTemp(ReceiveBuffer)));
ReceiveBuffer.Empty();
}
}
}
if (bWantsBinaryMessageEvents && bIsBinary)
{
bWakeGameThread = true;
bool bIsLast = (bool)bIsFinalFragment && BytesLeft == 0;
ReceiveBinaryFragmentQueue.Enqueue(MakeUnique<FLwsReceiveBufferBinaryFragment>(static_cast<const uint8*>(Data), Length, bIsLast));
}
if (bWantsRawMessageEvents)
{
bWakeGameThread = true;
ReceiveBinaryQueue.Enqueue(MakeUnique<FLwsReceiveBufferBinary>(static_cast<const uint8*>(Data), Length, BytesLeft));
}
if (bWakeGameThread)
{
FEmbeddedCommunication::WakeGameThread();
}
break;
}
case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE:
{
uint16 CloseStatus = *(static_cast<const uint16*>(Data));
#if PLATFORM_LITTLE_ENDIAN
// The status is the first two bytes of the message in network byte order
CloseStatus = BYTESWAP_ORDER16(CloseStatus);
#endif
FString CloseReasonString;
if (Length > sizeof(uint16))
{
auto Convert = StringCast<TCHAR>((const ANSICHAR*)Data + sizeof(uint16), Length - sizeof(uint16));
CloseReasonString.AppendChars(Convert.Get(), Convert.Length());
}
// We only modify our state if we are Connected or ClosingByRequest (effectively connected)
if (State == EState::Connected ||
State == EState::ClosingByRequest)
{
LwsConnection = nullptr;
const bool bPeerSpecifiedReason = !CloseReasonString.IsEmpty();
if (!bPeerSpecifiedReason)
{
CloseReasonString = TEXT("Peer did not specify a reason for initiating the closing");
}
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_WS_PEER_INITIATED_CLOSE, setting State=%s CloseStatus=%d Reason=%s bPeerSpecifiedReason=%d PreviousState=%s"),
Identifier, ToString(EState::Closed), (int32)CloseStatus, *CloseReasonString, (int32)bPeerSpecifiedReason, ToString(State));
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Closed;
ClosedReason.Reason = MoveTemp(CloseReasonString);
ClosedReason.CloseStatus = CloseStatus;
ClosedReason.bWasClean = true;
}
FEmbeddedCommunication::WakeGameThread();
}
else
{
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_WS_PEER_INITIATED_CLOSE, but ignoring because our State=%s CloseStatus=%d Reason=%s"),
Identifier, ToString(EState::Closed), (int32)CloseStatus, *CloseReasonString);
}
// TODO: Confirm what we want to return here. non-zero will close the socket immediately, zero will cause libwebsockets to mirror the close packet back to the server and wait for a response
return 1; // Close the connection without waiting for a response
}
case LWS_CALLBACK_WSI_DESTROY:
// Getting a WSI_DESTROY before a connection has been established and no errors reported usually means there was a timeout establishing a connection
if (State == EState::Connecting)
{
FString CloseReasonString(TEXT("Connection timed out"));
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_WSI_DESTROY, setting State=%s PreviousState=%s"), Identifier, ToString(EState::Error), ToString(State));
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Error;
ClosedReason.Reason = MoveTemp(CloseReasonString);
}
FEmbeddedCommunication::WakeGameThread();
}
else
{
if (State == EState::Connected)
{
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Closed;
ClosedReason.bWasClean = false;
ClosedReason.CloseStatus = LWS_CLOSE_STATUS_ABNORMAL_CLOSE;
}
FEmbeddedCommunication::WakeGameThread();
}
else if (State == EState::ClosingByRequest)
{
FString ClosedReasonString;
int32 ClosedStatus;
if (CloseRequest.Code == LWS_CLOSE_STATUS_MESSAGE_TOO_LARGE)
{
ClosedReasonString = TEXT("Received text message exceeded memory limit");
ClosedStatus = LWS_CLOSE_STATUS_MESSAGE_TOO_LARGE;
}
else
{
ClosedReasonString = TEXT("Successfully closed connection to our peer");
ClosedStatus = LWS_CLOSE_STATUS_NORMAL;
}
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Closed;
ClosedReason.Reason = MoveTemp(ClosedReasonString);
ClosedReason.CloseStatus = ClosedStatus;
ClosedReason.bWasClean = true;
}
FEmbeddedCommunication::WakeGameThread();
}
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_WSI_DESTROY, State=%s"), Identifier, ToString(State));
}
LwsConnection = nullptr;
break;
case LWS_CALLBACK_CLOSED:
{
// We only modify our state if we are Connected or ClosingByRequest (effectively connected)
if (State == EState::Connected ||
State == EState::ClosingByRequest)
{
const bool bClosingByRequest = (State == EState::ClosingByRequest);
LwsConnection = nullptr;
static const FString LocalInitiatedReason = TEXT("Successfully closed connection to our peer");
static const FString PeerInitiatedReason = TEXT("Connection closed by peer");
FString CloseReasonString = bClosingByRequest ? LocalInitiatedReason : PeerInitiatedReason;
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_CLOSED, setting State=%s CloseReason=%s PreviousState=%s"),
Identifier, ToString(EState::Closed), *CloseReasonString, ToString(State));
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Closed;
ClosedReason.Reason = MoveTemp(CloseReasonString);
ClosedReason.CloseStatus = LWS_CLOSE_STATUS_NORMAL;
ClosedReason.bWasClean = bClosingByRequest;
}
FEmbeddedCommunication::WakeGameThread();
}
else
{
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_CLOSED, ignoring and waiting for LWS_CALLBACK_WSI_DESTROY, State=%s"),
Identifier, ToString(EState::Closed));
}
break;
}
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
{
LwsConnection = nullptr;
FUTF8ToTCHAR Convert((const ANSICHAR*)Data, Length);
FString CloseReasonString = FString::ConstructFromPtrSize(Convert.Get(), Convert.Length());
bool bWakeGameThread = false;
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_CLIENT_CONNECTION_ERROR, setting State=%s CloseReason=%s PreviousState=%s"),
Identifier, ToString(EState::Error), *CloseReasonString, ToString(State));
{
UE::TScopeLock ScopeLock(StateLock);
if (State == EState::Connected ||
State == EState::ClosingByRequest)
{
State = EState::Closed;
ClosedReason.bWasClean = false;
ClosedReason.CloseStatus = LWS_CLOSE_STATUS_ABNORMAL_CLOSE;
bWakeGameThread = true;
}
else if (State != EState::Closed)
{
State = EState::Error;
bWakeGameThread = true;
}
ClosedReason.Reason = MoveTemp(CloseReasonString);
}
if (bWakeGameThread)
{
FEmbeddedCommunication::WakeGameThread();
}
return -1;
}
case LWS_CALLBACK_RECEIVE_PONG:
break;
case LWS_CALLBACK_CLIENT_WRITEABLE:
case LWS_CALLBACK_SERVER_WRITEABLE:
{
if (State == EState::ClosingByRequest)
{
LwsConnection = nullptr;
int32 CloseCodeCopy;
ANSICHAR* CloseReasonCopy;
{
// TODO: Is this scope lock necessary? We only got here because we recognized CloseRequest.Code was valid, and we do not allow it to be modified afer being set on the game thread, so shouldn't this thread have the correct value?
UE::TScopeLock ScopeLock(StateLock);
CloseCodeCopy = CloseRequest.Code;
CloseReasonCopy = CloseRequest.Reason;
}
// This only sets the reason for closing the connection
lws_close_reason(Instance, (enum lws_close_status)CloseCodeCopy, (unsigned char *)CloseReasonCopy, (SIZE_T)FCStringAnsi::Strlen(CloseReasonCopy));
return -1; // Returning non-zero will close the current connection
}
else if (State == EState::Connected)
{
SendFromQueue();
}
break;
}
case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
{
if (!UpgradeHeader.IsEmpty())
{
char** WriteableString = reinterpret_cast<char**>(Data);
auto UpgradeHeaderAnsi = StringCast<ANSICHAR>(*UpgradeHeader);
FCStringAnsi::Strncpy(*WriteableString, UpgradeHeaderAnsi.Get(), Length);
*WriteableString += UpgradeHeaderAnsi.Length();
}
break;
}
#if WITH_SSL
case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
{
// in FLwsWebSocketsManager::CallbackWrapper, we copied UserData to Data, so Data is the X509_STORE_CTX* instead of the SSL*
X509_STORE_CTX* Context = static_cast<X509_STORE_CTX*>(Data);
int PreverifyOk = 1;
if (!FLwsWebSocketsManager::Get().bDisableCertValidation)
{
PreverifyOk = Length;
if (PreverifyOk == 1)
{
const FString Domain = FGenericPlatformHttp::GetUrlDomain(Url);
if (!FSslModule::Get().GetCertificateManager().VerifySslCertificates(Context, Domain))
{
PreverifyOk = 0;
}
}
}
else
{
X509_STORE_CTX_set_error(Context, X509_V_OK);
}
return PreverifyOk == 1 ? 0 : 1;
}
#endif
default:
break;
}
return 0;
}
void FLwsWebSocket::GameThreadTick()
{
EState CurrentState;
{
UE::TScopeLock ScopeLock(StateLock);
CurrentState = State;
}
if (CurrentState != LastGameThreadState)
{
LastGameThreadState = CurrentState;
// State changed, broadcast events
if (CurrentState == EState::Connected)
{
OnConnected().Broadcast();
}
}
// If we requested a close then we don't care about any messages we receive
// No lock, only this thread will modify CloseRequest
if (CloseRequest.Code == 0)
{
FLwsReceiveBufferTextPtr BufferText;
while (ReceiveTextQueue.Dequeue(BufferText))
{
OnMessage().Broadcast(BufferText->Text);
}
FLwsReceiveBufferBinaryPtr BufferBinary;
while (ReceiveBinaryQueue.Dequeue(BufferBinary))
{
OnRawMessage().Broadcast(BufferBinary->Payload.GetData(), BufferBinary->Payload.Num(), BufferBinary->BytesRemaining);
}
FLwsReceiveBufferBinaryFragmentPtr BufferBinaryFragment;
while (ReceiveBinaryFragmentQueue.Dequeue(BufferBinaryFragment))
{
OnBinaryMessage().Broadcast(BufferBinaryFragment->Payload.GetData(), BufferBinaryFragment->Payload.Num(), BufferBinaryFragment->bIsLastFragment);
}
}
}
void FLwsWebSocket::GameThreadFinalize()
{
EState PreviousState;
FClosedReason LastClosedReason;
{
// TODO: The contract requires the libwebsockets be done with this object prior to this being called. Is there a better way to ensure we get the last set value by the libwebsockets thread? FPlatformMisc::MemoryBarrier maybe?
UE::TScopeLock ScopeLock(StateLock);
PreviousState = State;
State = EState::None; // Will be re-usable on final delegate triggering
LastGameThreadState = State;
LastClosedReason = MoveTemp(ClosedReason);
}
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::GameThreadFinalize: setting State=%s PreviousState=%s"),
Identifier, ToString(EState::None), ToString(PreviousState));
const bool bWasError = (PreviousState == EState::Error);
if (bWasError)
{
OnConnectionError().Broadcast(LastClosedReason.Reason);
}
else
{
// TODO: Do we need to ensure that an OnConnected() was triggered before OnClosed()?
OnClosed().Broadcast(LastClosedReason.CloseStatus, LastClosedReason.Reason, LastClosedReason.bWasClean);
}
}
bool FLwsWebSocket::LwsThreadInitialize(struct lws_context &LwsContext)
{
check(State == EState::StartConnecting);
ConnectInternal(LwsContext);
return State == EState::Connecting;
}
void FLwsWebSocket::LwsThreadTick()
{
if (State == EState::Connected)
{
check(LwsConnection);
{
// Check if we want to close
UE::TScopeLock ScopeLock(StateLock);
if (CloseRequest.Code != 0)
{
State = EState::ClosingByRequest;
}
}
if (State == EState::ClosingByRequest)
{
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::LwsThreadTick: Close requested while connected, setting State=%s PreviousState=%s"),
Identifier, ToString(EState::ClosingByRequest), ToString(EState::Connected));
if (bWasSendQueueEmpty)
{
lws_callback_on_writable(LwsConnection);
}
}
else
{
// When connected, request callback on writeable when we go from not having data to having data
bool bQueueIsEmpty = SendQueue.IsEmpty();
if (bWasSendQueueEmpty && !bQueueIsEmpty)
{
lws_callback_on_writable(LwsConnection);
}
bWasSendQueueEmpty = bQueueIsEmpty;
}
}
}
void FLwsWebSocket::ConnectInternal(struct lws_context &LwsContext)
{
check(!LwsConnection);
checkf(State == EState::StartConnecting, TEXT("FLwsWebSocket::ConnectInternal: State must be %s, but is %s instead"), ToString(EState::StartConnecting), ToString(State));
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::ConnectInternal: setting State=%s PreviousState=%s"),
Identifier, ToString(EState::Connecting), ToString(EState::StartConnecting));
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Connecting;
}
FTCHARToUTF8 UrlUtf8(*Url);
const char *UrlProtocol;
const char *TmpUrlPath;
const char* ParsedAddress;
int ParsedPort;
if (lws_parse_uri((char*)UrlUtf8.Get(), &UrlProtocol, &ParsedAddress, &ParsedPort, &TmpUrlPath))
{
FString Reason(TEXT("Bad URL"));
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::ConnectInternal: setting State=%s PreviousState=%s Reason=%s"),
Identifier, ToString(EState::Error), ToString(EState::Connecting), *Reason);
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Error;
ClosedReason.Reason = MoveTemp(Reason);
}
return;
}
int32 UrlPathLength = FCStringAnsi::Strlen(TmpUrlPath) + 2;
TArray<ANSICHAR> UrlPath;
UrlPath.Reserve(UrlPathLength);
UrlPath.Add('/');
UrlPath.Append(TmpUrlPath, UrlPathLength - 2);
UrlPath.Add('\0');
int SslConnection = 0;
// Use SSL and require a valid cerver cert
if (FCStringAnsi::Stricmp(UrlProtocol, "wss") == 0)
{
SslConnection = 1; // LCCSCF_USE_SSL
// Use ssl, and allow self-signed certs with bDisableCertValidation.
if (FLwsWebSocketsManager::Get().bDisableCertValidation)
{
SslConnection = 3; // LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED;
}
}
// Use SSL, and allow self-signed certs
else if (FCStringAnsi::Stricmp(UrlProtocol, "wss+insecure") == 0)
{
SslConnection = 3; // LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED;
}
// No encryption
else if (FCStringAnsi::Stricmp(UrlProtocol, "ws") == 0)
{
SslConnection = 0;
}
// Else return an error
else
{
FString Reason(FString::Printf(TEXT("Bad protocol '%s'. Use either 'ws', 'wss', or 'wss+insecure'"), UTF8_TO_TCHAR(UrlProtocol)));
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::ConnectInternal: setting State=%s PreviousState=%s Reason=%s"),
Identifier, ToString(EState::Error), ToString(EState::Connecting), *Reason);
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Error;
ClosedReason.Reason = MoveTemp(Reason);
}
return;
}
FString ParsedPortString = FString::FromInt(ParsedPort);
FTCHARToUTF8 ParsedPortStringUTF8(*ParsedPortString);
// Length is based on format [addr]:[port]\0
int32 ParsedAddressLength = FCStringAnsi::Strlen(ParsedAddress);
bool bUsingDefaultPort = (SslConnection == 0 && ParsedPort == 80) || (SslConnection != 0 && ParsedPort == 443);
int32 HostLength = bUsingDefaultPort ? (ParsedAddressLength + 1) : (ParsedAddressLength + 1 + ParsedPortStringUTF8.Length() + 1);
TArray<ANSICHAR> Host;
Host.Reserve(HostLength);
Host.Append(ParsedAddress, ParsedAddressLength);
if (!bUsingDefaultPort)
{
Host.Add(':');
Host.Append(ParsedPortStringUTF8.Get(), ParsedPortStringUTF8.Length());
}
Host.Add('\0');
// If we specify protocols, convert the comma separated list into UTF8
TOptional<FTCHARToUTF8> OptionalCombinedProtocolsUTF8;
if (Protocols.Num() > 0)
{
const FString CombinedProtocols(FString::Join(Protocols, TEXT(",")));
OptionalCombinedProtocolsUTF8.Emplace(*CombinedProtocols);
}
const char* const CombinedProtocolsUTF8 = OptionalCombinedProtocolsUTF8.IsSet() ? (const char*)OptionalCombinedProtocolsUTF8.GetValue().Get() : nullptr;
struct lws_client_connect_info ConnectInfo = {};
ConnectInfo.context = &LwsContext;
ConnectInfo.address = ParsedAddress;
ConnectInfo.port = ParsedPort;
ConnectInfo.ssl_connection = SslConnection;
ConnectInfo.path = UrlPath.GetData();
ConnectInfo.host = Host.GetData();
ConnectInfo.origin = ConnectInfo.address;
ConnectInfo.protocol = CombinedProtocolsUTF8;
ConnectInfo.ietf_version_or_minus_one = -1;
ConnectInfo.userdata = this;
if (lws_client_connect_via_info(&ConnectInfo))
{
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::ConnectInternal: lws_client_connect_via_info succeeded"), Identifier);
}
else
{
FString Reason(TEXT("Could not initialize connection"));
UE_LOG(LogWebSockets, Verbose, TEXT("FLwsWebSocket[%d]::ConnectInternal: setting State=%s PreviousState=%s Reason=%s"),
Identifier, ToString(EState::Error), ToString(EState::Connecting), *Reason);
{
UE::TScopeLock ScopeLock(StateLock);
State = EState::Error;
ClosedReason.Reason = MoveTemp(Reason);
}
}
}
#endif // #if WITH_WEBSOCKETS