proxygen
ZlibServerFilterTest.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2015-present, Facebook, Inc.
3  * All rights reserved.
4  *
5  * This source code is licensed under the BSD-style license found in the
6  * LICENSE file in the root directory of this source tree. An additional grant
7  * of patent rights can be found in the PATENTS file in the same directory.
8  *
9  */
10 #include <folly/Conv.h>
11 #include <folly/ScopeGuard.h>
12 #include <folly/io/IOBuf.h>
13 
20 
21 using namespace proxygen;
22 using namespace testing;
23 
24 MATCHER_P(IOBufEquals,
25  expected,
26  folly::to<std::string>(
27  "IOBuf is ", negation ? "not " : "", "'", expected, "'")) {
28  auto iob = arg->clone();
29  auto br = iob->coalesce();
30  std::string actual(br.begin(), br.end());
31  *result_listener << "'" << actual << "'";
32  return actual == expected;
33 }
34 
35 class ZlibServerFilterTest : public Test {
36  public:
37  void SetUp() override {
38  // requesthandler is the server, responsehandler is the client
39  requestHandler_ = new MockRequestHandler();
40  responseHandler_ = std::make_unique<MockResponseHandler>(requestHandler_);
41  zd_ = std::make_unique<ZlibStreamDecompressor>(ZlibCompressionType::GZIP);
42  }
43 
44  void TearDown() override {
45  Mock::VerifyAndClear(requestHandler_);
46  Mock::VerifyAndClear(responseHandler_.get());
47 
48  delete requestHandler_;
49  }
50 
51  protected:
52  ZlibServerFilter* filter_{nullptr};
54  std::unique_ptr<MockResponseHandler> responseHandler_;
55  std::unique_ptr<ZlibStreamDecompressor> zd_;
56  ResponseHandler* downstream_{nullptr};
57 
58  void exercise_compression(bool expectCompression,
59  std::string url,
60  std::string acceptedEncoding,
61  std::string expectedEncoding,
62  std::string originalRequestBody,
63  std::string responseContentType,
64  std::unique_ptr<folly::IOBuf> originalResponseBody,
65  int32_t compressionLevel = 4,
66  uint32_t minimumCompressionSize = 1) {
67 
68  // If there is only one IOBuf, then it's not chunked.
69  bool isResponseChunked = originalResponseBody->isChained();
70  size_t chunkCount = originalResponseBody->countChainElements();
71 
72  // Chunked and compressed responses will have an extra block
73  if (isResponseChunked && expectCompression) {
74  chunkCount += 1;
75  }
76 
77  // Request Handler Expectations
78  EXPECT_CALL(*requestHandler_, onBody(_)).Times(1);
79  EXPECT_CALL(*requestHandler_, onEOM()).Times(1);
80 
81  // Need to capture whatever the filter is for ResponseBuilder later
82  EXPECT_CALL(*requestHandler_, setResponseHandler(_))
83  .WillOnce(DoAll(SaveArg<0>(&downstream_), Return()));
84 
85  // Response Handler Expectations
86  // Headers are only sent once
87  EXPECT_CALL(*responseHandler_, sendHeaders(_)).WillOnce(DoAll(
88  Invoke([&](HTTPMessage& msg) {
89  auto& headers = msg.getHeaders();
90  if (expectCompression) {
92  HTTP_HEADER_CONTENT_ENCODING, expectedEncoding.c_str(), false));
93  }
94 
95  if (msg.getIsChunked()) {
96  EXPECT_FALSE(headers.exists("Content-Length"));
97  } else {
98  //Content-Length is not set on chunked messages
99  EXPECT_TRUE(headers.exists("Content-Length"));
100  }
101  }),
102  Return()));
103 
104  if (isResponseChunked) {
105  // The final chunk has 0 body
106  EXPECT_CALL(*responseHandler_, sendChunkHeader(_)).Times(chunkCount);
107  EXPECT_CALL(*responseHandler_, sendChunkTerminator()).Times(chunkCount);
108  } else {
109  EXPECT_CALL(*responseHandler_, sendChunkHeader(_)).Times(0);
110  EXPECT_CALL(*responseHandler_, sendChunkTerminator()).Times(0);
111  }
112 
113  // Accumulate the body, decompressing it if it's compressed
114  std::unique_ptr<folly::IOBuf> responseBody;
115  EXPECT_CALL(*responseHandler_, sendBody(_))
116  .Times(chunkCount)
117  .WillRepeatedly(DoAll(
118  Invoke([&](std::shared_ptr<folly::IOBuf> body) {
119 
120  std::unique_ptr<folly::IOBuf> processedBody;
121 
122  if (expectCompression) {
123  processedBody = zd_->decompress(body.get());
124  ASSERT_FALSE(zd_->hasError())
125  << "Failed to decompress body. r=" << zd_->getStatus();
126  } else {
127  processedBody = folly::IOBuf::copyBuffer(
128  body->data(), body->length(), 0, 0);
129  }
130 
131  if (responseBody) {
132  responseBody->prependChain(std::move(processedBody));
133  } else {
134  responseBody = std::move(processedBody);
135  }
136  }),
137  Return()));
138 
139  EXPECT_CALL(*responseHandler_, sendEOM()).Times(1);
140 
141  /* Simulate Request/Response */
142 
143  HTTPMessage msg;
144  msg.setURL(url);
145  msg.getHeaders().set(HTTP_HEADER_ACCEPT_ENCODING, acceptedEncoding);
146 
147  std::set<std::string> compressibleTypes = {"text/html"};
148  auto filterFactory = std::make_unique<ZlibServerFilterFactory>(
149  compressionLevel, minimumCompressionSize, compressibleTypes);
150 
151  auto filter = filterFactory->onRequest(requestHandler_, &msg);
152  filter->setResponseHandler(responseHandler_.get());
153 
154  // Send fake request
155  filter->onBody(folly::IOBuf::copyBuffer(originalRequestBody));
156  filter->onEOM();
157 
158  // Send a fake Response
159  if (isResponseChunked) {
160 
161  ResponseBuilder(downstream_)
162  .status(200, "OK")
163  .header(HTTP_HEADER_CONTENT_TYPE, responseContentType)
164  .send();
165 
166  folly::IOBuf* crtBuf;
167  crtBuf = originalResponseBody.get();
168 
169  do {
170  ResponseBuilder(downstream_).body(crtBuf->cloneOne()).send();
171  crtBuf = crtBuf->next();
172  } while (crtBuf != originalResponseBody.get());
173 
174  ResponseBuilder(downstream_).sendWithEOM();
175 
176  } else {
177 
178  // Send unchunked response
179  ResponseBuilder(downstream_)
180  .status(200, "OK")
181  .header(HTTP_HEADER_CONTENT_TYPE, responseContentType)
182  .body(originalResponseBody->clone())
183  .sendWithEOM();
184  }
185 
186  filter->requestComplete();
187 
188  EXPECT_THAT(responseBody, IOBufEquals(originalRequestBody));
189  }
190 
191  // Helper method to convert a vector of strings to an IOBuf chain
192  // specificaly create a chain because the chain pieces are chunks
193  std::unique_ptr<folly::IOBuf> createResponseChain(
194  std::vector<std::string> const& bodyStrings) {
195 
196  std::unique_ptr<folly::IOBuf> responseBodyChain;
197 
198  for (auto& s : bodyStrings) {
199  auto nextBody = folly::IOBuf::copyBuffer(s.c_str());
200  if (responseBodyChain) {
201  responseBodyChain->prependChain(std::move(nextBody));
202  } else {
203  responseBodyChain = std::move(nextBody);
204  }
205  }
206 
207  return responseBodyChain;
208  }
209 };
210 
211 // Basic smoke test
212 TEST_F(ZlibServerFilterTest, NonchunkedCompression) {
214  exercise_compression(true,
215  std::string("http://locahost/foo.compressme"),
216  std::string("gzip"),
217  std::string("gzip"),
218  std::string("Hello World"),
219  std::string("text/html"),
220  folly::IOBuf::copyBuffer("Hello World"));
221  });
222 }
223 
224 TEST_F(ZlibServerFilterTest, ChunkedCompression) {
225  std::vector<std::string> chunks = {"Hello", " World"};
227  exercise_compression(true,
228  std::string("http://locahost/foo.compressme"),
229  std::string("gzip"),
230  std::string("gzip"),
231  std::string("Hello World"),
232  std::string("text/html"),
233  createResponseChain(chunks));
234  });
235 }
236 
237 TEST_F(ZlibServerFilterTest, ParameterizedContenttype) {
239  exercise_compression(true,
240  std::string("http://locahost/foo.compressme"),
241  std::string("gzip"),
242  std::string("gzip"),
243  std::string("Hello World"),
244  std::string("text/html; param1"),
245  folly::IOBuf::copyBuffer("Hello World"));
246  });
247 }
248 
249 TEST_F(ZlibServerFilterTest, MixedcaseContenttype) {
251  exercise_compression(true,
252  std::string("http://locahost/foo.compressme"),
253  std::string("gzip"),
254  std::string("gzip"),
255  std::string("Hello World"),
256  std::string("Text/Html; param1"),
257  folly::IOBuf::copyBuffer("Hello World"));
258  });
259 }
260 
261 // Client supports multiple possible compression encodings
262 TEST_F(ZlibServerFilterTest, MultipleAcceptedEncodings) {
264  exercise_compression(true,
265  std::string("http://locahost/foo.compressme"),
266  std::string("gzip, identity, deflate"),
267  std::string("gzip"),
268  std::string("Hello World"),
269  std::string("text/html"),
270  folly::IOBuf::copyBuffer("Hello World"));
271  });
272 }
273 
274 TEST_F(ZlibServerFilterTest, MultipleAcceptedEncodingsQvalues) {
276  exercise_compression(true,
277  std::string("http://locahost/foo.compressme"),
278  std::string("gzip; q=.7;, identity"),
279  std::string("gzip"),
280  std::string("Hello World"),
281  std::string("text/html"),
282  folly::IOBuf::copyBuffer("Hello World"));
283  });
284 }
285 
286 TEST_F(ZlibServerFilterTest, NoCompressibleAcceptedEncodings) {
288  exercise_compression(false,
289  std::string("http://locahost/foo.compressme"),
290  std::string("identity; q=.7;"),
291  std::string(""),
292  std::string("Hello World"),
293  std::string("text/html"),
294  folly::IOBuf::copyBuffer("Hello World"));
295  });
296 }
297 
298 TEST_F(ZlibServerFilterTest, MissingAcceptedEncodings) {
300  exercise_compression(false,
301  std::string("http://locahost/foo.compressme"),
302  std::string(""),
303  std::string(""),
304  std::string("Hello World"),
305  std::string("text/html"),
306  folly::IOBuf::copyBuffer("Hello World"));
307  });
308 }
309 
310 // Content is of an-uncompressible content-type
311 TEST_F(ZlibServerFilterTest, UncompressibleContenttype) {
313  exercise_compression(false,
314  std::string("http://locahost/foo.nocompress"),
315  std::string("gzip"),
316  std::string(""),
317  std::string("Hello World"),
318  std::string("image/jpeg"),
319  folly::IOBuf::copyBuffer("Hello World"));
320  });
321 }
322 
323 TEST_F(ZlibServerFilterTest, UncompressibleContenttypeParam) {
325  exercise_compression(false,
326  std::string("http://locahost/foo.nocompress"),
327  std::string("gzip"),
328  std::string(""),
329  std::string("Hello World"),
330  std::string("application/jpeg; param1"),
331  folly::IOBuf::copyBuffer("Hello World"));
332  });
333 }
334 
335 // Content is under the minimum compression size
336 TEST_F(ZlibServerFilterTest, TooSmallToCompress) {
338  exercise_compression(false,
339  std::string("http://locahost/foo.smallfry"),
340  std::string("gzip"),
341  std::string(""),
342  std::string("Hello World"),
343  std::string("text/html"),
344  folly::IOBuf::copyBuffer("Hello World"),
345  4,
346  1000);
347  });
348 }
349 
350 TEST_F(ZlibServerFilterTest, SmallChunksCompress) {
351  // Expect this to compress despite being small because can't tell the content
352  // length when we're chunked
353  std::vector<std::string> chunks = {"Hello", " World"};
355  exercise_compression(true,
356  std::string("http://locahost/foo.compressme"),
357  std::string("gzip"),
358  std::string("gzip"),
359  std::string("Hello World"),
360  std::string("text/html"),
361  createResponseChain(chunks),
362  4,
363  1000);
364  });
365 }
366 
367 TEST_F(ZlibServerFilterTest, MinimumCompressSizeEqualToRequestSize){
368  auto requestBody = std::string("Hello World");
370  exercise_compression(true,
371  std::string("http://locahost/foo.compressme"),
372  std::string("gzip"),
373  std::string("gzip"),
374  requestBody,
375  std::string("text/html"),
376  folly::IOBuf::copyBuffer(requestBody),
377  4,
378  requestBody.length());
379  });
380 }
381 
382 TEST_F(ZlibServerFilterTest, NoResponseBody){
383  std::string acceptedEncoding = "gzip";
384  std::string expectedEncoding = "gzip";
385  std::string url = std::string("http://locahost/foo.compressme");
386  std::string responseContentType = std::string("text/html");
387  int32_t compressionLevel = 4;
388  uint32_t minimumCompressionSize = 0;
389 
391  // Request Handler Expectations
392  EXPECT_CALL(*requestHandler_, onEOM()).Times(1);
393 
394  // Need to capture whatever the filter is for ResponseBuilder later
395  EXPECT_CALL(*requestHandler_, setResponseHandler(_))
396  .WillOnce(DoAll(SaveArg<0>(&downstream_), Return()));
397 
398  // Response Handler Expectations
399  // Headers are only sent once
400  EXPECT_CALL(*responseHandler_, sendHeaders(_)).WillOnce(DoAll(
401  Invoke([&](HTTPMessage& msg) {
402  auto& headers = msg.getHeaders();
404  HTTP_HEADER_CONTENT_ENCODING, expectedEncoding.c_str(), false));
405  if (msg.getIsChunked()) {
406  EXPECT_FALSE(headers.exists("Content-Length"));
407  } else {
408  //Content-Length is not set on chunked messages
409  EXPECT_TRUE(headers.exists("Content-Length"));
410  }
411  }),
412  Return()));
413 
414  EXPECT_CALL(*responseHandler_, sendEOM()).Times(1);
415 
416  /* Simulate Request/Response where no body message received */
417  HTTPMessage msg;
418  msg.setURL(url);
419  msg.getHeaders().set(HTTP_HEADER_ACCEPT_ENCODING, acceptedEncoding);
420 
421  std::set<std::string> compressibleTypes = {"text/html"};
422  auto filterFactory = std::make_unique<ZlibServerFilterFactory>(
423  compressionLevel, minimumCompressionSize, compressibleTypes);
424 
425  auto filter = filterFactory->onRequest(requestHandler_, &msg);
426  filter->setResponseHandler(responseHandler_.get());
427 
428  // Send fake request
429  filter->onEOM();
430 
431  ResponseBuilder(downstream_)
432  .status(200, "OK")
433  .header(HTTP_HEADER_CONTENT_TYPE, responseContentType)
434  .send();
435 
436  ResponseBuilder(downstream_).sendWithEOM();
437 
438  filter->requestComplete();
439  });
440 }
auto chunks
ResponseBuilder & status(uint16_t code, const std::string &message)
void exercise_compression(bool expectCompression, std::string url, std::string acceptedEncoding, std::string expectedEncoding, std::string originalRequestBody, std::string responseContentType, std::unique_ptr< folly::IOBuf > originalResponseBody, int32_t compressionLevel=4, uint32_t minimumCompressionSize=1)
PUSHMI_INLINE_VAR constexpr detail::filter_fn filter
Definition: filter.h:75
constexpr detail::Map< Move > move
Definition: Base-inl.h:2567
bool isChained() const
Definition: IOBuf.h:760
std::unique_ptr< IOBuf > clone() const
Definition: IOBuf.cpp:527
size_t countChainElements() const
Definition: IOBuf.cpp:493
ResponseBuilder & body(std::unique_ptr< folly::IOBuf > bodyIn)
std::unique_ptr< folly::IOBuf > createResponseChain(std::vector< std::string > const &bodyStrings)
std::unique_ptr< folly::IOBuf > decompress(const folly::IOBuf *in)
void set(folly::StringPiece name, const std::string &value)
Definition: HTTPHeaders.h:119
std::unique_ptr< ZlibStreamDecompressor > zd_
bool checkForHeaderToken(const HTTPHeaderCode headerCode, char const *token, bool caseSensitive) const
ParseURL setURL(T &&url)
Definition: HTTPMessage.h:183
bool getIsChunked() const
Definition: HTTPMessage.h:80
PolymorphicAction< internal::InvokeAction< FunctionImpl > > Invoke(FunctionImpl function_impl)
ResponseBuilder & header(const std::string &headerIn, const T &value)
MockRequestHandler * requestHandler_
ssize_t send(NetworkSocket s, const void *buf, size_t len, int flags)
Definition: NetOps.cpp:319
std::unique_ptr< IOBuf > cloneOne() const
Definition: IOBuf.cpp:531
IOBuf * next()
Definition: IOBuf.h:600
HTTPHeaders & getHeaders()
Definition: HTTPMessage.h:273
std::unique_ptr< MockResponseHandler > responseHandler_
void prependChain(std::unique_ptr< IOBuf > &&iobuf)
Definition: IOBuf.cpp:509
#define EXPECT_TRUE(condition)
Definition: gtest.h:1859
#define EXPECT_THAT(value, matcher)
const char * string
Definition: Conv.cpp:212
static set< string > s
internal::DoBothAction< Action1, Action2 > DoAll(Action1 a1, Action2 a2)
#define EXPECT_CALL(obj, call)
const internal::AnythingMatcher _
TEST_F(HeaderTableTests, IndexTranslation)
#define ASSERT_NO_FATAL_FAILURE(statement)
Definition: gtest.h:2099
#define ASSERT_FALSE(condition)
Definition: gtest.h:1868
#define EXPECT_FALSE(condition)
Definition: gtest.h:1862
static std::unique_ptr< IOBuf > copyBuffer(const void *buf, std::size_t size, std::size_t headroom=0, std::size_t minTailroom=0)
Definition: IOBuf.h:1587
MATCHER_P(PtrBufHasLen, n,"")
Definition: TestUtils.h:410
internal::ReturnAction< R > Return(R value)