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 4 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
74 changes: 70 additions & 4 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,37 @@ 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() < COMPRESSION_THRESHOLD) {
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() - 1), compressedData);
assert(compressReturn != Z_MEM_ERROR);
protocol::addVarInt(out, compressedData.size() + (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 +160,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 @@ -225,7 +260,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, length)) {
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 Down Expand Up @@ -272,7 +329,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 +561,19 @@ void Client::disconnect(const chat::Message &reason)
N_LDEBUG("Sent a disconnect login packet");
}

void Client::sendSetCompression()
{
auto pck = protocol::createSetCompression(COMPRESSION_THRESHOLD);
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
90 changes: 90 additions & 0 deletions cubic-server/CompressionUtils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include <cstddef>
#include <cstdint>
#include <unistd.h>
#include <vector>
#include <zlib.h>

#include "Server.hpp"

static void add_buffer_to_vector(std::vector<uint8_t> &vector, const uint8_t *buffer, size_t length)
{
for (size_t character_index = 0; character_index < length; character_index++) {
char current_character = buffer[character_index];
vector.push_back(current_character);
Copy link
Member

Choose a reason for hiding this comment

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

?

Suggested change
char current_character = buffer[character_index];
vector.push_back(current_character);
vector.push_back(buffer[character_index]);

}
}

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>());
add_buffer_to_vector(destination, destination_data, destination_length);
free(destination_data);
return return_value;
}

bool decompressVector(const std::vector<uint8_t> &compressedBytes, std::vector<uint8_t> &uncompressedBytes, uint32_t size)
{
unsigned full_length = size;
unsigned half_length = size / 2;

unsigned uncompLength = full_length;
char *uncomp = (char *) calloc(sizeof(char), uncompLength);
Copy link
Member

Choose a reason for hiding this comment

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

Wsh

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is wrong with it?


z_stream strm;
strm.next_in = (Bytef *) compressedBytes.data();
strm.avail_in = size;
strm.total_out = 0;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;

bool done = false;

if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) {
free(uncomp);
return false;
}

while (!done) {
// If our output buffer is too small
if (strm.total_out >= uncompLength) {
// Increase size of output buffer
char *uncomp2 = (char *) calloc(sizeof(char), uncompLength + half_length);
memcpy(uncomp2, uncomp, uncompLength);
uncompLength += half_length;
free(uncomp);
uncomp = uncomp2;
}

strm.next_out = (Bytef *) (uncomp + strm.total_out);
strm.avail_out = uncompLength - strm.total_out;

// Inflate another chunk.
int err = inflate(&strm, Z_SYNC_FLUSH);
if (err == Z_STREAM_END)
done = true;
else if (err != Z_OK) {
break;
}
}

if (inflateEnd(&strm) != Z_OK) {
free(uncomp);
return false;
}

for (size_t i = 0; i < strm.total_out; ++i) {
uncompressedBytes.push_back(uncomp[i]);
}
// uncompressedBytes.insert(uncompressedBytes.begin(), uncomp, uncomp + strm.total_out);
free(uncomp);
return true;
}
29 changes: 29 additions & 0 deletions cubic-server/CompressionUtils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#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 true Decompression suceeded
* @return false Something bad happened
* @todo Needs to be heavily optimized, so much weird code
*/
bool 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
4 changes: 4 additions & 0 deletions cubic-server/Server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
constexpr char MC_VERSION[] = "1.19.3";
constexpr uint16_t MC_PROTOCOL = 761;
constexpr uint16_t MS_PER_TICK = 50;
constexpr uint32_t COMPRESSION_THRESHOLD = 256;

#define GLOBAL_PALETTE Server::getInstance()->getGlobalPalette()
#define ITEM_CONVERTER Server::getInstance()->getItemConverter()
Expand Down Expand Up @@ -153,6 +154,9 @@ class Server {
std::thread _writeThread;

RSAEncryptionHandler _rsaKey;

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

#endif // CUBICSERVER_SERVER_HPP
21 changes: 18 additions & 3 deletions cubic-server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ 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 compression level for outbound packets (0-9, 9 best compression)")
.valueFromConfig("network", "compression-level")
.valueFromEnvironmentVariable("CBSRV_COMPRESSION_LEVEL")
.defaultValue(1)
.inRange(0, 9)
.required();

program.add("max-players")
.help("sets the maximum number of players")
.valueFromConfig("general", "max_players")
Expand Down Expand Up @@ -117,12 +132,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
Loading