| /* |
| * Copyright 2010 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: sligocki@google.com (Shawn Ligocki) |
| |
| #include "net/instaweb/http/public/mock_url_fetcher.h" |
| |
| #include <map> |
| #include <utility> // for pair |
| |
| #include "base/logging.h" |
| #include "net/instaweb/http/public/async_fetch.h" |
| #include "pagespeed/kernel/base/basictypes.h" |
| #include "pagespeed/kernel/base/gtest.h" |
| #include "pagespeed/kernel/base/stl_util.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/base/thread_system.h" |
| #include "pagespeed/kernel/base/time_util.h" |
| #include "pagespeed/kernel/base/timer.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" |
| #include "pagespeed/kernel/util/platform.h" |
| |
| namespace net_instaweb { |
| |
| class MessageHandler; |
| |
| MockUrlFetcher::MockUrlFetcher() |
| : enabled_(true), |
| fail_on_unexpected_(true), |
| update_date_headers_(false), |
| omit_empty_writes_(false), |
| fail_after_headers_(false), |
| verify_host_header_(false), |
| verify_pagespeed_header_off_(false), |
| split_writes_(false), |
| supports_https_(false), |
| strip_query_params_(false), |
| timer_(NULL), |
| thread_system_(Platform::CreateThreadSystem()), |
| // TODO(hujie): We should pass in the mutex at all call-sites instead of |
| // creating a new mutex here. |
| mutex_(thread_system_->NewMutex()) { |
| } |
| |
| MockUrlFetcher::~MockUrlFetcher() { |
| Clear(); |
| } |
| |
| void MockUrlFetcher::SetResponse(const StringPiece& url, |
| const ResponseHeaders& response_header, |
| const StringPiece& response_body) { |
| // Note: This is a little kludgey, but if you set a normal response and |
| // always perform normal GETs you won't even notice that we've set the |
| // last_modified_time internally. |
| DCHECK(response_header.headers_complete()); |
| SetConditionalResponse(url, 0, "" , response_header, response_body); |
| } |
| |
| void MockUrlFetcher::AddToResponse(const StringPiece& url, |
| const StringPiece& name, |
| const StringPiece& value) { |
| ResponseMap::iterator iter = response_map_.find(url.as_string()); |
| CHECK(iter != response_map_.end()); |
| HttpResponse* http_response = iter->second; |
| ResponseHeaders* response = http_response->mutable_header(); |
| response->Add(name, value); |
| response->ComputeCaching(); |
| } |
| |
| void MockUrlFetcher::SetResponseFailure(const StringPiece& url) { |
| ResponseMap::iterator iter = response_map_.find(url.as_string()); |
| CHECK(iter != response_map_.end()); |
| HttpResponse* http_response = iter->second; |
| http_response->set_success(false); |
| } |
| |
| void MockUrlFetcher::SetConditionalResponse( |
| const StringPiece& url, int64 last_modified_time, const GoogleString& etag, |
| const ResponseHeaders& response_header, const StringPiece& response_body) { |
| GoogleString url_string = url.as_string(); |
| // Remove any old response. |
| RemoveResponse(url); |
| |
| // Add new response. |
| HttpResponse* response = new HttpResponse(last_modified_time, etag, |
| response_header, response_body); |
| response_map_.insert(ResponseMap::value_type(url_string, response)); |
| } |
| |
| void MockUrlFetcher::Clear() { |
| STLDeleteContainerPairSecondPointers(response_map_.begin(), |
| response_map_.end()); |
| response_map_.clear(); |
| // We don't have to protect response_map_ here, since only single |
| // setup/teardown would be called at a time. |
| { |
| ScopedMutex lock(mutex_.get()); |
| last_referer_.clear(); |
| } |
| } |
| |
| void MockUrlFetcher::RemoveResponse(const StringPiece& url) { |
| GoogleString url_string = url.as_string(); |
| ResponseMap::iterator iter = response_map_.find(url_string); |
| if (iter != response_map_.end()) { |
| delete iter->second; |
| response_map_.erase(iter); |
| } |
| } |
| |
| void MockUrlFetcher::Fetch( |
| const GoogleString& url_in, MessageHandler* message_handler, |
| AsyncFetch* fetch) { |
| const RequestHeaders& request_headers = *fetch->request_headers(); |
| ResponseHeaders* response_headers = fetch->response_headers(); |
| bool ret = false; |
| |
| bool enabled; |
| bool verify_host_header; |
| bool verify_pagespeed_header_off; |
| bool fail_after_headers; |
| bool update_date_headers; |
| bool omit_empty_writes; |
| bool fail_on_unexpected; |
| bool split_writes; |
| bool strip_query_params; |
| GoogleString error_message; |
| Timer* timer; |
| { |
| ScopedMutex lock(mutex_.get()); |
| enabled = enabled_; |
| verify_host_header = verify_host_header_; |
| verify_pagespeed_header_off = verify_pagespeed_header_off_; |
| timer = timer_; |
| fail_after_headers = fail_after_headers_; |
| update_date_headers = update_date_headers_; |
| omit_empty_writes = omit_empty_writes_; |
| fail_on_unexpected = fail_on_unexpected_; |
| error_message = error_message_; |
| split_writes = split_writes_; |
| strip_query_params = strip_query_params_; |
| } |
| if (enabled) { |
| GoogleString url = url_in; // editable version |
| GoogleUrl gurl(url); |
| EXPECT_TRUE(gurl.IsAnyValid()); |
| |
| if (strip_query_params) { |
| url = gurl.AllExceptQuery().as_string(); |
| gurl.Reset(url); |
| } |
| // Verify that the url and Host: header match. |
| if (verify_host_header) { |
| const char* host_header = request_headers.Lookup1(HttpAttributes::kHost); |
| EXPECT_STREQ(gurl.HostAndPort(), host_header); |
| } |
| if (verify_pagespeed_header_off) { |
| EXPECT_TRUE(request_headers.HasValue("PageSpeed", "off")); |
| } |
| const char* referer = request_headers.Lookup1(HttpAttributes::kReferer); |
| { |
| ScopedMutex lock(mutex_.get()); |
| if (referer == NULL) { |
| last_referer_.clear(); |
| } else { |
| last_referer_ = referer; |
| } |
| } |
| ResponseMap::iterator iter = response_map_.find(url); |
| if (iter != response_map_.end()) { |
| const HttpResponse* response = iter->second; |
| ret = response->success(); |
| |
| // Check if we should return 304 Not Modified or full response. |
| ConstStringStarVector values; |
| int64 if_modified_since_time; |
| if (request_headers.Lookup(HttpAttributes::kIfModifiedSince, &values) && |
| values.size() == 1 && |
| ConvertStringToTime(*values[0], &if_modified_since_time) && |
| if_modified_since_time > 0 && |
| if_modified_since_time >= response->last_modified_time()) { |
| // We recieved an If-Modified-Since header with a date that was |
| // parsable and at least as new our new resource. |
| // |
| // So, just serve 304 Not Modified. |
| response_headers->SetStatusAndReason(HttpStatus::kNotModified); |
| // TODO(sligocki): Perhaps allow other headers to be set. |
| // Date is technically required to be set. |
| } else if (!response->etag().empty() && |
| request_headers.Lookup(HttpAttributes::kIfNoneMatch, &values) && |
| values.size() == 1 && *values[0] == response->etag()) { |
| // We received an If-None-Match header whose etag matches that of the |
| // stored response. serve a 304 Not Modified. |
| response_headers->SetStatusAndReason(HttpStatus::kNotModified); |
| } else { |
| // Otherwise serve a normal 200 OK response. |
| int64 min_cache_ttl_ms_ = response_headers->min_cache_ttl_ms(); |
| int64 implicit_cache_ttl_ms_ = |
| response_headers->implicit_cache_ttl_ms(); |
| response_headers->CopyFrom(response->header()); |
| // implicit_cache_ttl_ms and min_cache_ttl_ms are set to default |
| // values from the origin fetch. The explicit values set in the test |
| // case take precedence over the default values set in origin fetch. |
| response_headers->set_implicit_cache_ttl_ms(implicit_cache_ttl_ms_); |
| response_headers->set_min_cache_ttl_ms(min_cache_ttl_ms_); |
| if (fail_after_headers) { |
| fetch->Done(false); |
| return; |
| } |
| if (update_date_headers) { |
| CHECK(timer != NULL); |
| // Update Date headers. |
| response_headers->SetDate(timer_->NowMs()); |
| } |
| response_headers->ComputeCaching(); |
| |
| if (!(response->body().empty() && omit_empty_writes)) { |
| if (!split_writes) { |
| // Normal case. |
| fetch->Write(response->body(), message_handler); |
| } else { |
| // This is used to test Ajax's RecordingFetch's cache recovery. |
| int mid = response->body().size() / 2; |
| StringPiece body = response->body(); |
| StringPiece head = body.substr(0, mid); |
| StringPiece tail = body.substr(mid, StringPiece::npos); |
| if (!(head.empty() && omit_empty_writes)) { |
| fetch->Write(head, message_handler); |
| } |
| if (!(tail.empty() && omit_empty_writes)) { |
| fetch->Write(tail, message_handler); |
| } |
| } |
| } |
| } |
| } else { |
| // This is used in tests and we do not expect the test to request a |
| // resource that we don't have. So fail if we do. |
| // |
| // If you want a 404 response, you must explicitly use SetResponse. |
| if (fail_on_unexpected) { |
| EXPECT_TRUE(false) << "Requested unset url " << url; |
| } |
| } |
| } |
| |
| if (!ret && !error_message.empty()) { |
| if (!response_headers->headers_complete()) { |
| response_headers->SetStatusAndReason(HttpStatus::kInternalServerError); |
| } |
| fetch->Write(error_message, message_handler); |
| } |
| |
| fetch->Done(ret); |
| } |
| |
| } // namespace net_instaweb |