blob: b8d92704d50a8f9cd0e88e679ddc9dae328e455b [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/autofill/automated_tests/cache_replayer.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/base64url.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind_test_util.h"
#include "base/values.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/cpp/resource_request.h"
#include "third_party/protobuf/src/google/protobuf/stubs/status.h"
#include "third_party/protobuf/src/google/protobuf/stubs/statusor.h"
#include "third_party/zlib/google/compression_utils.h"
// TODO(crbug/977571): Change returned bool for ::Status.
namespace autofill {
namespace test {
using base::JSONParserOptions;
using base::JSONReader;
namespace {
using google::protobuf::util::Status;
using google::protobuf::util::StatusOr;
constexpr char kHTTPBodySep[] = "\r\n\r\n";
// Makes an internal error that carries an error message.
Status MakeInternalError(const std::string& error_message) {
return Status(google::protobuf::util::error::INTERNAL, error_message);
}
// Container that represents a JSON node that contains a list of
// request/response pairs sharing the same URL.
struct QueryNode {
// Query URL.
std::string url;
// Value node with requests mapped with |url|.
const base::Value* node = nullptr;
};
// Gets a hexadecimal representation of a string.
std::string GetHexString(const std::string& input) {
std::string output("0x");
for (auto byte : input) {
base::StringAppendF(&output, "%02x", static_cast<unsigned char>(byte));
}
return output;
}
// Makes HTTP request from a header and body
std::string MakeHTTPTextFromSplit(const std::string& header,
const std::string& body) {
return base::JoinString({header, body}, kHTTPBodySep);
}
// Determines whether replayer should fail if there is an invalid json record.
bool FailOnError(int options) {
return static_cast<bool>(options &
ServerCacheReplayer::kOptionFailOnInvalidJsonRecord);
}
// Determines whether replayer should fail if there is nothing to fill the cache
// with.
bool FailOnEmpty(int options) {
return static_cast<bool>(options & ServerCacheReplayer::kOptionFailOnEmpty);
}
// Determines whether replayer should split and cache each form individually.
bool SplitRequestsByForm(int options) {
return static_cast<bool>(options &
ServerCacheReplayer::kOptionSplitRequestsByForm);
}
// Checks the validity of a json value node.
bool CheckNodeValidity(const base::Value* node,
const std::string& name,
base::Value::Type type) {
if (node == nullptr) {
VLOG(1) << "Did not find any " << name << "field in json";
return false;
}
if (node->type() != type) {
VLOG(1) << "Node value is not of type " << node->type()
<< " when it should be of type " << type;
return false;
}
return true;
}
// Gets the RequestType by guessing from the URL.
RequestType GetRequestTypeFromURL(base::StringPiece url) {
if (url.find("q=") != std::string::npos) {
return RequestType::kLegacyQueryProtoGET;
}
return RequestType::kLegacyQueryProtoPOST;
}
// Parse AutofillQueryContents or AutofillQueryResponseContents from the given
// |http_text|.
template <class T>
StatusOr<T> ParseProtoContents(const std::string http_text) {
T proto_contents;
if (!proto_contents.ParseFromString(http_text)) {
return MakeInternalError(
base::StrCat({"could not parse proto:`", proto_contents.GetTypeName(),
"` from raw data:`", GetHexString(http_text), "`."}));
}
return StatusOr<T>(std::move(proto_contents));
}
// Gets Query request proto content from GET URL.
StatusOr<AutofillQueryContents> GetAutofillQueryContentsFromGETQueryURL(
const GURL& url) {
std::string q_value;
if (!net::GetValueForKeyInQuery(url, "q", &q_value)) {
// This situation will never happen if check for the presence of "q=" is
// done before calling this function.
return MakeInternalError(
base::StrCat({"could not get any value from \"q\" query parameter in "
"Query GET URL: ",
url.spec()}));
}
// Base64-decode the "q" value.
std::string decoded_query;
if (!base::Base64UrlDecode(q_value,
base::Base64UrlDecodePolicy::REQUIRE_PADDING,
&decoded_query)) {
return MakeInternalError(base::StrCat(
{"could not base64-decode value of query parameter \"q\" in Query GET "
"URL: \"",
q_value, "\""}));
}
return ParseProtoContents<AutofillQueryContents>(decoded_query);
}
// Puts all data elements within the request or response body together in a
// single DataElement and returns the buffered content as a string. This ensures
// that all the response body data is utilized.
std::string GetStringFromDataElements(
const std::vector<network::DataElement>* data_elements) {
network::DataElement unified_data_element;
unified_data_element.SetToEmptyBytes();
for (auto it = data_elements->begin(); it != data_elements->end(); ++it) {
unified_data_element.AppendBytes(it->bytes(), it->length());
}
// Using the std::string constructor with length ensures that we don't rely
// on having a termination character to delimit the string. This is the
// safest approach.
return std::string(unified_data_element.bytes(),
unified_data_element.length());
}
// Gets Query request proto content from HTTP POST body.
StatusOr<AutofillQueryContents> GetAutofillQueryContentsFromPOSTQuery(
const network::ResourceRequest& resource_request) {
return ParseProtoContents<AutofillQueryContents>(
GetStringFromDataElements(resource_request.request_body->elements()));
}
// Validates, retrieves, and decodes node |node_name| from |request_node| and
// returns it in |decoded_value|. Returns false if unsuccessful.
bool RetrieveValueFromRequestNode(const base::Value& request_node,
const std::string node_name,
std::string* decoded_value) {
// Get and check field node string.
std::string serialized_value;
{
const base::Value* node = request_node.FindKey(node_name);
if (!CheckNodeValidity(node, node_name, base::Value::Type::STRING)) {
VLOG(1) << "Invalid Node in WPR archive";
return false;
}
serialized_value = node->GetString();
}
// Decode serialized request string.
{
if (!base::Base64Decode(serialized_value, decoded_value)) {
VLOG(1) << "Could not base64 decode serialized value: "
<< serialized_value;
return false;
}
}
return true;
}
// Gets AutofillQueryContents from WPR recorded HTTP request body for POST.
StatusOr<AutofillQueryContents> GetAutofillQueryContentsFromRequestNode(
const base::Value& request_node) {
std::string decoded_request_text;
if (!RetrieveValueFromRequestNode(request_node, "SerializedRequest",
&decoded_request_text)) {
return MakeInternalError(
"Unable to retrieve serialized request from WPR request_node");
}
return ParseProtoContents<AutofillQueryContents>(
SplitHTTP(decoded_request_text).second);
}
// Gets AutofillQueryResponseContents from WPR recorded HTTP response body.
// Also populates and returns the split |response_header_text|.
StatusOr<AutofillQueryResponseContents>
GetAutofillQueryResponseContentsFromRequestNode(
const base::Value& request_node,
std::string* response_header_text) {
std::string compressed_response_text;
if (!RetrieveValueFromRequestNode(request_node, "SerializedResponse",
&compressed_response_text)) {
return MakeInternalError(
"Unable to retrieve serialized request from WPR request_node");
}
auto http_pair = SplitHTTP(compressed_response_text);
std::string decompressed_body;
if (!compression::GzipUncompress(http_pair.second, &decompressed_body)) {
return MakeInternalError(
base::StrCat({"Could not gzip decompress HTTP response: ",
GetHexString(http_pair.second)}));
}
// Eventual response needs header information, so lift that as well.
*response_header_text = http_pair.first;
return ParseProtoContents<AutofillQueryResponseContents>(decompressed_body);
}
// Fills |cache_to_fill| with the keys from a single |query_request| and
// |query_response| pair. Loops through each form in request and creates an
// individual response of just the associated fields for that request. Uses
// |response_header_text| to build and store well-formed and backwards
// compatible http text in the cache.
bool FillFormSplitCache(const AutofillQueryContents& query_request,
const std::string& response_header_text,
const AutofillQueryResponseContents& query_response,
ServerCache* cache_to_fill) {
VLOG(2) << "Full Request Key is:" << GetKeyFromQueryRequest(query_request);
VLOG(2) << "Matching keys from Query request proto:\n" << query_request;
VLOG(2) << "To field types from Query response proto:\n" << query_response;
auto current_field = query_response.field().begin();
for (const auto& form : query_request.form()) {
std::string key = base::NumberToString(form.signature());
// If already stored a respones for this key, then just advance the
// current_field by that offset and continue.
if (base::Contains((*cache_to_fill), key)) {
VLOG(2) << "Already added key: " << key;
current_field += form.field_size();
continue;
}
// Grab fields for this form from overall response and add to unique form
// object.
AutofillQueryResponseContents individual_form_response;
for (int i = 0; i < form.field_size(); i++) {
if (current_field >= query_response.field().end()) {
VLOG(1) << "Reached end of query_response fields prematurely!";
return false;
}
individual_form_response.add_field()->CopyFrom(*current_field);
++current_field;
}
// Compress that form response to a string and gzip it.
std::string serialized_response;
if (!individual_form_response.SerializeToString(&serialized_response)) {
VLOG(1) << "Unable to serialize the new response for key! " << key;
continue;
}
std::string compressed_response_body;
if (!compression::GzipCompress(serialized_response,
&compressed_response_body)) {
VLOG(1) << "Unable to compress the new response for key! " << key;
continue;
}
// Final http text is header_text concatenated with a compressed body.
std::string http_text =
MakeHTTPTextFromSplit(response_header_text, compressed_response_body);
VLOG(1) << "Adding key:" << key
<< "\nAnd response:" << individual_form_response;
(*cache_to_fill)[key] = std::move(http_text);
}
return true;
}
// Populates |cache_to_fill| with content from |query_node| that contains a
// list of single request node that share the same URL field (e.g.,
// https://clients1.google.com/tbproxy/af/query) in the WPR capture json cache.
// Returns Status with message when there is an error when parsing the requests
// and OPTION_FAIL_ON_INVALID_JSON is flipped in |options|. Returns status ok
// regardless of errors if OPTION_FAIL_ON_INVALID_JSON is not flipped in
// |options| where bad nodes will be skipped. Keeps a log trace whenever there
// is an error even if OPTION_FAIL_ON_INVALID_JSON is not flipped. Uses only the
// form combinations seen in recorded session if OPTION_SPLIT_REQUESTS_BY_FORM
// is false, fill cache with individual form keys (and expect
// ServerCacheReplayer to be able to split incoming request by key and stitch
// results together).
ServerCacheReplayer::Status PopulateCacheFromQueryNode(
const QueryNode& query_node,
int options,
ServerCache* cache_to_fill) {
bool fail_on_error = FailOnError(options);
bool split_requests_by_form = SplitRequestsByForm(options);
for (const base::Value& request : query_node.node->GetList()) {
// Get AutofillQueryContents from request.
bool is_post_request = GetRequestTypeFromURL(query_node.url) ==
RequestType::kLegacyQueryProtoPOST;
StatusOr<AutofillQueryContents> query_request_statusor =
is_post_request
? GetAutofillQueryContentsFromRequestNode(request)
: GetAutofillQueryContentsFromGETQueryURL(GURL(query_node.url));
// Only proceed if successfully parse the query request proto, else drop to
// failure space.
if (query_request_statusor.ok()) {
VLOG(2) << "Getting key from Query request proto:\n "
<< query_request_statusor.ValueOrDie();
std::string key =
GetKeyFromQueryRequest(query_request_statusor.ValueOrDie());
bool is_single_form_request =
query_request_statusor.ValueOrDie().form_size() == 1;
// Switch to store forms as individuals or only in the groupings that they
// were sent on recording. If only a single form in request then can use
// old behavior still and skip decompression and combination steps.
if (!split_requests_by_form || is_single_form_request) {
std::string compressed_response_text;
if (RetrieveValueFromRequestNode(request, "SerializedResponse",
&compressed_response_text)) {
(*cache_to_fill)[key] = std::move(compressed_response_text);
VLOG(1) << "Cached response content for key: " << key;
continue;
}
} else {
// Get AutofillQueryResponseContents and response header text.
std::string response_header_text;
StatusOr<AutofillQueryResponseContents> query_response_statusor =
GetAutofillQueryResponseContentsFromRequestNode(
request, &response_header_text);
if (!query_response_statusor.ok()) {
VLOG(1) << "Unable to get AutofillQueryResponseContents from WPR node"
<< "SerializedResponse for request:" << key;
} else {
// We have a proper request and a proper response, we can populate for
// each form in the AutofillQueryContents.
if (FillFormSplitCache(
query_request_statusor.ValueOrDie(), response_header_text,
query_response_statusor.ValueOrDie(), cache_to_fill)) {
continue;
}
}
}
}
// If we've fallen to this level, something went bad with adding the request
// node. If fail_on_error is set then abort, else log and try the next one.
constexpr base::StringPiece status_msg =
"could not cache query node content";
if (fail_on_error) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kBadNode, status_msg.as_string()};
} else {
// Keep a trace when not set to fail on bad node.
VLOG(1) << status_msg;
}
}
return ServerCacheReplayer::Status{ServerCacheReplayer::StatusCode::kOk, ""};
}
// TODO(crbug/958125): Add the possibility to retrieve nodes with different
// Query URLs.
// Finds the Autofill server Query node in dictionary node. Gives nullptr if
// cannot find the node or |domain_dict| is invalid. The |domain_dict| has to
// outlive any usage of the returned value node pointers.
std::vector<QueryNode> FindAutofillQueryNodesInDomainDict(
const base::Value& domain_dict) {
if (!domain_dict.is_dict()) {
return {};
}
std::vector<QueryNode> nodes;
for (const auto& pair : domain_dict.DictItems()) {
if (pair.first.find("https://clients1.google.com/tbproxy/af/query") !=
std::string::npos) {
nodes.push_back(QueryNode{pair.first, &pair.second});
}
}
return nodes;
}
// Populates the cache mapping request keys to their corresponding compressed
// response.
ServerCacheReplayer::Status PopulateCacheFromJSONFile(
const base::FilePath& json_file_path,
int options,
ServerCache* cache_to_fill) {
// Read json file.
std::string json_text;
{
if (!base::ReadFileToString(json_file_path, &json_text)) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kBadRead,
"Could not read json file: "};
}
}
// Decompress the json text from gzip.
std::string decompressed_json_text;
if (!compression::GzipUncompress(json_text, &decompressed_json_text)) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kBadRead,
"Could not gzip decompress json in file: "};
}
// Parse json text content to json value node.
base::Value root_node;
{
JSONReader::ValueWithError value_with_error =
JSONReader().ReadAndReturnValueWithError(
decompressed_json_text, JSONParserOptions::JSON_PARSE_RFC);
if (!value_with_error.value) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kBadRead,
base::StrCat({"Could not load cache from json file ",
"because: ", value_with_error.error_message})};
}
root_node = std::move(value_with_error.value.value());
}
{
const char* const domain = "clients1.google.com";
const base::Value* domain_node = root_node.FindPath({"Requests", domain});
if (domain_node == nullptr) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kEmpty,
base::StrCat({"there were no nodes with autofill query content in "
"domain node \"",
domain, "\""})};
}
std::vector<QueryNode> query_nodes =
FindAutofillQueryNodesInDomainDict(*domain_node);
// Fill cache with the content of each Query node. There are 3 possible
// situations: (1) there is a single Query node that contains POST requests
// that share the same URL, (2) there is one Query node per GET request
// where each Query node only contains one request, and (3) a mix of (1) and
// (2). Exit early with false whenever there is an error parsing a node.
for (auto query_node : query_nodes) {
if (!CheckNodeValidity(query_node.node,
"Requests->clients1.google.com->clients1.google."
"com/tbproxy/af/query*",
base::Value::Type::LIST)) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kBadNode,
"could not read node content for node with URL " + query_node.url};
}
// Populate cache from Query node content.
auto status =
PopulateCacheFromQueryNode(query_node, options, cache_to_fill);
if (!status.Ok())
return status;
VLOG(1) << "Filled cache with " << cache_to_fill->size()
<< " requests for Query node with URL: " << query_node.url;
}
}
// Return error iff there are no Query nodes and replayer is set to fail on
// empty.
if (cache_to_fill->empty() && FailOnEmpty(options)) {
return ServerCacheReplayer::Status{
ServerCacheReplayer::StatusCode::kEmpty,
"there were no nodes with autofill query content for autofill server "
"domains in JSON"};
}
return ServerCacheReplayer::Status{ServerCacheReplayer::StatusCode::kOk, ""};
}
} // namespace
// Decompressed HTTP response read from WPR capture file. Will set
// |decompressed_http| to "" and return false if there is an error.
bool ServerCacheReplayer::RetrieveAndDecompressStoredHTTP(
const std::string& key,
std::string* decompressed_http) const {
// Safe to use at() here since we looked for key's presence and there is no
// mutation done when there is concurrency.
const std::string& http_text = const_cache_.at(key);
auto header_and_body = SplitHTTP(http_text);
if (header_and_body.first == "") {
*decompressed_http = "";
VLOG(1) << "No header found in supposed HTTP text: " << http_text;
return false;
}
// Look if there is a body to decompress, if not just return HTTP text as is.
if (header_and_body.second == "") {
*decompressed_http = http_text;
VLOG(1) << "There is no HTTP body to decompress: " << http_text;
return true;
}
// TODO(crbug.com/945925): Add compression format detection, return an
// error if not supported format.
// Decompress the body.
std::string decompressed_body;
if (!compression::GzipUncompress(header_and_body.second,
&decompressed_body)) {
VLOG(1) << "Could not gzip decompress HTTP response: "
<< GetHexString(header_and_body.second);
return false;
}
// Rebuild the response HTTP text by using the new decompressed body.
*decompressed_http =
MakeHTTPTextFromSplit(header_and_body.first, decompressed_body);
return true;
}
// Determines the Autofill Server Behavior from command line parameter.
AutofillServerBehaviorType ParseAutofillServerBehaviorType() {
std::string autofill_server_option =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kAutofillServerBehaviorParam);
VLOG(1) << "Autofill Server Behavior was:`" << autofill_server_option << "`.";
if (autofill_server_option.empty() ||
base::EqualsCaseInsensitiveASCII(autofill_server_option, "SavedCache")) {
return AutofillServerBehaviorType::kSavedCache;
} else if (base::EqualsCaseInsensitiveASCII(autofill_server_option,
"ProductionServer")) {
return AutofillServerBehaviorType::kProductionServer;
} else if (base::EqualsCaseInsensitiveASCII(autofill_server_option,
"OnlyLocalHeuristics")) {
return AutofillServerBehaviorType::kOnlyLocalHeuristics;
} else {
CHECK(false) << "Unrecognized command line value give for `"
<< kAutofillServerBehaviorParam << "` argument: `"
<< autofill_server_option << "`";
return AutofillServerBehaviorType::kSavedCache;
}
}
// Gives a pair that contains the HTTP text split in 2, where the first
// element is the HTTP head and the second element is the HTTP body.
std::pair<std::string, std::string> SplitHTTP(const std::string& http_text) {
const size_t split_index = http_text.find(kHTTPBodySep);
if (split_index != std::string::npos) {
const size_t sep_length = std::string(kHTTPBodySep).size();
std::string head = http_text.substr(0, split_index);
std::string body =
http_text.substr(split_index + sep_length, std::string::npos);
return std::make_pair(std::move(head), std::move(body));
}
return std::make_pair("", "");
}
// Streams in text format. For consistency, taken from anonymous namespace in
// components/autofill/core/browser/autofill_download_manager.cc
std::ostream& operator<<(std::ostream& out,
const autofill::AutofillQueryContents& query) {
out << "client_version: " << query.client_version();
for (const auto& form : query.form()) {
out << "\nForm\n signature: " << form.signature();
for (const auto& field : form.field()) {
out << "\n Field\n signature: " << field.signature();
if (!field.name().empty())
out << "\n name: " << field.name();
if (!field.type().empty())
out << "\n type: " << field.type();
}
}
return out;
}
// Streams in text format. For consistency, taken from anonymous namespace in
// components/autofill/core/browser/form_structure.cc
std::ostream& operator<<(
std::ostream& out,
const autofill::AutofillQueryResponseContents& response) {
for (const auto& field : response.field()) {
out << "\nautofill_type: " << field.overall_type_prediction();
}
return out;
}
// Gets a key for cache lookup from a query request.
std::string GetKeyFromQueryRequest(const AutofillQueryContents& query_request) {
std::vector<std::string> form_ids;
for (const auto& form : query_request.form()) {
form_ids.push_back(base::NumberToString(form.signature()));
}
std::sort(form_ids.begin(), form_ids.end());
return base::JoinString(form_ids, "_");
}
ServerCacheReplayer::~ServerCacheReplayer() {}
ServerCacheReplayer::ServerCacheReplayer(const base::FilePath& json_file_path,
int options)
: split_requests_by_form_(SplitRequestsByForm(options)) {
// If the behavior type is not cache, we can skip setup.
if (test::ParseAutofillServerBehaviorType() !=
AutofillServerBehaviorType::kSavedCache)
return;
// Using CHECK is fine here since ServerCacheReplayer will only be used for
// testing and we prefer the test to crash than being in an inconsistent state
// when the cache could not be properly populated from the JSON file.
ServerCacheReplayer::Status status =
PopulateCacheFromJSONFile(json_file_path, options, &cache_);
CHECK(status.Ok()) << status.message;
}
ServerCacheReplayer::ServerCacheReplayer(ServerCache server_cache,
bool split_requests_by_form)
: cache_(std::move(server_cache)),
split_requests_by_form_(split_requests_by_form) {}
bool ServerCacheReplayer::GetResponseForQuery(
const AutofillQueryContents& query,
std::string* http_text) const {
if (http_text == nullptr) {
VLOG(1) << "Cannot fill |http_text| because null";
return false;
}
std::string combined_key = GetKeyFromQueryRequest(query);
if (base::Contains(const_cache_, combined_key)) {
VLOG(1) << "Retrieving response for " << combined_key;
std::string decompressed_http_response;
if (!RetrieveAndDecompressStoredHTTP(combined_key,
&decompressed_http_response)) {
return false;
}
*http_text = decompressed_http_response;
return true;
}
// If we didn't find a single-form match and we're not splitting requests by
// form, we failed to find a response for this query.
if (!split_requests_by_form_) {
VLOG(1) << "Did not match any response for " << combined_key;
return false;
}
AutofillQueryResponseContents combined_form_response;
std::string response_header_text;
bool first_loop = true;
for (const auto& form : query.form()) {
std::string key = base::NumberToString(form.signature());
if (!base::Contains(const_cache_, key)) {
VLOG(2) << "Stubbing in fields for uncached key `" << key << "`.";
for (int i = 0; i < form.field_size(); i++) {
AutofillQueryResponseContents::Field* new_field =
combined_form_response.add_field();
new_field->set_overall_type_prediction(0);
}
continue;
}
std::string decompressed_http_response;
if (!RetrieveAndDecompressStoredHTTP(key, &decompressed_http_response)) {
return false;
}
if (first_loop) {
response_header_text = SplitHTTP(decompressed_http_response).first;
first_loop = false;
}
StatusOr<AutofillQueryResponseContents> single_form_response =
ParseProtoContents<AutofillQueryResponseContents>(
SplitHTTP(decompressed_http_response).second);
if (!single_form_response.ok()) {
VLOG(1) << "Unable to parse result contents for key:" << key;
return false;
}
for (auto& field : single_form_response.ValueOrDie().field()) {
combined_form_response.add_field()->CopyFrom(field);
}
}
// If all we got were stubbed forms, return false as not a single match.
if (first_loop) {
VLOG(1) << "Did not match any response for " << combined_key;
return false;
}
std::string compressed_response;
if (!combined_form_response.SerializeToString(&compressed_response)) {
VLOG(1) << "Unable to serialize the new response for keys!";
return false;
}
VLOG(1) << "Retrieving stitched response for " << combined_key;
*http_text = MakeHTTPTextFromSplit(response_header_text, compressed_response);
return true;
}
ServerUrlLoader::ServerUrlLoader(
std::unique_ptr<ServerCacheReplayer> cache_replayer)
: cache_replayer_(std::move(cache_replayer)),
autofill_server_behavior_type_(ParseAutofillServerBehaviorType()),
interceptor_(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) -> bool {
return InterceptAutofillRequest(params);
})) {
// Using CHECK is fine here since ServerCacheReplayer will only be used for
// testing and we prefer the test to crash with a CHECK rather than
// segfaulting with a stack trace that can be hard to read.
CHECK(cache_replayer_);
}
ServerUrlLoader::~ServerUrlLoader() {}
bool WriteNotFoundResponse(
content::URLLoaderInterceptor::RequestParams* params) {
// Give back 404 error to the server if there is not match in cache.
constexpr char kNoKeyMatchHTTPErrorHeaders[] = "HTTP/2.0 404 Not Found";
constexpr char kNoKeyMatchHTTPErrorBody[] =
"could not find response matching request";
VLOG(1) << "Served Autofill error response: " << kNoKeyMatchHTTPErrorBody;
content::URLLoaderInterceptor::WriteResponse(
std::string(kNoKeyMatchHTTPErrorHeaders),
std::string(kNoKeyMatchHTTPErrorBody), params->client.get());
return true;
}
bool ServerUrlLoader::InterceptAutofillRequest(
content::URLLoaderInterceptor::RequestParams* params) {
static const char kDefaultAutofillServerQueryURL[] =
"https://content-autofill.googleapis.com/query";
const network::ResourceRequest& resource_request = params->url_request;
base::StringPiece request_url = resource_request.url.spec();
// Let all requests that are not autofill queries go to WPR.
if (request_url.find(kDefaultAutofillServerQueryURL) == std::string::npos) {
return false;
}
// Check what the set behavior type is.
// For Production Server, return false to say don't intercept.
// For Only Local Heuristics, write empty server response.
// For Saved Cache, continue on and look for a response in the cache.
switch (autofill_server_behavior_type_) {
case AutofillServerBehaviorType::kProductionServer:
return false;
case AutofillServerBehaviorType::kOnlyLocalHeuristics:
return WriteNotFoundResponse(params);
case AutofillServerBehaviorType::kSavedCache:
default:
break;
}
// Intercept autofill query and serve back response from cache.
// Parse HTTP request body to proto.
VLOG(1) << "Intercepted in-flight request to Autofill Server: "
<< resource_request.url.spec();
bool is_post_request =
GetRequestTypeFromURL(request_url) == RequestType::kLegacyQueryProtoPOST;
// Look if the body has data if it is a POST request.
if (is_post_request && resource_request.request_body == nullptr) {
constexpr char kNoBodyHTTPErrorHeaders[] = "HTTP/2.0 400 Bad Request";
constexpr char kNoBodyHTTPErrorBody[] =
"there is no body data in the request";
VLOG(1) << "Served Autofill error response: " << kNoBodyHTTPErrorBody;
content::URLLoaderInterceptor::WriteResponse(
std::string(kNoBodyHTTPErrorHeaders), std::string(kNoBodyHTTPErrorBody),
params->client.get());
return true;
}
StatusOr<AutofillQueryContents> query_request_statusor =
is_post_request
? GetAutofillQueryContentsFromPOSTQuery(resource_request)
: GetAutofillQueryContentsFromGETQueryURL(resource_request.url);
// Using CHECK is fine here since ServerCacheReplayer will only be used for
// testing and we prefer the test to crash rather than missing the cache
// because the request content could not be parsed back to a Query request
// proto, which can be caused by bad data in the request from the browser
// during capture replay.
CHECK(query_request_statusor.ok()) << query_request_statusor.status();
// Get response from cache using query request proto as key.
std::string http_response;
if (!cache_replayer_->GetResponseForQuery(query_request_statusor.ValueOrDie(),
&http_response)) {
return WriteNotFoundResponse(params);
}
// Give back cache response HTTP content.
auto http_pair = SplitHTTP(http_response);
content::URLLoaderInterceptor::WriteResponse(
http_pair.first, http_pair.second, params->client.get());
VLOG(1) << "Giving back response from cache";
return true;
}
} // namespace test
} // namespace autofill