Skip to content
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

Net compression #205

Merged
merged 9 commits into from
Sep 17, 2023
Merged
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
2 changes: 2 additions & 0 deletions cubic-server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ target_sources (${CMAKE_PROJECT_NAME} PRIVATE
RSAEncryptionHandler.hpp
Checksum.cpp
Checksum.hpp
CompressionUtils.cpp
CompressionUtils.hpp
)

file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/plugins)
Expand Down
79 changes: 73 additions & 6 deletions cubic-server/Client.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <boost/archive/binary_iarchive.hpp>
#include <boost/iostreams/filtering_stream.hpp>
#include <boost/system/detail/error_category.hpp>
#include <boost/system/detail/error_code.hpp>
#include <cstdint>
Expand All @@ -12,12 +14,14 @@
#include <sys/types.h>
#include <thread>
#include <unistd.h>
#include <zlib.h>

#include "Client.hpp"
#include "PlayerAttributes.hpp"
#include "nbt.hpp"

#include "Checksum.hpp"
#include "CompressionUtils.hpp"
#include "Dimension.hpp"
#include "Player.hpp"
#include "Server.hpp"
Expand All @@ -28,6 +32,7 @@
#include "nlohmann/json.hpp"
#include "protocol/ClientPackets.hpp"
#include "protocol/ServerPackets.hpp"
#include "protocol/serialization/addPrimaryType.hpp"
#include "protocol/serialization/popPrimaryType.hpp"
#include "types.hpp"

Expand All @@ -41,7 +46,8 @@ Client::Client(tcp::socket &&socket, size_t clientID):
_player(nullptr),
_socket(std::move(socket)),
_clientID(clientID),
_isEncrypted(false)
_isEncrypted(false),
_isCompressed(false)
{
LDEBUG("Creating client");
}
Expand Down Expand Up @@ -90,8 +96,38 @@ void Client::doRead()
// Server::getInstance()->triggerClientCleanup(_clientID);
}

static void compressPacket(std::vector<uint8_t> &in, std::vector<uint8_t> &out)
{
// Don't compress if the packet is too small
if (in.size() < (size_t) CONFIG["compression-threshold"].as<int32_t>()) {
uint8_t *at = in.data();
int32_t size = protocol::popVarInt(at, in.data() + in.size() - 1);
protocol::addVarInt(out, size + 1);
protocol::addVarInt(out, 0);
out.insert(out.end(), at, in.data() + in.size());
return;
}
uint8_t *at = in.data();
int32_t size = protocol::popVarInt(at, in.data() + in.size() - 1);
std::vector<uint8_t> compressedData;
int compressReturn = compressVector(std::vector<uint8_t>(at, in.data() + in.size()), compressedData);
if (compressReturn != Z_OK)
abort(); // If we get here, we have a big problem
protocol::addVarInt(out, compressedData.size() + (uint64_t) (at - in.data()));
protocol::addVarInt(out, size);
out.insert(out.end(), compressedData.begin(), compressedData.end());
}

void Client::doWrite(std::unique_ptr<std::vector<uint8_t>> &&data)
{
if (_isCompressed) {
auto toSend = std::make_unique<std::vector<uint8_t>>();
compressPacket(*data, *toSend);
if (_isEncrypted)
_encryption.encrypt(*toSend);
Server::getInstance()->sendData(_clientID, std::move(toSend));
return;
}
if (_isEncrypted)
_encryption.encrypt(*data);
Server::getInstance()->sendData(_clientID, std::move(data));
Expand Down Expand Up @@ -125,7 +161,7 @@ void Client::handleParsedClientPacket(std::unique_ptr<protocol::BaseServerPacket
case ClientStatus::Status:
switch (packetID) {
case ServerPacketsID::StatusRequest:
PCK_CALLBACK(StatusRequest);
PCK_CALLBACK_EMPTY(StatusRequest);
case ServerPacketsID::PingRequest:
PCK_CALLBACK(PingRequest);
default:
Expand Down Expand Up @@ -212,7 +248,7 @@ void Client::_handlePacket()
if (bufferLength == 0)
break;
uint8_t *at = data.data();
uint8_t *eof = at + bufferLength;
uint8_t *eof = at + bufferLength - 1;
int32_t length = 0;
try {
length = protocol::popVarInt(at, eof);
Expand All @@ -225,7 +261,29 @@ void Client::_handlePacket()
} catch (const protocol::PacketEOF &_) {
break; // Not enough data in buffer to parse the length of the packet
}

const uint8_t *startPayload = at;

std::vector<uint8_t> uncompressedData;
if (_isCompressed) {
int32_t uncompressedLength = protocol::popVarInt(at, eof);
if (uncompressedLength == 0)
goto packetNotCompressed;
if (decompressVector(std::vector<uint8_t>(at, eof + 1), uncompressedData, uncompressedLength)) {
LERROR("Failed to decompress client packet!");
this->disconnect("Badly formed compressed packet!");
return;
}
if ((size_t) uncompressedLength != uncompressedData.size()) {
LERROR("{} != {}", uncompressedLength, uncompressedData.size());
this->disconnect("Bad packet compression metadata!");
return;
}
at = uncompressedData.data();
eof = uncompressedData.data() + uncompressedData.size() - 1;
}
packetNotCompressed:

bool error = false;
// Handle the packet if the length is there
const auto packetId = static_cast<protocol::ServerPacketsID>(protocol::popVarInt(at, eof));
Expand All @@ -244,7 +302,7 @@ void Client::_handlePacket()
case protocol::ClientStatus::Play:
GET_PARSER(Play);
}
std::vector<uint8_t> toParse(data.begin() + (at - data.data()), data.end());
std::vector<uint8_t> toParse(at, eof + 1);
data.erase(data.begin(), data.begin() + (startPayload - data.data()) + length);
if (error) {
N_LWARN("Unhandled packet: {} in status {}", packetId, _status);
Expand Down Expand Up @@ -272,7 +330,7 @@ void Client::_onHandshake(protocol::Handshake &pck)
this->setStatus(protocol::ClientStatus::Login);
}

void Client::_onStatusRequest(UNUSED protocol::StatusRequest &pck)
void Client::_onStatusRequest()
{
N_LDEBUG("Got a status request");

Expand Down Expand Up @@ -504,10 +562,19 @@ void Client::disconnect(const chat::Message &reason)
N_LDEBUG("Sent a disconnect login packet");
}

void Client::sendSetCompression()
{
auto pck = protocol::createSetCompression(CONFIG["compression-threshold"].as<int32_t>());
doWrite(std::move(pck));
_isCompressed = true;
N_LDEBUG("Send a set compression packet");
}

void Client::_loginSequence(const protocol::LoginSuccess &pck)
{
// Encryption request
// Set Compression
if (Server::getInstance()->isCompressed())
this->sendSetCompression();
this->sendLoginSuccess(pck);
this->switchToPlayState(pck.uuid, pck.username);
this->sendLoginPlay();
Expand Down
9 changes: 8 additions & 1 deletion cubic-server/Client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@

#define PCK_CALLBACK(type) __PCK_CALLBACK_PRIM(type, this)

#define __PCK_CALLBACK_EMPTY_PRIM(type, object) return object->_on##type()

#define PCK_CALLBACK_EMPTY(type) __PCK_CALLBACK_EMPTY_PRIM(type, this)

#define __PCK_CALLBACK_PLAY(type) __PCK_CALLBACK_PRIM(type, _player)

#define PCK_CALLBACK_PLAY(type) \
Expand Down Expand Up @@ -78,6 +82,7 @@ class Client : public std::enable_shared_from_this<Client> {
void sendLoginSuccess(const protocol::LoginSuccess &packet);
void sendLoginPlay(void);
void sendEncryptionRequest(void);
void sendSetCompression();

// Disconnect the client
void disconnect(const chat::Message &reason = "Disconnected");
Expand All @@ -93,13 +98,14 @@ class Client : public std::enable_shared_from_this<Client> {
void _tryFlushAllSendData();
// void _sendData(std::vector<uint8_t> &data);
void _onHandshake(protocol::Handshake &pck);
void _onStatusRequest(protocol::StatusRequest &pck);
void _onStatusRequest();
void _onLoginStart(protocol::LoginStart &pck);
void _onPingRequest(protocol::PingRequest &pck);
void _onEncryptionResponse(protocol::EncryptionResponse &pck);
void _loginSequence(const protocol::LoginSuccess &packet);
bool _handleOnline(const std::array<uint8_t, 16> &key);
NODISCARD inline const std::vector<protocol::PlayerProperty> &getProperties() const { return _resPck.properties; }
NODISCARD inline bool isCompressed() const { return _isCompressed; }

private:
std::atomic<bool> _isRunning;
Expand All @@ -118,6 +124,7 @@ class Client : public std::enable_shared_from_this<Client> {
bool _isEncrypted;
EASEncryptionHandler _encryption;
protocol::LoginSuccess _resPck;
bool _isCompressed;
};

#endif // CUBICSERVER_CLIENT_HPP
35 changes: 35 additions & 0 deletions cubic-server/CompressionUtils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <unistd.h>
#include <vector>
#include <zlib.h>

#include "Server.hpp"

int compressVector(const std::vector<uint8_t> &source, std::vector<uint8_t> &destination)
{
unsigned long source_length = source.size();
size_t destination_length = compressBound(source_length);

uint8_t *destination_data = (uint8_t *) malloc(destination_length);
emneo-dev marked this conversation as resolved.
Show resolved Hide resolved
if (destination_data == nullptr) {
return Z_MEM_ERROR;
}

Bytef *source_data = (Bytef *) source.data();
int return_value = compress2((Bytef *) destination_data, &destination_length, source_data, source_length, CONFIG["compression-level"].as<int>());
destination.insert(destination.end(), destination_data, destination_data + destination_length);
free(destination_data);
return return_value;
}

int decompressVector(const std::vector<uint8_t> &compressedBytes, std::vector<uint8_t> &uncompressedBytes, uint32_t size)
{
Bytef *dest = (Bytef *) malloc(sizeof(char) * size);
uLongf maxSize = size;
int return_value = uncompress(dest, &maxSize, (const Bytef *) compressedBytes.data(), (uLong) compressedBytes.size());
uncompressedBytes.insert(uncompressedBytes.end(), dest, dest + maxSize);
free(dest);
return return_value;
}
28 changes: 28 additions & 0 deletions cubic-server/CompressionUtils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef COMPRESSION_UTILS_HPP_
#define COMPRESSION_UTILS_HPP_

#include <cstdint>
#include <vector>

/**
* @brief Compresses a vector using zlib
*
* @param source Vector to compress
* @param destination Destion vector containing compressed data
* @return int Whatever zlib returns when compressing
* @todo Needs to be checked for lags
*/
int compressVector(const std::vector<uint8_t> &source, std::vector<uint8_t> &destination);

/**
* @brief Decompresses a vector using zlib
*
* @param compressedBytes Vector to decompress
* @param uncompressedBytes Destination vector with decompressed data
* @param size The size of the destination vector to expect
* @return int Whatever zlib return when uncompressing
* @todo Needs to be heavily optimized, so much weird code
*/
int decompressVector(const std::vector<uint8_t> &compressedBytes, std::vector<uint8_t> &uncompressedBytes, uint32_t size);

#endif // COMPRESSION_UTILS_HPP_
3 changes: 2 additions & 1 deletion cubic-server/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ Server::Server():
// _motd = _config.getMotd();
// _enforceWhitelist = _config.getEnforceWhitelist();

_commands.reserve(27);
_commands.emplace_back(std::make_unique<command_parser::Help>());
_commands.emplace_back(std::make_unique<command_parser::QuestionMark>());
_commands.emplace_back(std::make_unique<command_parser::Stop>());
Expand Down Expand Up @@ -228,6 +227,8 @@ void Server::_writeLoop()
boost::asio::write(client->getSocket(), boost::asio::buffer(data.data->data(), data.data->size()), ec);
// TODO(huntears): Handle errors properly xd
if (ec) {
client->disconnect("Network error");
delete data.data;
LERROR(ec.what());
continue;
}
Expand Down
3 changes: 3 additions & 0 deletions cubic-server/Server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ class Server {
std::thread _writeThread;

RSAEncryptionHandler _rsaKey;

public:
NODISCARD inline bool isCompressed() const { return _config["compression"].as<bool>(); }
};

#endif // CUBICSERVER_SERVER_HPP
29 changes: 26 additions & 3 deletions cubic-server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ auto initArgs(int argc, const char *const argv[])
.defaultValue(25565)
.required();

program.add("compression")
.help("enables compression of the protocol")
.valueFromConfig("network", "compression")
.valueFromEnvironmentVariable("CBSRV_COMPRESSION")
.defaultValue(true)
.required();

program.add("compression-level")
.help("sets the zlib compression level for outbound packets (0-9, 9 best compression)")
.valueFromConfig("network", "compression-level")
.valueFromEnvironmentVariable("CBSRV_COMPRESSION_LEVEL")
.defaultValue(6)
.inRange(0, 9)
.required();

program.add("compression-threshold")
.help("Minimum of bytes in a packet before compressing it")
.valueFromConfig("network", "compression-threshold")
.valueFromEnvironmentVariable("CBSRV_COMPRESSION_THRESHOLD")
.defaultValue(256)
.inRange(0, std::numeric_limits<int32_t>::max())
.required();

program.add("max-players")
.help("sets the maximum number of players")
.valueFromConfig("general", "max_players")
Expand Down Expand Up @@ -117,12 +140,12 @@ auto initArgs(int argc, const char *const argv[])
.defaultValue(10);

program.add("online-mode")
.help("Enable client/server encryption and only accepts legitimate accounts")
.valueFromConfig("general", "online-mode")
.help("[EXPERIMENTAL] Enable client/server encryption and only accepts legitimate accounts")
.valueFromConfig("experimental", "online-mode")
.valueFromEnvironmentVariable("CBSRV_ONLINE_MODE")
.valueFromArgument("--online-mode")
.possibleValues(false, true)
.defaultValue(true);
.defaultValue(false);

program.add("gamemode")
.help("Default gamemode")
Expand Down
13 changes: 13 additions & 0 deletions cubic-server/protocol/ClientPackets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ std::unique_ptr<std::vector<uint8_t>> protocol::createLoginSuccess(const LoginSu
return packet;
}

std::unique_ptr<std::vector<uint8_t>> protocol::createSetCompression(int32_t compressionTreshold)
{
std::vector<uint8_t> payload;
// clang-format off
serialize(payload,
compressionTreshold, addVarInt
);
// clang-format on
auto packet = std::make_unique<std::vector<uint8_t>>();
finalize(*packet, payload, ClientPacketID::SetCompression);
return packet;
}

std::unique_ptr<std::vector<uint8_t>> protocol::createStatusResponse(const StatusResponse &in)
{
std::vector<uint8_t> payload;
Expand Down
3 changes: 3 additions & 0 deletions cubic-server/protocol/ClientPackets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum class ClientPacketID : int32_t {
DisconnectLogin = 0x00,
EncryptionRequest = 0x01,
LoginSuccess = 0x02,
SetCompression = 0x03,

// Status State
Status = 0x00,
Expand Down Expand Up @@ -105,6 +106,8 @@ struct LoginSuccess {
};
std::unique_ptr<std::vector<uint8_t>> createLoginSuccess(const LoginSuccess &);

std::unique_ptr<std::vector<uint8_t>> createSetCompression(int32_t compressionTreshold);

struct StatusResponse {
std::string payload;
};
Expand Down