diff --git a/cubic-server/CMakeLists.txt b/cubic-server/CMakeLists.txt index 7f93eb54f..242aa4969 100644 --- a/cubic-server/CMakeLists.txt +++ b/cubic-server/CMakeLists.txt @@ -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) diff --git a/cubic-server/Client.cpp b/cubic-server/Client.cpp index f45515cc2..5eda22271 100644 --- a/cubic-server/Client.cpp +++ b/cubic-server/Client.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -12,12 +14,14 @@ #include #include #include +#include #include "Client.hpp" #include "PlayerAttributes.hpp" #include "nbt.hpp" #include "Checksum.hpp" +#include "CompressionUtils.hpp" #include "Dimension.hpp" #include "Player.hpp" #include "Server.hpp" @@ -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" @@ -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"); } @@ -90,8 +96,38 @@ void Client::doRead() // Server::getInstance()->triggerClientCleanup(_clientID); } +static void compressPacket(std::vector &in, std::vector &out) +{ + // Don't compress if the packet is too small + if (in.size() < (size_t) CONFIG["compression-threshold"].as()) { + 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 compressedData; + int compressReturn = compressVector(std::vector(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> &&data) { + if (_isCompressed) { + auto toSend = std::make_unique>(); + 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)); @@ -125,7 +161,7 @@ void Client::handleParsedClientPacket(std::unique_ptr uncompressedData; + if (_isCompressed) { + int32_t uncompressedLength = protocol::popVarInt(at, eof); + if (uncompressedLength == 0) + goto packetNotCompressed; + if (decompressVector(std::vector(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::popVarInt(at, eof)); @@ -244,7 +302,7 @@ void Client::_handlePacket() case protocol::ClientStatus::Play: GET_PARSER(Play); } - std::vector toParse(data.begin() + (at - data.data()), data.end()); + std::vector toParse(at, eof + 1); data.erase(data.begin(), data.begin() + (startPayload - data.data()) + length); if (error) { N_LWARN("Unhandled packet: {} in status {}", packetId, _status); @@ -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"); @@ -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()); + 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(); diff --git a/cubic-server/Client.hpp b/cubic-server/Client.hpp index 615dfa90d..a93c2aff9 100644 --- a/cubic-server/Client.hpp +++ b/cubic-server/Client.hpp @@ -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) \ @@ -78,6 +82,7 @@ class Client : public std::enable_shared_from_this { void sendLoginSuccess(const protocol::LoginSuccess &packet); void sendLoginPlay(void); void sendEncryptionRequest(void); + void sendSetCompression(); // Disconnect the client void disconnect(const chat::Message &reason = "Disconnected"); @@ -93,13 +98,14 @@ class Client : public std::enable_shared_from_this { void _tryFlushAllSendData(); // void _sendData(std::vector &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 &key); NODISCARD inline const std::vector &getProperties() const { return _resPck.properties; } + NODISCARD inline bool isCompressed() const { return _isCompressed; } private: std::atomic _isRunning; @@ -118,6 +124,7 @@ class Client : public std::enable_shared_from_this { bool _isEncrypted; EASEncryptionHandler _encryption; protocol::LoginSuccess _resPck; + bool _isCompressed; }; #endif // CUBICSERVER_CLIENT_HPP diff --git a/cubic-server/CompressionUtils.cpp b/cubic-server/CompressionUtils.cpp new file mode 100644 index 000000000..ce7cf8395 --- /dev/null +++ b/cubic-server/CompressionUtils.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include +#include +#include + +#include "Server.hpp" + +int compressVector(const std::vector &source, std::vector &destination) +{ + unsigned long source_length = source.size(); + size_t destination_length = compressBound(source_length); + + uint8_t *destination_data = (uint8_t *) malloc(destination_length); + 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()); + destination.insert(destination.end(), destination_data, destination_data + destination_length); + free(destination_data); + return return_value; +} + +int decompressVector(const std::vector &compressedBytes, std::vector &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; +} diff --git a/cubic-server/CompressionUtils.hpp b/cubic-server/CompressionUtils.hpp new file mode 100644 index 000000000..60490c206 --- /dev/null +++ b/cubic-server/CompressionUtils.hpp @@ -0,0 +1,28 @@ +#ifndef COMPRESSION_UTILS_HPP_ +#define COMPRESSION_UTILS_HPP_ + +#include +#include + +/** + * @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 &source, std::vector &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 &compressedBytes, std::vector &uncompressedBytes, uint32_t size); + +#endif // COMPRESSION_UTILS_HPP_ diff --git a/cubic-server/Server.cpp b/cubic-server/Server.cpp index c6b4143fc..c3533c83e 100644 --- a/cubic-server/Server.cpp +++ b/cubic-server/Server.cpp @@ -51,7 +51,6 @@ Server::Server(): // _motd = _config.getMotd(); // _enforceWhitelist = _config.getEnforceWhitelist(); - _commands.reserve(27); _commands.emplace_back(std::make_unique()); _commands.emplace_back(std::make_unique()); _commands.emplace_back(std::make_unique()); @@ -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; } diff --git a/cubic-server/Server.hpp b/cubic-server/Server.hpp index 800c519e6..715867b7f 100644 --- a/cubic-server/Server.hpp +++ b/cubic-server/Server.hpp @@ -153,6 +153,9 @@ class Server { std::thread _writeThread; RSAEncryptionHandler _rsaKey; + +public: + NODISCARD inline bool isCompressed() const { return _config["compression"].as(); } }; #endif // CUBICSERVER_SERVER_HPP diff --git a/cubic-server/main.cpp b/cubic-server/main.cpp index 6aa32c9b4..98bd50bdc 100644 --- a/cubic-server/main.cpp +++ b/cubic-server/main.cpp @@ -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::max()) + .required(); + program.add("max-players") .help("sets the maximum number of players") .valueFromConfig("general", "max_players") @@ -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") diff --git a/cubic-server/protocol/ClientPackets.cpp b/cubic-server/protocol/ClientPackets.cpp index 925cacef4..c8dceda1c 100644 --- a/cubic-server/protocol/ClientPackets.cpp +++ b/cubic-server/protocol/ClientPackets.cpp @@ -68,6 +68,19 @@ std::unique_ptr> protocol::createLoginSuccess(const LoginSu return packet; } +std::unique_ptr> protocol::createSetCompression(int32_t compressionTreshold) +{ + std::vector payload; + // clang-format off + serialize(payload, + compressionTreshold, addVarInt + ); + // clang-format on + auto packet = std::make_unique>(); + finalize(*packet, payload, ClientPacketID::SetCompression); + return packet; +} + std::unique_ptr> protocol::createStatusResponse(const StatusResponse &in) { std::vector payload; diff --git a/cubic-server/protocol/ClientPackets.hpp b/cubic-server/protocol/ClientPackets.hpp index 3555c0068..82e00e31e 100644 --- a/cubic-server/protocol/ClientPackets.hpp +++ b/cubic-server/protocol/ClientPackets.hpp @@ -24,6 +24,7 @@ enum class ClientPacketID : int32_t { DisconnectLogin = 0x00, EncryptionRequest = 0x01, LoginSuccess = 0x02, + SetCompression = 0x03, // Status State Status = 0x00, @@ -105,6 +106,8 @@ struct LoginSuccess { }; std::unique_ptr> createLoginSuccess(const LoginSuccess &); +std::unique_ptr> createSetCompression(int32_t compressionTreshold); + struct StatusResponse { std::string payload; };