265 lines
8.7 KiB
C++
265 lines
8.7 KiB
C++
// 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 <errno.h>
|
|
#include <unistd.h>
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#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<int> 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<std::string, std::string>* 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<int>(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
|