| // Copyright 2015 Google Inc. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // Author: jefftk@google.com (Jeff Kaufman) |
| |
| #include "pagespeed/apache/apache_fetch.h" |
| |
| #include "net/instaweb/http/public/http_cache.h" |
| #include "net/instaweb/rewriter/public/rewrite_test_base.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "pagespeed/apache/apache_httpd_includes.h" |
| #include "pagespeed/apache/header_util.h" |
| #include "pagespeed/apache/mock_apache.h" |
| #include "pagespeed/kernel/base/function.h" |
| #include "pagespeed/kernel/base/gtest.h" |
| #include "pagespeed/kernel/base/mock_message_handler.h" |
| #include "pagespeed/kernel/base/timer.h" |
| #include "pagespeed/kernel/cache/delay_cache.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/http/http_names.h" |
| #include "pagespeed/kernel/http/request_headers.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| |
| namespace { |
| |
| const char kJsData[] = "alert ( 'hello, world!' ) "; |
| const char kJsUrl[] = "http://example.com/foo.js"; |
| const char kExampleUrl[] = "http://www.example.com"; |
| const char kCacheFragment[] = ""; |
| const char kDebugInfo[] = "ignored"; |
| const char kExpectedHeaders[] = |
| "Content-Type: text/plain\n" |
| "Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n"; |
| |
| } // namespace |
| |
| namespace net_instaweb { |
| |
| class ApacheFetchTest : public RewriteTestBase { |
| public: |
| ApacheFetchTest() |
| : apache_writer_( |
| new ApacheWriter(&request_, server_context_->thread_system())), |
| request_headers_(new RequestHeaders()), |
| request_ctx_(RequestContext::NewTestRequestContextWithTimer( |
| server_context_->thread_system(), timer())) { |
| } |
| |
| virtual void SetUp() { |
| RewriteTestBase::SetUp(); |
| |
| MockApache::Initialize(); |
| MockApache::PrepareRequest(&request_); |
| } |
| |
| virtual void TearDown() { |
| apache_fetch_.reset(NULL); |
| RewriteTestBase::TearDown(); |
| } |
| |
| void InitFetchUnbuffered(StringPiece url, HttpStatus::Code code) { |
| InitFetch(url, code, false); |
| } |
| |
| void InitFetchBuffered(StringPiece url, HttpStatus::Code code) { |
| InitFetch(url, code, true); |
| } |
| |
| void InitFetch(StringPiece url, HttpStatus::Code code, bool buffered) { |
| // Takes ownership of apache_writer and request_headers but nothing else. |
| // Keeping a copy them violates ApacheFetch's mutex guarantees, and should |
| // only be done by tests. |
| apache_fetch_.reset(new ApacheFetch( |
| url.as_string(), kDebugInfo, rewrite_driver_, apache_writer_, |
| request_headers_, request_ctx_, options(), message_handler())); |
| apache_fetch_->set_buffered(buffered); |
| apache_fetch_->response_headers()->SetStatusAndReason(code); |
| |
| // HTTP 1.1 is most common now. |
| apache_fetch_->response_headers()->set_major_version(1); |
| apache_fetch_->response_headers()->set_minor_version(1); |
| |
| // Responses need a content type or we will 403. |
| apache_fetch_->response_headers()->MergeContentType("text/plain"); |
| |
| // Set cookies to make sure they get removed. |
| apache_fetch_->response_headers()->Add("Set-Cookie", "test=cookie"); |
| apache_fetch_->response_headers()->Add("Set-Cookie", "tasty=cookie"); |
| apache_fetch_->response_headers()->Add("Set-Cookie2", "obselete"); |
| |
| // None of these should have been passed through to the apache_writer. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| } |
| |
| virtual ~ApacheFetchTest() { |
| MockApache::CleanupRequest(&request_); |
| MockApache::Terminate(); |
| } |
| |
| void WaitIproTest(bool buffered) { |
| // Run through a successful completion and verify that no issues were |
| // sent to the message handler. |
| AddFilter(RewriteOptions::kRewriteJavascriptExternal); |
| ResponseHeaders response_headers; |
| DefaultResponseHeaders(kContentTypeJavascript, 600, &response_headers); |
| RequestHeaders::Properties req_properties; |
| req_properties.has_cookie = false; |
| req_properties.has_cookie2 = false; |
| http_cache()->Put(kJsUrl, kCacheFragment, req_properties, |
| ResponseHeaders::kIgnoreVaryOnResources, |
| &response_headers, kJsData, &message_handler_); |
| |
| GoogleUrl gurl(kJsUrl); |
| InitFetch(kJsUrl, HttpStatus::kOK, buffered); |
| rewrite_driver_->FetchInPlaceResource(gurl, false /* proxy_mode */, |
| apache_fetch_.get()); |
| apache_fetch_->Wait(); |
| EXPECT_EQ(0, message_handler()->TotalMessages()); |
| |
| EXPECT_EQ(StrCat("ap_set_content_length(26) " |
| "ap_set_content_type(application/javascript) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(application/javascript) " |
| "ap_rwrite(", kJsData, ")"), |
| MockApache::ActionsSinceLastCall()); |
| apache_fetch_.reset(NULL); |
| } |
| |
| void ReleaseKey(GoogleString key) { |
| delay_cache()->ReleaseKey(key); |
| } |
| |
| protected: |
| void FetchDone() { |
| apache_fetch_->Done(true); |
| } |
| |
| // After a request is all set up, run through a successful completion and |
| // verify that no issues were sent to the message handler. |
| void WaitExpectSuccess() { |
| rewrite_driver_->scheduler()->AddAlarmAtUs( |
| timer()->NowUs() + 1, |
| MakeFunction(this, &ApacheFetchTest::FetchDone)); |
| apache_fetch_->Wait(); |
| EXPECT_EQ(0, message_handler_.TotalMessages()); |
| } |
| |
| GoogleString SynthesizeWarning(StringPiece message) { |
| return StrCat("W[Wed Jan 01 00:00:00 2014] [Warning] [00000] ", message); |
| } |
| |
| request_rec request_; |
| ApacheWriter* apache_writer_; |
| RequestHeaders* request_headers_; |
| RequestContextPtr request_ctx_; |
| scoped_ptr<ApacheFetch> apache_fetch_; |
| }; |
| |
| TEST_F(ApacheFetchTest, WaitIproUnbuffered) { |
| WaitIproTest(false); |
| } |
| |
| TEST_F(ApacheFetchTest, WaitIproUnbufferedWithTest) { |
| GoogleString key = http_cache()->CompositeKey(kJsUrl, kCacheFragment); |
| delay_cache()->DelayKey(key); |
| // This form looks clearer and more concise, and doesn't require a |
| // helper method. However it doesn't compile because of template |
| // problems I didn't feel like debugging. |
| // |
| // Function* alarm = MakeFunction(delay_cache(), &DelayCache::ReleaseKey,key); |
| ApacheFetchTest* test = this; |
| Function* alarm = MakeFunction(test, &ApacheFetchTest::ReleaseKey, key); |
| server_context_->scheduler()->AddAlarmAtUs(timer()->NowUs() + 100, alarm); |
| WaitIproTest(false); |
| } |
| |
| TEST_F(ApacheFetchTest, WaitIproBuffered) { |
| WaitIproTest(true); |
| } |
| |
| TEST_F(ApacheFetchTest, SuccessBuffered) { |
| InitFetchBuffered(kExampleUrl, HttpStatus::kOK); |
| EXPECT_TRUE(apache_fetch_->Write("hello ", &message_handler_)); |
| EXPECT_TRUE(apache_fetch_->Write("world", &message_handler_)); |
| EXPECT_TRUE(apache_fetch_->Flush(&message_handler_)); |
| EXPECT_TRUE(apache_fetch_->Write(".", &message_handler_)); |
| EXPECT_TRUE(apache_fetch_->Flush(&message_handler_)); |
| // All flushes are dropped, all writed are buffered. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| |
| WaitExpectSuccess(); |
| |
| EXPECT_EQ(200, request_.status); |
| EXPECT_EQ( |
| "ap_set_content_type(text/plain) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/plain) " |
| "ap_rwrite(hello world.)", // writes combined into one call. |
| MockApache::ActionsSinceLastCall()); |
| |
| // TODO(jefftk): Cookies are present here even though we asked ApacheWriter to |
| // remove them because of an ApacheWriter bug. |
| EXPECT_EQ(kExpectedHeaders, HeadersOutToString(&request_)); |
| } |
| |
| TEST_F(ApacheFetchTest, SuccessUnbuffered) { |
| InitFetchUnbuffered(kExampleUrl, HttpStatus::kOK); |
| EXPECT_TRUE(apache_fetch_->Write("hello ", &message_handler_)); |
| EXPECT_EQ(200, request_.status); |
| EXPECT_STREQ( |
| "ap_set_content_type(text/plain) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/plain) " |
| "ap_rwrite(hello )", |
| MockApache::ActionsSinceLastCall()); |
| |
| // TODO(jefftk): Cookies are present here even though we asked ApacheWriter to |
| // remove them because of an ApacheWriter bug. |
| EXPECT_EQ(kExpectedHeaders, HeadersOutToString(&request_)); |
| |
| EXPECT_TRUE(apache_fetch_->Write("world", message_handler())); |
| EXPECT_STREQ("ap_rwrite(world)", MockApache::ActionsSinceLastCall()); |
| EXPECT_TRUE(apache_fetch_->Flush(message_handler())); |
| EXPECT_STREQ("ap_rflush()", MockApache::ActionsSinceLastCall()); |
| EXPECT_TRUE(apache_fetch_->Write(".", message_handler())); |
| EXPECT_STREQ("ap_rwrite(.)", MockApache::ActionsSinceLastCall()); |
| EXPECT_TRUE(apache_fetch_->Flush(message_handler())); |
| EXPECT_STREQ("ap_rflush()", MockApache::ActionsSinceLastCall()); |
| apache_fetch_->Done(true); |
| } |
| |
| TEST_F(ApacheFetchTest, NotFound404Buffered) { |
| InitFetchBuffered(kExampleUrl, HttpStatus::kNotFound); |
| EXPECT_TRUE(apache_fetch_->Write("Couldn't find it.", &message_handler_)); |
| // Writes are buffered. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| |
| WaitExpectSuccess(); |
| |
| EXPECT_EQ(404, request_.status); |
| EXPECT_EQ(kExpectedHeaders, HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_set_content_type(text/plain) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/plain) " |
| "ap_rwrite(Couldn't find it.)", |
| MockApache::ActionsSinceLastCall()); |
| } |
| |
| TEST_F(ApacheFetchTest, NotFound404Unbuffered) { |
| InitFetchUnbuffered(kExampleUrl, HttpStatus::kNotFound); |
| EXPECT_TRUE(apache_fetch_->Write("Couldn't find it.", message_handler())); |
| EXPECT_EQ(404, request_.status); |
| EXPECT_EQ(kExpectedHeaders, HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_set_content_type(text/plain) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/plain) " |
| "ap_rwrite(Couldn't find it.)", |
| MockApache::ActionsSinceLastCall()); |
| apache_fetch_->Done(true); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType200Buffered) { |
| InitFetchBuffered(kExampleUrl, HttpStatus::kOK); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll( |
| HttpAttributes::kContentType)); |
| EXPECT_TRUE(apache_fetch_->Write("Example response.", &message_handler_)); |
| // Writes are buffered. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| |
| WaitExpectSuccess(); |
| |
| EXPECT_EQ(403, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Content-Type: text/html\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_set_content_type(text/html) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/html) " |
| "ap_rwrite(Missing Content-Type required for proxied resource)", |
| MockApache::ActionsSinceLastCall()); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType200Unbuffered) { |
| InitFetchUnbuffered(kExampleUrl, HttpStatus::kOK); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll( |
| HttpAttributes::kContentType)); |
| EXPECT_TRUE(apache_fetch_->Write("Example response.", message_handler())); |
| EXPECT_EQ(403, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Content-Type: text/html\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_set_content_type(text/html) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/html) " |
| "ap_rwrite(Missing Content-Type required for proxied resource)", |
| MockApache::ActionsSinceLastCall()); |
| apache_fetch_->Done(true); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType301Buffered) { |
| InitFetchBuffered(kExampleUrl, HttpStatus::kMovedPermanently); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type")); |
| apache_fetch_->response_headers()->Add("Location", "elsewhere"); |
| |
| EXPECT_TRUE(apache_fetch_->Write("moved elsewhere", &message_handler_)); |
| // Writes are buffered. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| |
| WaitExpectSuccess(); |
| |
| EXPECT_EQ(403, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Location: elsewhere\n" |
| "Content-Type: text/html\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_set_content_type(text/html) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/html) " |
| "ap_rwrite(Missing Content-Type required for proxied resource)", |
| MockApache::ActionsSinceLastCall()); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType301Unbuffered) { |
| InitFetchUnbuffered(kExampleUrl, HttpStatus::kMovedPermanently); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type")); |
| apache_fetch_->response_headers()->Add("Location", "elsewhere"); |
| EXPECT_TRUE(apache_fetch_->Write("moved elsewhere", message_handler())); |
| EXPECT_EQ(403, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Location: elsewhere\n" |
| "Content-Type: text/html\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_set_content_type(text/html) " |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_set_content_type(text/html) " |
| "ap_rwrite(Missing Content-Type required for proxied resource)", |
| MockApache::ActionsSinceLastCall()); |
| apache_fetch_->Done(true); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType304Buffered) { |
| InitFetchBuffered(kExampleUrl, HttpStatus::kNotModified); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type")); |
| |
| EXPECT_TRUE(apache_fetch_->Write("not modified", &message_handler_)); |
| // Writes are buffered. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| |
| WaitExpectSuccess(); |
| |
| EXPECT_EQ(304, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_rwrite(not modified)", |
| MockApache::ActionsSinceLastCall()); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType304Unbuffered) { |
| InitFetchUnbuffered(kExampleUrl, HttpStatus::kNotModified); |
| apache_fetch_->response_headers()->set_status_code(304); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type")); |
| EXPECT_TRUE(apache_fetch_->Write("not modified", message_handler())); |
| EXPECT_EQ(304, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_EQ( |
| "ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT) " |
| "ap_rwrite(not modified)", |
| MockApache::ActionsSinceLastCall()); |
| apache_fetch_->Done(true); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType204Buffered) { |
| InitFetchBuffered(kExampleUrl, HttpStatus::kNoContent); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type")); |
| apache_fetch_->HeadersComplete(); |
| // Headers are buffered. |
| EXPECT_STREQ("", MockApache::ActionsSinceLastCall()); |
| |
| WaitExpectSuccess(); |
| EXPECT_EQ(204, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_STREQ("ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT)", |
| MockApache::ActionsSinceLastCall()); |
| } |
| |
| TEST_F(ApacheFetchTest, NoContentType204Unbuffered) { |
| InitFetchUnbuffered(kExampleUrl, HttpStatus::kNoContent); |
| apache_fetch_->response_headers()->set_status_code(204); |
| EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type")); |
| apache_fetch_->HeadersComplete(); |
| EXPECT_EQ(204, request_.status); |
| EXPECT_STREQ("Set-Cookie: test=cookie\n" |
| "Set-Cookie: tasty=cookie\n" |
| "Set-Cookie2: obselete\n" |
| "Date: Tue, 02 Feb 2010 18:51:26 GMT\n" |
| "X-Content-Type-Options: nosniff\n" |
| "Cache-Control: max-age=0, no-cache\n", |
| HeadersOutToString(&request_)); |
| EXPECT_STREQ("ap_remove_output_filter(MOD_EXPIRES) " |
| "ap_remove_output_filter(FIXUP_HEADERS_OUT)", |
| MockApache::ActionsSinceLastCall()); |
| apache_fetch_->Done(true); |
| } |
| |
| } // namespace net_instaweb |