1// Copyright 2015 The Weave Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "examples/provider/curl_http_client.h"
6
7#include <algorithm>
8#include <future>
9#include <thread>
10
11#include <base/bind.h>
12#include <base/logging.h>
13#include <curl/curl.h>
14#include <weave/enum_to_string.h>
15#include <weave/provider/task_runner.h>
16
17namespace weave {
18namespace examples {
19
20namespace {
21
22struct ResponseImpl : public provider::HttpClient::Response {
23  int GetStatusCode() const override { return status; }
24  std::string GetContentType() const override { return content_type; }
25  std::string GetData() const override { return data; }
26
27  long status{0};
28  std::string content_type;
29  std::string data;
30};
31
32size_t WriteFunction(void* contents, size_t size, size_t nmemb, void* userp) {
33  static_cast<std::string*>(userp)->append(static_cast<const char*>(contents),
34                                           size * nmemb);
35  return size * nmemb;
36}
37
38size_t HeaderFunction(void* contents, size_t size, size_t nmemb, void* userp) {
39  std::string header(static_cast<const char*>(contents), size * nmemb);
40  auto pos = header.find(':');
41  if (pos != std::string::npos) {
42    std::pair<std::string, std::string> header_pair;
43
44    static const char kSpaces[] = " \t\r\n";
45    header_pair.first = header.substr(0, pos);
46    pos = header.find_first_not_of(kSpaces, pos + 1);
47    if (pos != std::string::npos) {
48      auto last_non_space = header.find_last_not_of(kSpaces);
49      if (last_non_space >= pos)
50        header_pair.second = header.substr(pos, last_non_space - pos + 1);
51    }
52
53    static_cast<provider::HttpClient::Headers*>(userp)->emplace_back(
54        std::move(header_pair));
55  }
56  return size * nmemb;
57}
58
59std::pair<std::unique_ptr<CurlHttpClient::Response>, ErrorPtr>
60SendRequestBlocking(CurlHttpClient::Method method,
61                    const std::string& url,
62                    const CurlHttpClient::Headers& headers,
63                    const std::string& data) {
64  std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl{curl_easy_init(),
65                                                           &curl_easy_cleanup};
66  CHECK(curl);
67
68  switch (method) {
69    case CurlHttpClient::Method::kGet:
70      CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HTTPGET, 1L));
71      break;
72    case CurlHttpClient::Method::kPost:
73      CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HTTPPOST, 1L));
74      break;
75    case CurlHttpClient::Method::kPatch:
76    case CurlHttpClient::Method::kPut:
77      CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST,
78                                          weave::EnumToString(method).c_str()));
79      break;
80  }
81
82  CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()));
83
84  curl_slist* chunk = nullptr;
85  for (const auto& h : headers)
86    chunk = curl_slist_append(chunk, (h.first + ": " + h.second).c_str());
87
88  CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, chunk));
89
90  if (!data.empty() || method == CurlHttpClient::Method::kPost) {
91    CHECK_EQ(CURLE_OK,
92             curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, data.c_str()));
93  }
94
95  std::unique_ptr<ResponseImpl> response{new ResponseImpl};
96  CHECK_EQ(CURLE_OK,
97           curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &WriteFunction));
98  CHECK_EQ(CURLE_OK,
99           curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response->data));
100  CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION,
101                                      &HeaderFunction));
102  provider::HttpClient::Headers response_headers;
103  CHECK_EQ(CURLE_OK,
104           curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &response_headers));
105
106  CURLcode res = curl_easy_perform(curl.get());
107  if (chunk)
108    curl_slist_free_all(chunk);
109
110  ErrorPtr error;
111  if (res != CURLE_OK) {
112    Error::AddTo(&error, FROM_HERE, "curl_easy_perform_error",
113                 curl_easy_strerror(res));
114    return {nullptr, std::move(error)};
115  }
116
117  for (const auto& header : response_headers) {
118    if (header.first == "Content-Type")
119      response->content_type = header.second;
120  }
121
122  CHECK_EQ(CURLE_OK, curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE,
123                                       &response->status));
124
125  return {std::move(response), nullptr};
126}
127
128}  // namespace
129
130CurlHttpClient::CurlHttpClient(provider::TaskRunner* task_runner)
131    : task_runner_{task_runner} {}
132
133void CurlHttpClient::SendRequest(Method method,
134                                 const std::string& url,
135                                 const Headers& headers,
136                                 const std::string& data,
137                                 const SendRequestCallback& callback) {
138  pending_tasks_.emplace_back(
139      std::async(std::launch::async, SendRequestBlocking, method, url, headers,
140                 data),
141      callback);
142  if (pending_tasks_.size() == 1)  // More means check is scheduled.
143    CheckTasks();
144}
145
146void CurlHttpClient::CheckTasks() {
147  VLOG(4) << "CurlHttpClient::CheckTasks, size=" << pending_tasks_.size();
148  auto ready_begin =
149      std::partition(pending_tasks_.begin(), pending_tasks_.end(),
150                     [](const decltype(pending_tasks_)::value_type& value) {
151                       return value.first.wait_for(std::chrono::seconds(0)) !=
152                              std::future_status::ready;
153                     });
154
155  for (auto it = ready_begin; it != pending_tasks_.end(); ++it) {
156    CHECK(it->first.valid());
157    auto result = it->first.get();
158    VLOG(2) << "CurlHttpClient::CheckTasks done";
159    task_runner_->PostDelayedTask(
160        FROM_HERE, base::Bind(it->second, base::Passed(&result.first),
161                              base::Passed(&result.second)),
162        {});
163  }
164
165  pending_tasks_.erase(ready_begin, pending_tasks_.end());
166
167  if (pending_tasks_.empty()) {
168    VLOG(2) << "No more CurlHttpClient tasks";
169    return;
170  }
171
172  task_runner_->PostDelayedTask(
173      FROM_HERE,
174      base::Bind(&CurlHttpClient::CheckTasks, weak_ptr_factory_.GetWeakPtr()),
175      base::TimeDelta::FromMilliseconds(100));
176}
177
178}  // namespace examples
179}  // namespace weave
180