diff --git a/code/components/citizen-server-impl/component.json b/code/components/citizen-server-impl/component.json index 1d18f8aca3..eaf403282a 100644 --- a/code/components/citizen-server-impl/component.json +++ b/code/components/citizen-server-impl/component.json @@ -24,6 +24,7 @@ "vendor:eastl", "vendor:thread-pool-cpp", "vendor:utfcpp", + "vendor:zlib", "vendor:breakpad", "vendor:prometheus-cpp", "vendor:folly", diff --git a/code/components/citizen-server-impl/src/ClientHttpHandler.cpp b/code/components/citizen-server-impl/src/ClientHttpHandler.cpp index c7da723608..afb184ae5e 100644 --- a/code/components/citizen-server-impl/src/ClientHttpHandler.cpp +++ b/code/components/citizen-server-impl/src/ClientHttpHandler.cpp @@ -11,11 +11,62 @@ #include +#include + using json = nlohmann::json; static std::shared_ptr> g_threadedHttpVar; static std::shared_ptr> g_maxClientEndpointRequestSize; +namespace +{ +bool ClientAcceptsGzip(const fwRefContainer& request) +{ + const auto header = request->GetHeader("accept-encoding"); + + for (size_t i = 0; i + 4 <= header.size(); ++i) + { + if ((header[i] | 0x20) == 'g' && (header[i + 1] | 0x20) == 'z' && + (header[i + 2] | 0x20) == 'i' && (header[i + 3] | 0x20) == 'p') + { + return true; + } + } + + return false; +} + +std::optional GzipCompress(std::string_view input) +{ + z_stream zs{}; + + // MAX_WBITS + 16 for the gzip wrapper format + if (deflateInit2(&zs, Z_BEST_SPEED, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) + { + return std::nullopt; + } + + std::string output; + output.resize(deflateBound(&zs, static_cast(input.size()))); + + zs.next_in = reinterpret_cast(const_cast(input.data())); + zs.avail_in = static_cast(input.size()); + zs.next_out = reinterpret_cast(output.data()); + zs.avail_out = static_cast(output.size()); + + const int rc = deflate(&zs, Z_FINISH); + if (rc != Z_STREAM_END) + { + deflateEnd(&zs); + return std::nullopt; + } + + output.resize(zs.total_out); + deflateEnd(&zs); + return output; +} +} + namespace fx { auto ClientMethodRegistry::GetHandler(const std::string& method) -> std::optional, THandler>> @@ -90,7 +141,7 @@ namespace fx } else if (handler->index() == 1) { - (std::get<1>(*handler))(postMap, request, [response](const rapidjson::Document& data) + (std::get<1>(*handler))(postMap, request, [request, response](const rapidjson::Document& data) { if (data.IsNull()) { @@ -110,12 +161,30 @@ namespace fx sb.Put('\r'); sb.Put('\n'); + // Compress with gzip when client supports it + constexpr size_t kGzipMinSize = 1024; + + std::string compressedHolder; + std::string_view bodyView{ sb.GetString(), sb.GetSize() }; + + if (bodyView.size() >= kGzipMinSize && ClientAcceptsGzip(request)) + { + if (auto compressed = GzipCompress(bodyView)) + { + compressedHolder = std::move(*compressed); + bodyView = compressedHolder; + response->SetHeader(net::HeaderString{ "content-encoding" }, + net::HeaderString{ "gzip" }); + } + } + // for TCP write timeout bits, write this in chunks constexpr size_t kChunkSize = 16384; - for (size_t i = 0; i < sb.GetLength(); i += kChunkSize) + for (size_t i = 0; i < bodyView.size(); i += kChunkSize) { - response->Write(std::string{ sb.GetString() + i, std::min(kChunkSize, sb.GetLength() - i) }); + response->Write(std::string{ bodyView.data() + i, + std::min(kChunkSize, bodyView.size() - i) }); } }); } diff --git a/code/components/http-client/src/HttpClient.cpp b/code/components/http-client/src/HttpClient.cpp index 9ac3e869ee..5c8034f852 100644 --- a/code/components/http-client/src/HttpClient.cpp +++ b/code/components/http-client/src/HttpClient.cpp @@ -557,7 +557,8 @@ static std::shared_ptr SetupCURLHandle(const std::unique_ptr