From 1d5fbe6a5b2d57838aecee878791d7df392a1bfc Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 28 Dec 2017 20:47:52 -0500 Subject: [PATCH] Add gzip support. resolved #11 --- README.md | 14 +++++++++ example/Makefile | 11 +++---- httplib.h | 75 +++++++++++++++++++++++++++++++++++++++++++----- test/Makefile | 3 +- test/test.cc | 39 ++++++++++++++++++++++++- 5 files changed, 128 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c440cfa..69a7f11 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,20 @@ SSLServer svr("./cert.pem", "./key.pem"); SSLClient cli("localhost", 8080); ``` +Zlib Support +------------ + +'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. + +The server applies gzip compression to the following MIME type contents: + + * all text types + * image/svg+xml + * application/javascript + * application/json + * application/xml + * application/xhtml+xml + License ------- diff --git a/example/Makefile b/example/Makefile index a0e4944..28af76c 100644 --- a/example/Makefile +++ b/example/Makefile @@ -2,23 +2,24 @@ CC = clang++ CFLAGS = -std=c++14 -I.. -Wall -Wextra -lpthread #OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib -lssl -lcrypto +ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz all: server client hello simplesvr benchmark server : server.cc ../httplib.h Makefile - $(CC) -o server $(CFLAGS) server.cc $(OPENSSL_SUPPORT) + $(CC) -o server $(CFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) client : client.cc ../httplib.h Makefile - $(CC) -o client $(CFLAGS) client.cc $(OPENSSL_SUPPORT) + $(CC) -o client $(CFLAGS) client.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) hello : hello.cc ../httplib.h Makefile - $(CC) -o hello $(CFLAGS) hello.cc $(OPENSSL_SUPPORT) + $(CC) -o hello $(CFLAGS) hello.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) simplesvr : simplesvr.cc ../httplib.h Makefile - $(CC) -o simplesvr $(CFLAGS) simplesvr.cc $(OPENSSL_SUPPORT) + $(CC) -o simplesvr $(CFLAGS) simplesvr.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) benchmark : benchmark.cc ../httplib.h Makefile - $(CC) -o benchmark $(CFLAGS) benchmark.cc $(OPENSSL_SUPPORT) + $(CC) -o benchmark $(CFLAGS) benchmark.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) pem: openssl genrsa 2048 > key.pem diff --git a/httplib.h b/httplib.h index 4f66cbf..cded0e1 100644 --- a/httplib.h +++ b/httplib.h @@ -63,6 +63,10 @@ typedef int socket_t; #include #endif +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include +#endif + /* * Configuration */ @@ -597,19 +601,15 @@ inline std::string file_extension(const std::string& path) return std::string(); } -inline const char* content_type(const std::string& path) +inline const char* find_content_type(const std::string& path) { auto ext = file_extension(path); if (ext == "txt") { return "text/plain"; } else if (ext == "html") { return "text/html"; - } else if (ext == "js") { - return "text/javascript"; } else if (ext == "css") { return "text/css"; - } else if (ext == "xml") { - return "text/xml"; } else if (ext == "jpeg" || ext == "jpg") { return "image/jpg"; } else if (ext == "png") { @@ -624,6 +624,10 @@ inline const char* content_type(const std::string& path) return "application/json"; } else if (ext == "pdf") { return "application/pdf"; + } else if (ext == "js") { + return "application/javascript"; + } else if (ext == "xml") { + return "application/xml"; } else if (ext == "xhtml") { return "application/xhtml+xml"; } @@ -774,7 +778,7 @@ bool read_content(Stream& strm, T& x, Progress progress = Progress()) if (len) { return read_content_with_length(strm, x, len, progress); } else { - auto encoding = get_header_value(x.headers, "Transfer-Encoding", ""); + const auto& encoding = get_header_value(x.headers, "Transfer-Encoding", ""); if (!strcmp(encoding, "chunked")) { return read_content_chunked(strm, x); @@ -1070,6 +1074,59 @@ inline void make_range_header_core(std::string& field, uint64_t value1, uint64_t make_range_header_core(field, args...); } +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +inline bool can_compress(const std::string& content_type) { + return !content_type.find("text/") || + content_type == "image/svg+xml" || + content_type == "application/javascript" || + content_type == "application/json" || + content_type == "application/xml" || + content_type == "application/xhtml+xml"; +} + +inline void compress(const Request& req, Response& res) +{ + // TODO: Server version is HTTP/1.1 and 'Accpet-Encoding' has gzip, not gzip;q=0 + const auto& encodings = req.get_header_value("Accept-Encoding"); + if (encodings.find("gzip") == std::string::npos) { + return; + } + + if (!can_compress(res.get_header_value("Content-Type"))) { + return; + } + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + auto ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + return; + } + + strm.avail_in = res.body.size(); + strm.next_in = (Bytef *)res.body.data(); + + std::string compressed; + + const auto bufsiz = 16384; + char buff[bufsiz]; + do { + strm.avail_out = bufsiz; + strm.next_out = (Bytef *)buff; + deflate(&strm, Z_FINISH); + compressed.append(buff, bufsiz - strm.avail_out); + } while (strm.avail_out == 0); + + res.set_header("Content-Encoding", "gzip"); + res.body.swap(compressed); + + deflateEnd(&strm); +} +#endif + #ifdef _WIN32 class WSInit { public: @@ -1375,6 +1432,10 @@ inline void Server::write_response(Stream& strm, bool last_connection, const Req } if (!res.body.empty()) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + detail::compress(req, res); +#endif + if (!res.has_header("Content-Type")) { res.set_header("Content-Type", "text/plain"); } @@ -1407,7 +1468,7 @@ inline bool Server::handle_file_request(Request& req, Response& res) if (detail::is_file(path)) { detail::read_file(path, res.body); - auto type = detail::content_type(path); + auto type = detail::find_content_type(path); if (type) { res.set_header("Content-Type", type); } diff --git a/test/Makefile b/test/Makefile index f466617..8c76650 100644 --- a/test/Makefile +++ b/test/Makefile @@ -2,12 +2,13 @@ CC = clang++ CFLAGS = -std=c++11 -DGTEST_USE_OWN_TR1_TUPLE -I.. -I. -Wall -Wextra -lpthread #OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib -lssl -lcrypto +#ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz all : test ./test test : test.cc ../httplib.h Makefile - $(CC) -o test $(CFLAGS) test.cc gtest/gtest-all.cc gtest/gtest_main.cc $(OPENSSL_SUPPORT) + $(CC) -o test $(CFLAGS) test.cc gtest/gtest-all.cc gtest/gtest_main.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) pem: openssl genrsa 2048 > key.pem diff --git a/test/test.cc b/test/test.cc index ba4b173..1edb8af 100644 --- a/test/test.cc +++ b/test/test.cc @@ -267,7 +267,16 @@ protected: EXPECT_EQ("application/octet-stream", file.content_type); EXPECT_EQ(0u, file.length); } - }); + }) +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + .get("/gzip", [&](const Request& /*req*/, Response& res) { + res.set_content("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "text/plain"); + }) + .get("/nogzip", [&](const Request& /*req*/, Response& res) { + res.set_content("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "application/octet-stream"); + }) +#endif + ; persons_["john"] = "programmer"; @@ -580,6 +589,34 @@ TEST_F(ServerTest, CaseInsensitiveHeaderName) EXPECT_EQ("Hello World!", res->body); } +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +TEST_F(ServerTest, Gzip) +{ + Headers headers; + headers.emplace("Accept-Encoding", "gzip, deflate"); + auto res = cli_.get("/gzip", headers); + + ASSERT_TRUE(res != nullptr); + EXPECT_EQ("gzip", res->get_header_value("Content-Encoding")); + EXPECT_EQ("text/plain", res->get_header_value("Content-Type")); + EXPECT_EQ("33", res->get_header_value("Content-Length")); + EXPECT_EQ(200, res->status); +} + +TEST_F(ServerTest, NoGzip) +{ + Headers headers; + headers.emplace("Accept-Encoding", "gzip, deflate"); + auto res = cli_.get("/nogzip", headers); + + ASSERT_TRUE(res != nullptr); + EXPECT_EQ(false, res->has_header("Content-Encoding")); + EXPECT_EQ("application/octet-stream", res->get_header_value("Content-Type")); + EXPECT_EQ("100", res->get_header_value("Content-Length")); + EXPECT_EQ(200, res->status); +} +#endif + class ServerTestWithAI_PASSIVE : public ::testing::Test { protected: ServerTestWithAI_PASSIVE()