// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine License // Agreement. #include "url_request.h" #include #include #include #include #include #include "http_socket.h" #include "log.h" #include "string_conversions.h" namespace wvcdm { namespace { const int kMaxConnectAttempts = 3; const int kReadBufferSize = 1024; const int kConnectTimeoutMs = 15000; const int kWriteTimeoutMs = 12000; const int kReadTimeoutMs = 12000; constexpr int kHttpOk = 200; const std::vector kRetryCodes = {502, 504}; const std::string kGoogleHeaderUpper("X-Google"); const std::string kGoogleHeaderLower("x-google"); const std::string kCrLf("\r\n"); constexpr unsigned kRetryCount = 3; constexpr unsigned kRetryIntervalSeconds = 1; // Concatenate all chunks into one blob and returns the response with // header information. void ConcatenateChunkedResponse(const std::string http_response, std::string* modified_response) { if (http_response.empty()) return; modified_response->clear(); const std::string kChunkedTag = "Transfer-Encoding: chunked\r\n\r\n"; size_t chunked_tag_pos = http_response.find(kChunkedTag); if (std::string::npos != chunked_tag_pos) { // processes chunked encoding size_t chunk_size = 0; size_t chunk_size_pos = chunked_tag_pos + kChunkedTag.size(); sscanf(&http_response[chunk_size_pos], "%zx", &chunk_size); if (chunk_size > http_response.size()) { // precaution, in case we misread chunk size LOGE("Invalid chunk size %zu", chunk_size); return; } // Search for chunks in the following format: // header // chunk size\r\n <-- chunk_size_pos @ beginning of chunk size // chunk data\r\n <-- chunk_pos @ beginning of chunk data // chunk size\r\n // chunk data\r\n // 0\r\n size_t chunk_pos = http_response.find(kCrLf, chunk_size_pos); modified_response->assign(http_response, 0, chunk_size_pos); while ((chunk_size > 0) && (std::string::npos != chunk_pos)) { chunk_pos += kCrLf.size(); modified_response->append(http_response, chunk_pos, chunk_size); // Search for next chunk chunk_size_pos = chunk_pos + chunk_size + kCrLf.size(); sscanf(&http_response[chunk_size_pos], "%zx", &chunk_size); if (chunk_size > http_response.size()) { // precaution, in case we misread chunk size LOGE("Invalid chunk size %zu", chunk_size); break; } chunk_pos = http_response.find(kCrLf, chunk_size_pos); } } else { // Response is not chunked encoded modified_response->assign(http_response); } } } // namespace UrlRequest::UrlRequest(const std::string& url) : is_connected_(false), socket_(url) { Reconnect(); } UrlRequest::~UrlRequest() {} void UrlRequest::Reconnect() { for (uint32_t i = 0; i < kMaxConnectAttempts && !is_connected_; ++i) { socket_.CloseSocket(); if (socket_.ConnectAndLogErrors(kConnectTimeoutMs)) { is_connected_ = true; } else { LOGE("Failed to connect: url = %s, port = %d, attempt = %u", socket_.url().c_str(), socket_.port(), i); } } } bool UrlRequest::GetResponse(std::string* message) { std::string response; // Keep reading until end of stream (0 bytes read) or timeout. Partial // buffers worth of data can and do happen, especially with OpenSSL in // non-blocking mode. while (true) { char read_buffer[kReadBufferSize]; const int bytes = socket_.ReadAndLogErrors(read_buffer, sizeof(read_buffer), kReadTimeoutMs); if (bytes > 0) { response.append(read_buffer, bytes); } else if (bytes < 0) { LOGE("Read error, errno = %d", errno); return false; } else { // end of stream. break; } } ConcatenateChunkedResponse(std::move(response), message); LOGV("HTTP response from %s: (%zu): %s", socket_.url().c_str(), message->size(), message->c_str()); return true; } void UrlRequest::AssertOkResponseWithRetry(std::string* message) { ASSERT_TRUE(message); int status_code = 0; for (unsigned i = 0; i < kRetryCount; i++) { *message = ""; ASSERT_TRUE(GetResponse(message)) << "For attempt " << (i + 1); status_code = GetStatusCode(*message); // If we didn't get a retry status, then we're done. if (std::find(kRetryCodes.begin(), kRetryCodes.end(), status_code) == kRetryCodes.end()) { ASSERT_EQ(kHttpOk, status_code) << "HTTP response from " << socket_.url() << ": (" << message->size() << ") :\n" << *message; return; } std::cerr << "Temporary failure HTTP response from " << socket_.url() << ": (" << message->size() << ") :\n" << *message << "\n" << "Attempt " << (i + 1) << "\n"; socket_.CloseSocket(); is_connected_ = false; sleep(kRetryIntervalSeconds << i); Reconnect(); SendRequestOnce(); } GTEST_FAIL() << "HTTP response from " << socket_.url() << ": (" << message->size() << ") :\n" << *message; } // static int UrlRequest::GetStatusCode(const std::string& response) { const std::string kHttpVersion("HTTP/1.1 "); int status_code = -1; size_t pos = response.find(kHttpVersion); if (pos != std::string::npos) { pos += kHttpVersion.size(); sscanf(response.substr(pos).c_str(), "%d", &status_code); } 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) { request_.clear(); request_.append("POST "); request_.append(path); request_.append(" HTTP/1.1\r\n"); request_.append("Host: "); request_.append(socket_.domain_name()); request_.append("\r\n"); request_.append("Connection: close\r\n"); request_.append("User-Agent: Widevine CDM v1.0\r\n"); request_.append("X-Return-Encrypted-Headers: request_and_response\r\n"); request_.append("Content-Length: "); request_.append(std::to_string(data.size())); request_.append("\r\n"); request_.append("\r\n"); // empty line to terminate headers request_.append(data); return SendRequestOnce(); } bool UrlRequest::SendRequestOnce() { const int ret = socket_.WriteAndLogErrors( request_.c_str(), static_cast(request_.size()), kWriteTimeoutMs); LOGV("HTTP request: (%zu): %s", request_.size(), request_.c_str()); LOGV("HTTP request hex: %s", wvutil::b2a_hex(request_).c_str()); return ret != -1; } bool UrlRequest::PostRequest(const std::string& data) { return PostRequestWithPath(socket_.resource_path(), data); } bool UrlRequest::PostCertRequestInQueryString(const std::string& data) { std::string path = socket_.resource_path(); path.append((path.find('?') == std::string::npos) ? "?" : "&"); path.append("signedRequest="); path.append(data); return PostRequestWithPath(path, ""); } } // namespace wvcdm