Aktualizr
C++ SOTA Client
httpclient.cc
1 #include "httpclient.h"
2 #include "utilities/utils.h"
3 
4 #include <assert.h>
5 #include <openssl/crypto.h>
6 #include <openssl/err.h>
7 #include <openssl/evp.h>
8 #include <openssl/opensslv.h>
9 #include <openssl/pem.h>
10 #include <openssl/pkcs12.h>
11 #include <openssl/rand.h>
12 #include <openssl/rsa.h>
13 #include <openssl/ssl.h>
14 
15 #include "crypto/openssl_compat.h"
16 
18  std::string out;
19  int64_t limit{0};
20 };
21 
22 /*****************************************************************************/
23 /**
24  * \par Description:
25  * A writeback handler for the curl library. It handles writing response
26  * data from curl into a string.
27  * https://curl.haxx.se/libcurl/c/CURLOPT_WRITEFUNCTION.html
28  *
29  */
30 static size_t writeString(void* contents, size_t size, size_t nmemb, void* userp) {
31  assert(contents);
32  assert(userp);
33  // append the writeback data to the provided string
34  auto* arg = static_cast<WriteStringArg*>(userp);
35  if (arg->limit > 0) {
36  if (arg->out.length() + size * nmemb > static_cast<uint64_t>(arg->limit)) {
37  return 0;
38  }
39  }
40  (static_cast<WriteStringArg*>(userp))->out.append(static_cast<char*>(contents), size * nmemb);
41 
42  // return size of written data
43  return size * nmemb;
44 }
45 
46 HttpClient::HttpClient() : user_agent(std::string("Aktualizr/") + AKTUALIZR_VERSION) {
47  curl = curl_easy_init();
48  if (curl == nullptr) {
49  throw std::runtime_error("Could not initialize curl");
50  }
51  headers = nullptr;
52  http_code = 0;
53 
54  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
55  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
56  curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 60L);
57 
58  // let curl use our write function
59  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeString);
60  curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL);
61 
62  curl_easy_setopt(curl, CURLOPT_VERBOSE, get_curlopt_verbose());
63 
64  headers = curl_slist_append(headers, "Content-Type: application/json");
65  headers = curl_slist_append(headers, "Accept: */*");
66  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
67  curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
68 }
69 
70 HttpClient::HttpClient(const HttpClient& curl_in) : pkcs11_key(curl_in.pkcs11_key), pkcs11_cert(curl_in.pkcs11_key) {
71  curl = curl_easy_duphandle(curl_in.curl);
72 
73  struct curl_slist* inlist = curl_in.headers;
74  headers = nullptr;
75  struct curl_slist* tmp;
76 
77  while (inlist != nullptr) {
78  tmp = curl_slist_append(headers, inlist->data);
79 
80  if (tmp == nullptr) {
81  curl_slist_free_all(headers);
82  throw std::runtime_error("curl_slist_append returned null");
83  }
84 
85  headers = tmp;
86  inlist = inlist->next;
87  }
88 }
89 
90 CurlGlobalInitWrapper HttpClient::manageCurlGlobalInit_{};
91 
92 HttpClient::~HttpClient() {
93  curl_slist_free_all(headers);
94  curl_easy_cleanup(curl);
95 }
96 
97 HttpResponse HttpClient::get(const std::string& url, int64_t maxsize) {
98  CURL* curl_get = curl_easy_duphandle(curl);
99 
100  // TODO: it is a workaround for an unidentified bug in libcurl. Ideally the bug itself should be fixed.
101  if (pkcs11_key) {
102  curl_easy_setopt(curl_get, CURLOPT_SSLENGINE, "pkcs11");
103  curl_easy_setopt(curl_get, CURLOPT_SSLKEYTYPE, "ENG");
104  }
105 
106  if (pkcs11_cert) {
107  curl_easy_setopt(curl_get, CURLOPT_SSLCERTTYPE, "ENG");
108  }
109 
110  // Clear POSTFIELDS to remove any lingering references to strings that have
111  // probably since been deallocated.
112  curl_easy_setopt(curl_get, CURLOPT_POSTFIELDS, "");
113  curl_easy_setopt(curl_get, CURLOPT_URL, url.c_str());
114  curl_easy_setopt(curl_get, CURLOPT_HTTPGET, 1L);
115  if (maxsize >= 0) {
116  // it will only take effect if the server declares the size in advance,
117  // writeString callback takes care of the other case
118  curl_easy_setopt(curl_get, CURLOPT_MAXFILESIZE_LARGE, maxsize);
119  }
120  curl_easy_setopt(curl_get, CURLOPT_LOW_SPEED_TIME, speed_limit_time_interval_);
121  curl_easy_setopt(curl_get, CURLOPT_LOW_SPEED_LIMIT, speed_limit_bytes_per_sec_);
122  LOG_DEBUG << "GET " << url;
123  HttpResponse response = perform(curl_get, RETRY_TIMES, maxsize);
124  curl_easy_cleanup(curl_get);
125  return response;
126 }
127 
128 void HttpClient::setCerts(const std::string& ca, CryptoSource ca_source, const std::string& cert,
129  CryptoSource cert_source, const std::string& pkey, CryptoSource pkey_source) {
130  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
131  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2);
132  curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL);
133 
134  if (ca_source == CryptoSource::kPkcs11) {
135  throw std::runtime_error("Accessing CA certificate on PKCS11 devices isn't currently supported");
136  }
137  std::unique_ptr<TemporaryFile> tmp_ca_file = std_::make_unique<TemporaryFile>("tls-ca");
138  tmp_ca_file->PutContents(ca);
139  curl_easy_setopt(curl, CURLOPT_CAINFO, tmp_ca_file->Path().c_str());
140  tls_ca_file = std::move_if_noexcept(tmp_ca_file);
141 
142  if (cert_source == CryptoSource::kPkcs11) {
143  curl_easy_setopt(curl, CURLOPT_SSLCERT, cert.c_str());
144  curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "ENG");
145  } else { // cert_source == CryptoSource::kFile
146  std::unique_ptr<TemporaryFile> tmp_cert_file = std_::make_unique<TemporaryFile>("tls-cert");
147  tmp_cert_file->PutContents(cert);
148  curl_easy_setopt(curl, CURLOPT_SSLCERT, tmp_cert_file->Path().c_str());
149  curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "PEM");
150  tls_cert_file = std::move_if_noexcept(tmp_cert_file);
151  }
152  pkcs11_cert = (cert_source == CryptoSource::kPkcs11);
153 
154  if (pkey_source == CryptoSource::kPkcs11) {
155  curl_easy_setopt(curl, CURLOPT_SSLENGINE, "pkcs11");
156  curl_easy_setopt(curl, CURLOPT_SSLENGINE_DEFAULT, 1L);
157  curl_easy_setopt(curl, CURLOPT_SSLKEY, pkey.c_str());
158  curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "ENG");
159  } else { // pkey_source == CryptoSource::kFile
160  std::unique_ptr<TemporaryFile> tmp_pkey_file = std_::make_unique<TemporaryFile>("tls-pkey");
161  tmp_pkey_file->PutContents(pkey);
162  curl_easy_setopt(curl, CURLOPT_SSLKEY, tmp_pkey_file->Path().c_str());
163  curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
164  tls_pkey_file = std::move_if_noexcept(tmp_pkey_file);
165  }
166  pkcs11_key = (pkey_source == CryptoSource::kPkcs11);
167 }
168 
169 HttpResponse HttpClient::post(const std::string& url, const Json::Value& data) {
170  curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
171  curl_easy_setopt(curl, CURLOPT_POST, 1);
172  std::string data_str = Json::FastWriter().write(data);
173  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data_str.c_str());
174  LOG_TRACE << "post request body:" << data;
175  return perform(curl, RETRY_TIMES, HttpInterface::kPostRespLimit);
176 }
177 
178 HttpResponse HttpClient::put(const std::string& url, const Json::Value& data) {
179  CURL* curl_put = curl_easy_duphandle(curl);
180 
181  // TODO: it is a workaround for an unidentified bug in libcurl. Ideally the bug itself should be fixed.
182  if (pkcs11_key) {
183  curl_easy_setopt(curl_put, CURLOPT_SSLENGINE, "pkcs11");
184  curl_easy_setopt(curl_put, CURLOPT_SSLKEYTYPE, "ENG");
185  }
186 
187  if (pkcs11_cert) {
188  curl_easy_setopt(curl_put, CURLOPT_SSLCERTTYPE, "ENG");
189  }
190 
191  curl_easy_setopt(curl_put, CURLOPT_URL, url.c_str());
192  std::string data_str = Json::FastWriter().write(data);
193  curl_easy_setopt(curl_put, CURLOPT_POSTFIELDS, data_str.c_str());
194  curl_easy_setopt(curl_put, CURLOPT_CUSTOMREQUEST, "PUT");
195  LOG_TRACE << "put request body:" << data;
196  HttpResponse result = perform(curl_put, RETRY_TIMES, HttpInterface::kPutRespLimit);
197  curl_easy_cleanup(curl_put);
198  return result;
199 }
200 
201 HttpResponse HttpClient::perform(CURL* curl_handler, int retry_times, int64_t size_limit) {
202  WriteStringArg response_arg;
203  response_arg.limit = size_limit;
204  curl_easy_setopt(curl_handler, CURLOPT_WRITEDATA, (void*)&response_arg);
205  CURLcode result = curl_easy_perform(curl_handler);
206  curl_easy_getinfo(curl_handler, CURLINFO_RESPONSE_CODE, &http_code);
207  HttpResponse response(response_arg.out, http_code, result, (result != CURLE_OK) ? curl_easy_strerror(result) : "");
208  if (response.curl_code != CURLE_OK || response.http_status_code >= 500) {
209  std::ostringstream error_message;
210  error_message << "curl error " << response.curl_code << " (http code " << response.http_status_code
211  << "): " << response.error_message;
212  LOG_ERROR << error_message.str();
213  if (retry_times != 0) {
214  sleep(1);
215  response = perform(curl_handler, --retry_times, size_limit);
216  }
217  }
218  LOG_TRACE << "response http code: " << response.http_status_code;
219  LOG_TRACE << "response: " << response.body;
220  return response;
221 }
222 
223 HttpResponse HttpClient::download(const std::string& url, curl_write_callback callback, void* userp) {
224  CURL* curl_download = curl_easy_duphandle(curl);
225 
226  // TODO: it is a workaround for an unidentified bug in libcurl. Ideally the bug itself should be fixed.
227  if (pkcs11_key) {
228  curl_easy_setopt(curl_download, CURLOPT_SSLENGINE, "pkcs11");
229  curl_easy_setopt(curl_download, CURLOPT_SSLKEYTYPE, "ENG");
230  }
231 
232  if (pkcs11_cert) {
233  curl_easy_setopt(curl_download, CURLOPT_SSLCERTTYPE, "ENG");
234  }
235 
236  curl_easy_setopt(curl_download, CURLOPT_URL, url.c_str());
237  curl_easy_setopt(curl_download, CURLOPT_HTTPGET, 1L);
238  curl_easy_setopt(curl_download, CURLOPT_FOLLOWLOCATION, 1L);
239  curl_easy_setopt(curl_download, CURLOPT_WRITEFUNCTION, callback);
240  curl_easy_setopt(curl_download, CURLOPT_WRITEDATA, userp);
241  curl_easy_setopt(curl_download, CURLOPT_LOW_SPEED_TIME, speed_limit_time_interval_);
242  curl_easy_setopt(curl_download, CURLOPT_LOW_SPEED_LIMIT, speed_limit_bytes_per_sec_);
243 
244  CURLcode result = curl_easy_perform(curl_download);
245  curl_easy_getinfo(curl_download, CURLINFO_RESPONSE_CODE, &http_code);
246  HttpResponse response("", http_code, result, (result != CURLE_OK) ? curl_easy_strerror(result) : "");
247  curl_easy_cleanup(curl_download);
248  return response;
249 }
250 
251 // vim: set tabstop=2 shiftwidth=2 expandtab:
General data structures.
Definition: types.cc:6
STL namespace.
Helper class to manage curl_global_init/curl_global_cleanup calls.
Definition: httpclient.h:16