Browse Source

Add gzip support. resolved #11

yhirose 8 years ago
parent
commit
1d5fbe6a5b
5 changed files with 128 additions and 14 deletions
  1. 14 0
      README.md
  2. 6 5
      example/Makefile
  3. 68 7
      httplib.h
  4. 2 1
      test/Makefile
  5. 38 1
      test/test.cc

+ 14 - 0
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
 -------
 

+ 6 - 5
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

+ 68 - 7
httplib.h

@@ -63,6 +63,10 @@ typedef int socket_t;
 #include <openssl/ssl.h>
 #endif
 
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+#include <zlib.h>
+#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);
             }

+ 2 - 1
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

+ 38 - 1
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()