From 147c9e0afab15f191ea24392c95f83ffc9239306 Mon Sep 17 00:00:00 2001 From: Alex Dale Date: Tue, 28 Jul 2020 13:30:42 -0700 Subject: [PATCH] Log X-Google fields on failed HTTP requests. [ Merge of http://go/wvgerrit/103395 ] To help with debugging failures in HTTP requests during unit tests, this CL adds logging for Google's debugging response header fields. These fields are of the type "X-Google-*" or "x-google-*" and provide information such as the service name, server cell, error details, and other details that can help isolate the cause of failure on the server's end. An additional unittest has been created to test the parser for the header fields. Tests that are known to exprience HTTP failures have been extended to include logs for these fields should they be present. Bug: 137619348 Test: Linux unit tests and Jenkins test Change-Id: I69959af2ba91510f345bbb02cf7ca35c3f1119da --- libwvdrmengine/cdm/core/test/url_request.cpp | 44 +++++++- libwvdrmengine/cdm/core/test/url_request.h | 6 + .../cdm/core/test/url_request_unittest.cpp | 104 ++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 libwvdrmengine/cdm/core/test/url_request_unittest.cpp diff --git a/libwvdrmengine/cdm/core/test/url_request.cpp b/libwvdrmengine/cdm/core/test/url_request.cpp index 2f89e07a..e33aed8c 100644 --- a/libwvdrmengine/cdm/core/test/url_request.cpp +++ b/libwvdrmengine/cdm/core/test/url_request.cpp @@ -21,6 +21,10 @@ const int kConnectTimeoutMs = 15000; const int kWriteTimeoutMs = 12000; const int kReadTimeoutMs = 12000; +const std::string kGoogleHeaderUpper("X-Google"); +const std::string kGoogleHeaderLower("x-google"); +const std::string kCrLf("\r\n"); + // Concatenate all chunks into one blob and returns the response with // header information. void ConcatenateChunkedResponse(const std::string http_response, @@ -48,7 +52,6 @@ void ConcatenateChunkedResponse(const std::string http_response, // chunk size\r\n // chunk data\r\n // 0\r\n - const std::string kCrLf = "\r\n"; size_t chunk_pos = http_response.find(kCrLf, chunk_size_pos); modified_response->assign(http_response, 0, chunk_size_pos); @@ -131,6 +134,45 @@ int UrlRequest::GetStatusCode(const std::string& response) { return status_code; } +// static +bool UrlRequest::GetDebugHeaderFields( + const std::string& response, std::map* fields) { + if (fields == nullptr) return false; + fields->clear(); + + const auto find_next = [&](size_t pos) -> size_t { + // Some of Google's HTTPS return header fields in lower case, + // this lambda will check for both and return the position that + // that is next or npos if none are found. + const size_t lower_pos = response.find(kGoogleHeaderLower, pos + 1); + const size_t upper_pos = response.find(kGoogleHeaderUpper, pos + 1); + if (lower_pos == std::string::npos) return upper_pos; + if (upper_pos == std::string::npos || lower_pos < upper_pos) + return lower_pos; + return upper_pos; + }; + + // Search is relatively simple, and may pick up additional matches within + // the body of the request. This is not anticiapted for the limited use + // cases of parsing provisioning/license/renewal responses. + for (size_t key_pos = find_next(0); key_pos != std::string::npos; + key_pos = find_next(key_pos)) { + const size_t end_key_pos = response.find(":", key_pos); + const size_t end_value_pos = response.find(kCrLf, key_pos); + // Skip if the colon cannot be found. Technically possible to find + // "X-Google" inside the value of a nother header field. + if (end_key_pos == std::string::npos || end_value_pos == std::string::npos) + continue; + const size_t value_pos = end_key_pos + 2; + if (end_value_pos < value_pos) continue; + const std::string key = response.substr(key_pos, end_key_pos - key_pos); + const std::string value = + response.substr(value_pos, end_value_pos - value_pos); + fields->insert({key, value}); + } + return !fields->empty(); +} + bool UrlRequest::PostRequestWithPath(const std::string& path, const std::string& data) { std::string request; diff --git a/libwvdrmengine/cdm/core/test/url_request.h b/libwvdrmengine/cdm/core/test/url_request.h index 4289598e..550894f1 100644 --- a/libwvdrmengine/cdm/core/test/url_request.h +++ b/libwvdrmengine/cdm/core/test/url_request.h @@ -5,7 +5,9 @@ #ifndef CDM_TEST_URL_REQUEST_H_ #define CDM_TEST_URL_REQUEST_H_ +#include #include + #include "disallow_copy_and_assign.h" #include "http_socket.h" @@ -27,6 +29,10 @@ class UrlRequest { bool GetResponse(std::string* message); static int GetStatusCode(const std::string& response); + static bool GetDebugHeaderFields( + const std::string& response, + std::map* header_fields); + private: bool PostRequestWithPath(const std::string& path, const std::string& data); diff --git a/libwvdrmengine/cdm/core/test/url_request_unittest.cpp b/libwvdrmengine/cdm/core/test/url_request_unittest.cpp new file mode 100644 index 00000000..18261a79 --- /dev/null +++ b/libwvdrmengine/cdm/core/test/url_request_unittest.cpp @@ -0,0 +1,104 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "url_request.h" + +#include +#include + +#include "test_base.h" + +namespace wvcdm { + +namespace { + +// A sample HTTP response which contains an HTTP header with several +// standard and non-standard header fields. The fields of interests +// those beginning with "X-Google-" or "x-google-". +const std::string kSampleResponse = + "HTTP/2 301\r\n" + "Location: https://www.google.com/\r\b" + "Content-Type: text/html; charset=UTF-8\r\n" + "X-Google-GFE-Backend-Request-Info: eid=7js14zjC48OpziR0VCF02a0\r\n" + "x-google-signals: FRAMEWORK=TEST\r\n" + "X-Google-Binary-Version: 322287520\r\n" + "X-Google-Request-Cost: 2.50\r\n" + "Date: Thu, 23 Jul 2020 22:52:55 GMT\r\n" + "Expires: Sat, 22 Aug 2020 22:52:55 GMT\r\n" + "Cache-Control: public, max-age=2592000\r\n" + "x-google-data-version: 322866295\r\n" + "Server: gws\r\n" + "X-Google-Debug-Label: /srv/alt/job/service.number/1380\r\n" + "Content-Length: 220\r\n" + "X-XSS-Protection: 0\r\n" + "X-Frame-Options: SAMEORIGIN\r\n" + "X-Google-Service: web\r\n" + "X-Google-DOS-Service-Trace: main:home\r\n" + "Alt-Svc: h3-29=\":443\"; ma=2592000,h3-27=\":443\"; " + "ma=2592000,h3-25=\":443\"; ma=2592000,h3-T050=\":443\"; " + "ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; " + "ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; " + "v=\"46,43\"\r\n" + "\r\n" + "\r\n" + "301 Moved\r\n" + "

301 Moved

\r\n" + "The document has moved\r\n" + "here.\r\n" + "\r\n\r\n"; + +// A map containing all the Google debug header fields found in +// |kSampleResponse|. +const std::map kSampleFields = { + {"X-Google-GFE-Backend-Request-Info", "eid=7js14zjC48OpziR0VCF02a0"}, + {"x-google-signals", "FRAMEWORK=TEST"}, + {"X-Google-Binary-Version", "322287520"}, + {"X-Google-Request-Cost", "2.50"}, + {"x-google-data-version", "322866295"}, + {"X-Google-Debug-Label", "/srv/alt/job/service.number/1380"}, + {"X-Google-Service", "web"}, + {"X-Google-DOS-Service-Trace", "main:home"}}; + +// A sample HTTP response which does not contain any special header fields. +const std::string kFieldlessResponse = + "HTTP/2 301\r\n" + "Location: https://www.google.com/\r\b" + "Content-Type: text/html; charset=UTF-8\r\n" + "Date: Thu, 23 Jul 2020 22:52:55 GMT\r\n" + "Content-Length: 220\r\n" + "\r\n" + "\r\n" + "301 Moved\r\n" + "

301 Moved

\r\n" + "The document has moved\r\n" + "here.\r\n" + "\r\n\r\n"; + +} // namespace + +class UrlRequestTest : public WvCdmTestBase { + protected: + void SetUp() override { WvCdmTestBase::SetUp(); } + void TearDown() override {} +}; + +TEST_F(UrlRequestTest, ParseDebugHeader) { + // Output map cannot be null. + EXPECT_FALSE(UrlRequest::GetDebugHeaderFields(kSampleResponse, nullptr)); + std::map fields; + // Expect false if no debug information can be determined. + EXPECT_FALSE(UrlRequest::GetDebugHeaderFields(kFieldlessResponse, &fields)); + // Expect success. + EXPECT_TRUE(UrlRequest::GetDebugHeaderFields(kSampleResponse, &fields)); + EXPECT_EQ(kSampleFields.size(), fields.size()); + for (auto it = kSampleFields.cbegin(); it != kSampleFields.cend(); ++it) { + auto field = fields.find(it->first); + EXPECT_NE(fields.end(), field); + EXPECT_EQ(it->second, field->second) << "Key: \"" << it->first << "\""; + } +} + +} // namespace wvcdm