// ProtocolRecognizer.cpp
// Implements the cProtocolRecognizer class representing the meta-protocol that recognizes possibly multiple
// protocol versions and redirects everything to them
#include "Globals.h"
#include "ProtocolRecognizer.h"
#include "Protocol_1_8.h"
#include "Protocol_1_9.h"
#include "Protocol_1_10.h"
#include "Protocol_1_11.h"
#include "Protocol_1_12.h"
#include "Protocol_1_13.h"
#include "Protocol_1_14.h"
#include "../ClientHandle.h"
#include "../Root.h"
#include "../Server.h"
#include "../World.h"
#include "../JsonUtils.h"
#include "../Bindings/PluginManager.h"
struct TriedToJoinWithUnsupportedProtocolException : public std::runtime_error
{
explicit TriedToJoinWithUnsupportedProtocolException(const std::string & a_Message) :
std::runtime_error(a_Message)
{
}
};
cMultiVersionProtocol::cMultiVersionProtocol() :
m_Buffer(32 KiB),
m_WaitingForData(true)
{
}
AString cMultiVersionProtocol::GetVersionTextFromInt(cProtocol::Version a_ProtocolVersion)
{
switch (a_ProtocolVersion)
{
case cProtocol::Version::v1_8_0: return "1.8";
case cProtocol::Version::v1_9_0: return "1.9";
case cProtocol::Version::v1_9_1: return "1.9.1";
case cProtocol::Version::v1_9_2: return "1.9.2";
case cProtocol::Version::v1_9_4: return "1.9.4";
case cProtocol::Version::v1_10_0: return "1.10";
case cProtocol::Version::v1_11_0: return "1.11";
case cProtocol::Version::v1_11_1: return "1.11.1";
case cProtocol::Version::v1_12: return "1.12";
case cProtocol::Version::v1_12_1: return "1.12.1";
case cProtocol::Version::v1_12_2: return "1.12.2";
case cProtocol::Version::v1_13: return "1.13";
case cProtocol::Version::v1_13_1: return "1.13.1";
case cProtocol::Version::v1_13_2: return "1.13.2";
case cProtocol::Version::v1_14: return "1.14";
case cProtocol::Version::v1_14_1: return "1.14.1";
case cProtocol::Version::v1_14_2: return "1.14.2";
case cProtocol::Version::v1_14_3: return "1.14.3";
case cProtocol::Version::v1_14_4: return "1.14.4";
}
ASSERT(!"Unknown protocol version");
return fmt::format(FMT_STRING("Unknown protocol ({})"), a_ProtocolVersion);
}
void cMultiVersionProtocol::HandleIncomingDataInRecognitionStage(cClientHandle & a_Client, ContiguousByteBuffer & a_Data)
{
// NOTE: If a new protocol is added or an old one is removed, adjust MCS_CLIENT_VERSIONS and MCS_PROTOCOL_VERSIONS macros in the header file
/* Write all incoming data unmodified into m_Buffer.
Writing everything is always okay to do:
1. We can be sure protocol encryption hasn't started yet since m_Protocol hasn't been called, hence no decryption needs to take place
2. The extra data are processed at the end of this function */
if (!m_Buffer.Write(a_Data.data(), a_Data.size()))
{
a_Client.PacketBufferFull();
return;
}
if (TryHandleHTTPRequest(a_Client, a_Data))
{
return;
}
// TODO: recover from git history
// Unlengthed protocol, ...
// Lengthed protocol, try if it has the entire initial handshake packet:
if (
UInt32 PacketLen;
// If not enough bytes for the packet length, keep waiting
!m_Buffer.ReadVarInt(PacketLen) ||
// If not enough bytes for the packet, keep waiting
// (More of a sanity check to make sure no one tries anything funny, since ReadXXX can wait for data themselves)
!m_Buffer.CanReadBytes(PacketLen)
)
{
m_Buffer.ResetRead();
return;
}
/* Figure out the client's version.
1. m_Protocol != nullptr: the protocol is supported and we have a handler
2. m_Protocol == nullptr: the protocol is unsupported, handling is a special case done by ourselves
3. Exception: the data sent were garbage, the client handle deals with it by disconnecting */
m_Protocol = TryRecognizeLengthedProtocol(a_Client);
// Version recognised. Cause HandleIncomingData to stop calling us to handle data:
m_WaitingForData = false;
// Explicitly process any remaining data (already written to m_Buffer) with the new handler:
{
ContiguousByteBuffer Empty;
HandleIncomingData(a_Client, Empty);
}
}
void cMultiVersionProtocol::HandleIncomingDataInOldPingResponseStage(cClientHandle & a_Client, const ContiguousByteBufferView a_Data)
{
if (!m_Buffer.Write(a_Data.data(), a_Data.size()))
{
a_Client.PacketBufferFull();
return;
}
// Handle server list ping packets
for (;;)
{
UInt32 PacketLen;
UInt32 PacketID;
if (
!m_Buffer.ReadVarInt32(PacketLen) ||
!m_Buffer.CanReadBytes(PacketLen) ||
!m_Buffer.ReadVarInt32(PacketID)
)
{
// Not enough data
m_Buffer.ResetRead();
break;
}
if ((PacketID == 0x00) && (PacketLen == 1)) // Request packet
{
HandlePacketStatusRequest(a_Client);
}
else if ((PacketID == 0x01) && (PacketLen == 9)) // Ping packet
{
HandlePacketStatusPing(a_Client);
}
else
{
a_Client.PacketUnknown(PacketID);
return;
}
m_Buffer.CommitRead();
}
}
void cMultiVersionProtocol::HandleIncomingData(cClientHandle & a_Client, ContiguousByteBuffer & a_Data)
{
if (m_WaitingForData)
{
HandleIncomingDataInRecognitionStage(a_Client, a_Data);
}
else if (m_Protocol == nullptr)
{
// Got a Handshake for an unrecognised version, process future data accordingly:
HandleIncomingDataInOldPingResponseStage(a_Client, a_Data);
}
else
{
// The protocol recogniser succesfully identified a supported version, direct data to that protocol:
m_Protocol->DataReceived(m_Buffer, a_Data);
}
}
void cMultiVersionProtocol::HandleOutgoingData(ContiguousByteBuffer & a_Data)
{
// Normally only the protocol sends data, so outgoing data are only present when m_Protocol != nullptr.
// However, for unrecognised protocols we send data too, and that's when m_Protocol == nullptr. Check to avoid crashing (GH #5260).
if (m_Protocol != nullptr)
{
m_Protocol->DataPrepared(a_Data);
}
}
void cMultiVersionProtocol::SendDisconnect(cClientHandle & a_Client, const AString & a_Reason)
{
if (m_Protocol != nullptr)
{
m_Protocol->SendDisconnect(a_Reason);
return;
}
const AString Message = JsonUtils::SerializeSingleValueJsonObject("text", a_Reason);
const auto PacketID = GetPacketID(cProtocol::ePacketType::pktDisconnectDuringLogin);
cByteBuffer Out(
cByteBuffer::GetVarIntSize(PacketID) +
cByteBuffer::GetVarIntSize(static_cast<UInt32>(Message.size())) + Message.size()
);
VERIFY(Out.WriteVarInt32(PacketID));
VERIFY(Out.WriteVarUTF8String(Message));
SendPacket(a_Client, Out);
}
bool cMultiVersionProtocol::TryHandleHTTPRequest(cClientHandle & a_Client, ContiguousByteBuffer & a_Data)
{
const auto RedirectUrl = cRoot::Get()->GetServer()->GetCustomRedirectUrl();
if (RedirectUrl.empty())
{
return false;
}
ContiguousByteBuffer Buffer;
m_Buffer.ReadSome(Buffer, 10U);
m_Buffer.ResetRead();
// The request line, hastily decoded with the hope that it's encoded in US-ASCII.
const std::string_view Value(reinterpret_cast<const char *>(Buffer.data()), Buffer.size());
if (Value == u8"GET / HTTP")
{
const auto Response = fmt::format(u8"HTTP/1.0 303 See Other\r\nLocation: {}\r\n\r\n", cRoot::Get()->GetServer()->GetCustomRedirectUrl());
a_Client.SendData({ reinterpret_cast<const std::byte *>(Response.data()), Response.size() });
a_Client.Destroy();
return true;
}
return false;
}
std::unique_ptr<cProtocol> cMultiVersionProtocol::TryRecognizeLengthedProtocol(cClientHandle & a_Client)
{
UInt32 PacketType;
UInt32 ProtocolVersion;
AString ServerAddress;
UInt16 ServerPort;
UInt32 NextStateValue;
if (!m_Buffer.ReadVarInt(PacketType) || (PacketType != 0x00))
{
// Not an initial handshake packet, we don't know how to talk to them:
LOGD("Client \"%s\" uses an unsupported protocol (lengthed, initial packet %u)",
a_Client.GetIPString().c_str(), PacketType
);
throw TriedToJoinWithUnsupportedProtocolException("Your client isn't supported.\nTry connecting with Minecraft " MCS_CLIENT_VERSIONS);
}
if (
!m_Buffer.ReadVarInt(ProtocolVersion) ||
!m_Buffer.ReadVarUTF8String(ServerAddress) ||
!m_Buffer.ReadBEUInt16(ServerPort) ||
!m_Buffer.ReadVarInt(NextStateValue)
)
{
// TryRecognizeProtocol guarantees that we will have as much
// data to read as the client claims in the protocol length field:
throw TriedToJoinWithUnsupportedProtocolException("Incorrect amount of data received - hacked client?");
}
const auto NextState = [NextStateValue]
{
switch (NextStateValue)
{
case 1: return cProtocol::State::Status;
case 2: return cProtocol::State::Login;
case 3: return cProtocol::State::Game;
default: throw TriedToJoinWithUnsupportedProtocolException("Your client isn't supported.\nTry connecting with Minecraft " MCS_CLIENT_VERSIONS);
}
}();
// TODO: this should be a protocol property, not ClientHandle:
a_Client.SetProtocolVersion(ProtocolVersion);
// All good, eat up the data:
m_Buffer.CommitRead();
switch (ProtocolVersion)
{
case static_cast<UInt32>(cProtocol::Version::v1_8_0): return std::make_unique<cProtocol_1_8_0> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_9_0): return std::make_unique<cProtocol_1_9_0> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_9_1): return std::make_unique<cProtocol_1_9_1> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_9_2): return std::make_unique<cProtocol_1_9_2> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_9_4): return std::make_unique<cProtocol_1_9_4> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_10_0): return std::make_unique<cProtocol_1_10_0>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_11_0): return std::make_unique<cProtocol_1_11_0>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_11_1): return std::make_unique<cProtocol_1_11_1>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_12): return std::make_unique<cProtocol_1_12> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_12_1): return std::make_unique<cProtocol_1_12_1>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_12_2): return std::make_unique<cProtocol_1_12_2>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_13): return std::make_unique<cProtocol_1_13> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_13_1): return std::make_unique<cProtocol_1_13_1>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_13_2): return std::make_unique<cProtocol_1_13_2>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_14): return std::make_unique<cProtocol_1_14> (&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_14_1): return std::make_unique<cProtocol_1_14_1>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_14_2): return std::make_unique<cProtocol_1_14_2>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_14_3): return std::make_unique<cProtocol_1_14_3>(&a_Client, ServerAddress, NextState);
case static_cast<UInt32>(cProtocol::Version::v1_14_4): return std::make_unique<cProtocol_1_14_4>(&a_Client, ServerAddress, NextState);
default:
{
LOGD("Client \"%s\" uses an unsupported protocol (lengthed, version %u (0x%x))",
a_Client.GetIPString(), ProtocolVersion, ProtocolVersion
);
if (NextState != cProtocol::State::Status)
{
throw TriedToJoinWithUnsupportedProtocolException(
fmt::format(FMT_STRING("Unsupported protocol version {}.\nTry connecting with Minecraft {}"),
ProtocolVersion, MCS_CLIENT_VERSIONS)
);
}
// No cProtocol can handle the client:
return nullptr;
}
}
}
void cMultiVersionProtocol::SendPacket(cClientHandle & a_Client, cByteBuffer & a_OutPacketBuffer)
{
// Writes out the packet normally.
UInt32 PacketLen = static_cast<UInt32>(a_OutPacketBuffer.GetUsedSpace());
cByteBuffer OutPacketLenBuffer(cByteBuffer::GetVarIntSize(PacketLen));
// Compression doesn't apply to this state, send raw data:
VERIFY(OutPacketLenBuffer.WriteVarInt32(PacketLen));
ContiguousByteBuffer LengthData;
OutPacketLenBuffer.ReadAll(LengthData);
a_Client.SendData(LengthData);
// Send the packet's payload:
ContiguousByteBuffer PacketData;
a_OutPacketBuffer.ReadAll(PacketData);
a_OutPacketBuffer.CommitRead();
a_Client.SendData(PacketData);
}
UInt32 cMultiVersionProtocol::GetPacketID(cProtocol::ePacketType a_PacketType)
{
switch (a_PacketType)
{
case cProtocol::ePacketType::pktDisconnectDuringLogin: return 0x00;
case cProtocol::ePacketType::pktStatusResponse: return 0x00;
case cProtocol::ePacketType::pktPingResponse: return 0x01;
default:
{
ASSERT(!"GetPacketID() called for an unhandled packet");
return 0;
}
}
}
void cMultiVersionProtocol::HandlePacketStatusRequest(cClientHandle & a_Client)
{
cServer * Server = cRoot::Get()->GetServer();
AString ServerDescription = Server->GetDescription();
auto NumPlayers = static_cast<signed>(Server->GetNumPlayers());
auto MaxPlayers = static_cast<signed>(Server->GetMaxPlayers());
AString Favicon = Server->GetFaviconData();
cRoot::Get()->GetPluginManager()->CallHookServerPing(a_Client, ServerDescription, NumPlayers, MaxPlayers, Favicon);
// Version:
Json::Value Version;
Version["name"] = "Cuberite " MCS_CLIENT_VERSIONS;
Version["protocol"] = MCS_LATEST_PROTOCOL_VERSION;
// Players:
Json::Value Players;
Players["online"] = NumPlayers;
Players["max"] = MaxPlayers;
// TODO: Add "sample"
// Description:
Json::Value Description;
Description["text"] = ServerDescription.c_str();
// Create the response:
Json::Value ResponseValue;
ResponseValue["version"] = Version;
ResponseValue["players"] = Players;
ResponseValue["description"] = Description;
if (!Favicon.empty())
{
ResponseValue["favicon"] = "data:image/png;base64," + Favicon;
}
AString Response = JsonUtils::WriteFastString(ResponseValue);
// Send the response in a packet:
cByteBuffer out(Response.size() + 12); // String + 2x VarInt + extra space for safety
VERIFY(out.WriteVarInt32(GetPacketID(cProtocol::ePacketType::pktStatusResponse)));
VERIFY(out.WriteVarUTF8String(Response));
SendPacket(a_Client, out);
}
void cMultiVersionProtocol::HandlePacketStatusPing(cClientHandle & a_Client)
{
Int64 Timestamp;
if (!m_Buffer.ReadBEInt64(Timestamp))
{
return;
}
// Send the ping response packet:
cByteBuffer out(16); // VarInt + Int64 + extra space for safety
VERIFY(out.WriteVarInt32(GetPacketID(cProtocol::ePacketType::pktPingResponse)));
VERIFY(out.WriteBEInt64(Timestamp));
SendPacket(a_Client, out);
}