Shaka Packager SDK
http_file.cc
1 // Copyright 2020 Google LLC. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file or at
5 // https://developers.google.com/open-source/licenses/bsd
6 
7 #include "packager/file/http_file.h"
8 
9 #include <curl/curl.h>
10 #include <gflags/gflags.h>
11 
12 #include "packager/base/bind.h"
13 #include "packager/base/files/file_util.h"
14 #include "packager/base/logging.h"
15 #include "packager/base/strings/string_number_conversions.h"
16 #include "packager/base/strings/stringprintf.h"
17 #include "packager/base/threading/worker_pool.h"
18 #include "packager/version/version.h"
19 
20 DEFINE_string(user_agent, "",
21  "Set a custom User-Agent string for HTTP requests.");
22 DEFINE_string(ca_file,
23  "",
24  "Absolute path to the Certificate Authority file for the "
25  "server cert. PEM format");
26 DEFINE_string(client_cert_file,
27  "",
28  "Absolute path to client certificate file.");
29 DEFINE_string(client_cert_private_key_file,
30  "",
31  "Absolute path to the Private Key file.");
32 DEFINE_string(client_cert_private_key_password,
33  "",
34  "Password to the private key file.");
35 DEFINE_bool(disable_peer_verification,
36  false,
37  "Disable peer verification. This is needed to talk to servers "
38  "without valid certificates.");
39 DECLARE_uint64(io_cache_size);
40 
41 namespace shaka {
42 
43 namespace {
44 
45 constexpr const char* kBinaryContentType = "application/octet-stream";
46 constexpr const int kMinLogLevelForCurlDebugFunction = 2;
47 
48 size_t CurlWriteCallback(char* buffer, size_t size, size_t nmemb, void* user) {
49  IoCache* cache = reinterpret_cast<IoCache*>(user);
50  size_t length = size * nmemb;
51  if (cache) {
52  length = cache->Write(buffer, length);
53  VLOG(3) << "CurlWriteCallback length=" << length;
54  } else {
55  // For the case of HTTP Put, the returned data may not be consumed. Return
56  // the size of the data to avoid curl errors.
57  }
58  return length;
59 }
60 
61 size_t CurlReadCallback(char* buffer, size_t size, size_t nitems, void* user) {
62  IoCache* cache = reinterpret_cast<IoCache*>(user);
63  size_t length = cache->Read(buffer, size * nitems);
64  VLOG(3) << "CurlRead length=" << length;
65  return length;
66 }
67 
68 int CurlDebugCallback(CURL* /* handle */,
69  curl_infotype type,
70  const char* data,
71  size_t size,
72  void* /* userptr */) {
73  const char* type_text;
74  int log_level;
75  bool in_hex;
76  switch (type) {
77  case CURLINFO_TEXT:
78  type_text = "== Info";
79  log_level = kMinLogLevelForCurlDebugFunction + 1;
80  in_hex = false;
81  break;
82  case CURLINFO_HEADER_IN:
83  type_text = "<= Recv header";
84  log_level = kMinLogLevelForCurlDebugFunction;
85  in_hex = false;
86  break;
87  case CURLINFO_HEADER_OUT:
88  type_text = "=> Send header";
89  log_level = kMinLogLevelForCurlDebugFunction;
90  in_hex = false;
91  break;
92  case CURLINFO_DATA_IN:
93  type_text = "<= Recv data";
94  log_level = kMinLogLevelForCurlDebugFunction + 1;
95  in_hex = true;
96  break;
97  case CURLINFO_DATA_OUT:
98  type_text = "=> Send data";
99  log_level = kMinLogLevelForCurlDebugFunction + 1;
100  in_hex = true;
101  break;
102  case CURLINFO_SSL_DATA_IN:
103  type_text = "<= Recv SSL data";
104  log_level = kMinLogLevelForCurlDebugFunction + 2;
105  in_hex = true;
106  break;
107  case CURLINFO_SSL_DATA_OUT:
108  type_text = "=> Send SSL data";
109  log_level = kMinLogLevelForCurlDebugFunction + 2;
110  in_hex = true;
111  break;
112  default:
113  // Ignore other debug data.
114  return 0;
115  }
116 
117  VLOG(log_level) << "\n\n"
118  << type_text << " (0x" << std::hex << size << std::dec
119  << " bytes)\n"
120  << (in_hex ? base::HexEncode(data, size)
121  : std::string(data, size));
122  return 0;
123 }
124 
125 class LibCurlInitializer {
126  public:
127  LibCurlInitializer() {
128  curl_global_init(CURL_GLOBAL_DEFAULT);
129  }
130 
131  ~LibCurlInitializer() {
132  curl_global_cleanup();
133  }
134 
135  LibCurlInitializer(const LibCurlInitializer&) = delete;
136  LibCurlInitializer& operator=(const LibCurlInitializer&) = delete;
137 };
138 
139 template <typename List>
140 bool AppendHeader(const std::string& header, List* list) {
141  auto* temp = curl_slist_append(list->get(), header.c_str());
142  if (temp) {
143  list->release(); // Don't free old list since it's part of the new one.
144  list->reset(temp);
145  return true;
146  } else {
147  return false;
148  }
149 }
150 
151 } // namespace
152 
153 HttpFile::HttpFile(HttpMethod method, const std::string& url)
154  : HttpFile(method, url, kBinaryContentType, {}, 0) {}
155 
156 HttpFile::HttpFile(HttpMethod method,
157  const std::string& url,
158  const std::string& upload_content_type,
159  const std::vector<std::string>& headers,
160  uint32_t timeout_in_seconds)
161  : File(url.c_str()),
162  url_(url),
163  upload_content_type_(upload_content_type),
164  timeout_in_seconds_(timeout_in_seconds),
165  method_(method),
166  download_cache_(FLAGS_io_cache_size),
167  upload_cache_(FLAGS_io_cache_size),
168  curl_(curl_easy_init()),
169  status_(Status::OK),
170  user_agent_(FLAGS_user_agent),
171  task_exit_event_(base::WaitableEvent::ResetPolicy::MANUAL,
172  base::WaitableEvent::InitialState::NOT_SIGNALED) {
173  static LibCurlInitializer lib_curl_initializer;
174  if (user_agent_.empty()) {
175  user_agent_ += "ShakaPackager/" + GetPackagerVersion();
176  }
177 
178  // We will have at least one header, so use a null header to signal error
179  // to Open.
180 
181  // Don't wait for 100-Continue.
182  std::unique_ptr<curl_slist, CurlDelete> temp_headers;
183  if (!AppendHeader("Expect:", &temp_headers))
184  return;
185  if (!upload_content_type.empty() &&
186  !AppendHeader("Content-Type: " + upload_content_type_, &temp_headers)) {
187  return;
188  }
189  if (method != HttpMethod::kGet &&
190  !AppendHeader("Transfer-Encoding: chunked", &temp_headers)) {
191  return;
192  }
193  for (const auto& item : headers) {
194  if (!AppendHeader(item, &temp_headers)) {
195  return;
196  }
197  }
198  request_headers_ = std::move(temp_headers);
199 }
200 
201 HttpFile::~HttpFile() {}
202 
203 bool HttpFile::Open() {
204  VLOG(2) << "Opening " << url_;
205 
206  if (!curl_ || !request_headers_) {
207  LOG(ERROR) << "curl_easy_init() failed.";
208  return false;
209  }
210  // TODO: Try to connect initially so we can return connection error here.
211 
212  // TODO: Implement retrying with exponential backoff, see
213  // "widevine_key_source.cc"
214 
215  base::WorkerPool::PostTask(
216  FROM_HERE, base::Bind(&HttpFile::ThreadMain, base::Unretained(this)),
217  /* task_is_slow= */ true);
218 
219  return true;
220 }
221 
222 Status HttpFile::CloseWithStatus() {
223  VLOG(2) << "Closing " << url_;
224  // Close the cache first so the thread will finish uploading. Otherwise it
225  // will wait for more data forever.
226  download_cache_.Close();
227  upload_cache_.Close();
228  task_exit_event_.Wait();
229 
230  const Status result = status_;
231  LOG_IF(ERROR, !result.ok()) << "HttpFile request failed: " << result;
232  delete this;
233  return result;
234 }
235 
236 bool HttpFile::Close() {
237  return CloseWithStatus().ok();
238 }
239 
240 int64_t HttpFile::Read(void* buffer, uint64_t length) {
241  VLOG(2) << "Reading from " << url_ << ", length=" << length;
242  return download_cache_.Read(buffer, length);
243 }
244 
245 int64_t HttpFile::Write(const void* buffer, uint64_t length) {
246  VLOG(2) << "Writing to " << url_ << ", length=" << length;
247  return upload_cache_.Write(buffer, length);
248 }
249 
250 int64_t HttpFile::Size() {
251  VLOG(1) << "HttpFile does not support Size().";
252  return -1;
253 }
254 
255 bool HttpFile::Flush() {
256  upload_cache_.Close();
257  return true;
258 }
259 
260 bool HttpFile::Seek(uint64_t position) {
261  LOG(ERROR) << "HttpFile does not support Seek().";
262  return false;
263 }
264 
265 bool HttpFile::Tell(uint64_t* position) {
266  LOG(ERROR) << "HttpFile does not support Tell().";
267  return false;
268 }
269 
270 void HttpFile::CurlDelete::operator()(CURL* curl) {
271  curl_easy_cleanup(curl);
272 }
273 
274 void HttpFile::CurlDelete::operator()(curl_slist* headers) {
275  curl_slist_free_all(headers);
276 }
277 
278 void HttpFile::SetupRequest() {
279  auto* curl = curl_.get();
280 
281  switch (method_) {
282  case HttpMethod::kGet:
283  curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
284  break;
285  case HttpMethod::kPost:
286  curl_easy_setopt(curl, CURLOPT_POST, 1L);
287  break;
288  case HttpMethod::kPut:
289  curl_easy_setopt(curl, CURLOPT_PUT, 1L);
290  break;
291  }
292 
293  curl_easy_setopt(curl, CURLOPT_URL, url_.c_str());
294  curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent_.c_str());
295  curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_in_seconds_);
296  curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
297  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
298  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
299  curl_easy_setopt(curl, CURLOPT_WRITEDATA,
300  method_ == HttpMethod::kPut ? nullptr : &download_cache_);
301  if (method_ != HttpMethod::kGet) {
302  curl_easy_setopt(curl, CURLOPT_READFUNCTION, &CurlReadCallback);
303  curl_easy_setopt(curl, CURLOPT_READDATA, &upload_cache_);
304  }
305 
306  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, request_headers_.get());
307 
308  if (FLAGS_disable_peer_verification)
309  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
310 
311  // Client authentication
312  if (!FLAGS_client_cert_private_key_file.empty() &&
313  !FLAGS_client_cert_file.empty()) {
314  curl_easy_setopt(curl, CURLOPT_SSLKEY,
315  FLAGS_client_cert_private_key_file.data());
316  curl_easy_setopt(curl, CURLOPT_SSLCERT, FLAGS_client_cert_file.data());
317  curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
318  curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "PEM");
319 
320  if (!FLAGS_client_cert_private_key_password.empty()) {
321  curl_easy_setopt(curl, CURLOPT_KEYPASSWD,
322  FLAGS_client_cert_private_key_password.data());
323  }
324  }
325  if (!FLAGS_ca_file.empty()) {
326  curl_easy_setopt(curl, CURLOPT_CAINFO, FLAGS_ca_file.data());
327  }
328 
329  if (VLOG_IS_ON(kMinLogLevelForCurlDebugFunction)) {
330  curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, CurlDebugCallback);
331  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
332  }
333 }
334 
335 void HttpFile::ThreadMain() {
336  SetupRequest();
337 
338  CURLcode res = curl_easy_perform(curl_.get());
339  if (res != CURLE_OK) {
340  std::string error_message = curl_easy_strerror(res);
341  if (res == CURLE_HTTP_RETURNED_ERROR) {
342  long response_code = 0;
343  curl_easy_getinfo(curl_.get(), CURLINFO_RESPONSE_CODE, &response_code);
344  error_message +=
345  base::StringPrintf(", response code: %ld.", response_code);
346  }
347 
348  status_ = Status(
349  res == CURLE_OPERATION_TIMEDOUT ? error::TIME_OUT : error::HTTP_FAILURE,
350  error_message);
351  }
352 
353  download_cache_.Close();
354  task_exit_event_.Signal();
355 }
356 
357 } // namespace shaka
All the methods that are virtual are virtual for mocking.