1 /*
2 * Copyright 2022 Google LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "fcp/client/http/curl/curl_http_request_handle.h"
18
19 #include <memory>
20 #include <optional>
21 #include <string>
22 #include <utility>
23
24 #include "absl/strings/match.h"
25 #include "curl/curl.h"
26 #include "fcp/base/monitoring.h"
27 #include "fcp/client/http/curl/curl_api.h"
28 #include "fcp/client/http/curl/curl_header_parser.h"
29 #include "fcp/client/http/curl/curl_http_response.h"
30 #include "fcp/client/http/http_client_util.h"
31
32 namespace fcp::client::http::curl {
33 namespace {
34 // A type check for the macro.
AsCode(CURLcode code)35 inline CURLcode AsCode(CURLcode code) { return code; }
36 /**
37 * Macro which allows to check for a status code and return from the
38 * current method if not OK. Example:
39 *
40 * Status DoSomething() {
41 * CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(...));
42 * }
43 */
44 #define CURL_RETURN_IF_ERROR(expr) \
45 do { \
46 CURLcode __code = AsCode(expr); \
47 if (__code != CURLE_OK) { \
48 FCP_LOG(ERROR) << "Easy handle failed with " \
49 << CurlEasyHandle::StrError(__code); \
50 return (__code); \
51 } \
52 } while (false)
53
54 // Add a new element to a C-style list.
AddToCurlHeaderList(curl_slist * header_list,const std::string & key,const std::string & value)55 curl_slist* AddToCurlHeaderList(curl_slist* header_list, const std::string& key,
56 const std::string& value) {
57 // A null pointer is returned if anything went wrong, otherwise a new
58 // list pointer is returned.
59 curl_slist* tmp =
60 curl_slist_append(header_list, absl::StrCat(key, ": ", value).c_str());
61 FCP_CHECK(tmp != nullptr);
62 return tmp;
63 }
64
65 } // namespace
66
HeaderCallback(char * buffer,size_t size,size_t n_items,void * user_data)67 size_t CurlHttpRequestHandle::HeaderCallback(char* buffer, size_t size,
68 size_t n_items, void* user_data) {
69 auto self = static_cast<CurlHttpRequestHandle*>(user_data);
70 std::string str_header(static_cast<char*>(buffer), size * n_items);
71
72 self->header_parser_.ParseHeader(str_header);
73 if (!self->header_parser_.IsLastHeader()) {
74 return size * n_items;
75 }
76
77 self->response_ =
78 std::make_unique<CurlHttpResponse>(self->header_parser_.GetStatusCode(),
79 self->header_parser_.GetHeaderList());
80
81 FCP_CHECK(self->callback_ != nullptr);
82 absl::Status status =
83 self->callback_->OnResponseStarted(*self->request_, *self->response_);
84
85 if (!status.ok()) {
86 FCP_LOG(ERROR) << "Called OnResponseStarted. Received status: " << status;
87 self->callback_->OnResponseError(*self->request_, status);
88 }
89
90 return size * n_items;
91 }
92
DownloadCallback(void * body,size_t size,size_t nmemb,void * user_data)93 size_t CurlHttpRequestHandle::DownloadCallback(void* body, size_t size,
94 size_t nmemb, void* user_data) {
95 auto self = static_cast<CurlHttpRequestHandle*>(user_data);
96 absl::string_view str_body(static_cast<char*>(body), size * nmemb);
97
98 absl::Status status = self->callback_->OnResponseBody(
99 *self->request_, *self->response_, str_body);
100
101 if (!status.ok()) {
102 FCP_LOG(ERROR) << "Called OnResponseBody. Received status: " << status;
103 self->callback_->OnResponseBodyError(*self->request_, *self->response_,
104 status);
105 }
106
107 return size * nmemb;
108 }
109
UploadCallback(char * buffer,size_t size,size_t num,void * user_data)110 size_t CurlHttpRequestHandle::UploadCallback(char* buffer, size_t size,
111 size_t num, void* user_data) {
112 auto self = static_cast<CurlHttpRequestHandle*>(user_data);
113 size_t buffer_size = size * num;
114
115 absl::StatusOr<int64_t> read_size =
116 self->request_->ReadBody(buffer, buffer_size);
117 if (read_size.ok()) {
118 return read_size.value();
119 } else if (read_size.status().code() == absl::StatusCode::kOutOfRange) {
120 return 0;
121 }
122 return CURL_READFUNC_ABORT;
123 }
124
ProgressCallback(void * user_data,curl_off_t dltotal,curl_off_t dlnow,curl_off_t ultotal,curl_off_t ulnow)125 size_t CurlHttpRequestHandle::ProgressCallback(void* user_data,
126 curl_off_t dltotal,
127 curl_off_t dlnow,
128 curl_off_t ultotal,
129 curl_off_t ulnow) {
130 auto self = static_cast<CurlHttpRequestHandle*>(user_data);
131 absl::MutexLock lock(&self->mutex_);
132 // Abort is any number except zero.
133 return (self->is_cancelled_) ? 1 : 0;
134 }
135
CurlHttpRequestHandle(std::unique_ptr<HttpRequest> request,std::unique_ptr<CurlEasyHandle> easy_handle,const std::string & test_cert_path)136 CurlHttpRequestHandle::CurlHttpRequestHandle(
137 std::unique_ptr<HttpRequest> request,
138 std::unique_ptr<CurlEasyHandle> easy_handle,
139 const std::string& test_cert_path)
140 : request_(std::move(request)),
141 response_(nullptr),
142 easy_handle_(std::move(easy_handle)),
143 callback_(nullptr),
144 is_being_performed_(false),
145 is_completed_(false),
146 is_cancelled_(false),
147 header_list_(nullptr) {
148 FCP_CHECK(request_ != nullptr);
149 FCP_CHECK(easy_handle_ != nullptr);
150
151 CURLcode code = InitializeConnection(test_cert_path);
152 if (code != CURLE_OK) {
153 FCP_LOG(ERROR) << "easy_handle initialization failed with code "
154 << CurlEasyHandle::StrError(code);
155 FCP_LOG(ERROR) << error_buffer_;
156 callback_->OnResponseError(*request_, absl::InternalError(error_buffer_));
157 return;
158 }
159 }
160
~CurlHttpRequestHandle()161 CurlHttpRequestHandle::~CurlHttpRequestHandle() {
162 curl_slist_free_all(header_list_);
163 }
164
Cancel()165 void CurlHttpRequestHandle::Cancel() {
166 absl::MutexLock lock(&mutex_);
167
168 if (callback_ == nullptr || is_cancelled_ || is_completed_) {
169 return;
170 }
171 if (response_ != nullptr) {
172 callback_->OnResponseBodyError(*request_, *response_,
173 absl::CancelledError());
174 } else {
175 callback_->OnResponseError(*request_, absl::CancelledError());
176 }
177 is_cancelled_ = true;
178 }
179
MarkAsCompleted()180 void CurlHttpRequestHandle::MarkAsCompleted() {
181 absl::MutexLock lock(&mutex_);
182
183 FCP_CHECK(callback_ != nullptr);
184 if (!is_cancelled_ && !is_completed_) {
185 if (response_ != nullptr) {
186 callback_->OnResponseCompleted(*request_, *response_);
187 } else {
188 callback_->OnResponseError(*request_,
189 absl::InternalError("response_ is nullptr"));
190 }
191 }
192 is_completed_ = true;
193 }
194
AddToMulti(CurlMultiHandle * multi_handle,HttpRequestCallback * callback)195 absl::Status CurlHttpRequestHandle::AddToMulti(CurlMultiHandle* multi_handle,
196 HttpRequestCallback* callback) {
197 absl::MutexLock lock(&mutex_);
198
199 FCP_CHECK(callback != nullptr);
200 FCP_CHECK(multi_handle != nullptr);
201
202 if (is_cancelled_) {
203 callback->OnResponseError(*request_, absl::CancelledError());
204 return absl::CancelledError();
205 } else if (is_being_performed_ || is_completed_) {
206 return absl::ResourceExhaustedError(
207 "The handle was previously passed to another PerformRequests call.");
208 }
209
210 is_being_performed_ = true;
211 callback_ = callback;
212
213 CURLMcode code = multi_handle->AddEasyHandle(easy_handle_.get());
214 if (code != CURLM_OK) {
215 FCP_LOG(ERROR) << "AddEasyHandle failed with code " << code;
216 FCP_LOG(ERROR) << error_buffer_;
217 callback_->OnResponseError(*request_, absl::InternalError(error_buffer_));
218 return absl::InternalError(error_buffer_);
219 }
220
221 return absl::OkStatus();
222 }
223
RemoveFromMulti(CurlMultiHandle * multi_handle)224 void CurlHttpRequestHandle::RemoveFromMulti(CurlMultiHandle* multi_handle) {
225 absl::MutexLock lock(&mutex_);
226
227 FCP_CHECK(multi_handle != nullptr);
228 CURLMcode code = multi_handle->RemoveEasyHandle(easy_handle_.get());
229 if (code != CURLM_OK) {
230 FCP_LOG(ERROR) << "RemoveEasyHandle failed with code "
231 << CurlMultiHandle::StrError(code);
232 FCP_LOG(ERROR) << error_buffer_;
233 }
234 }
235
236 HttpRequestHandle::SentReceivedBytes
TotalSentReceivedBytes() const237 CurlHttpRequestHandle::TotalSentReceivedBytes() const {
238 absl::MutexLock lock(&mutex_);
239 curl_off_t total_sent_bytes = 0;
240 CURLcode code =
241 easy_handle_->GetInfo(CURLINFO_SIZE_UPLOAD_T, &total_sent_bytes);
242 if (code != CURLE_OK) {
243 FCP_LOG(ERROR) << "TotalSentBytes failed with code " << code;
244 FCP_LOG(ERROR) << error_buffer_;
245 }
246 curl_off_t total_received_bytes = 0;
247 code = easy_handle_->GetInfo(CURLINFO_SIZE_DOWNLOAD_T, &total_received_bytes);
248 if (code != CURLE_OK) {
249 FCP_LOG(ERROR) << "TotalReceivedBytes failed with code " << code;
250 FCP_LOG(ERROR) << error_buffer_;
251 }
252 return {.sent_bytes = total_sent_bytes,
253 .received_bytes = total_received_bytes};
254 }
255
InitializeConnection(const std::string & test_cert_path)256 CURLcode CurlHttpRequestHandle::InitializeConnection(
257 const std::string& test_cert_path) {
258 error_buffer_[0] = 0;
259 // Needed to read an error message.
260 CURL_RETURN_IF_ERROR(
261 easy_handle_->SetOpt(CURLOPT_ERRORBUFFER, error_buffer_));
262
263 // Skip all signal handling because is not thread-safe.
264 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_NOSIGNAL, 1L));
265
266 CURL_RETURN_IF_ERROR(
267 easy_handle_->SetOpt(CURLOPT_URL, std::string(request_->uri())));
268
269 // Forces curl to follow redirects.
270 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_FOLLOWLOCATION, 1L));
271
272 // Suppresses headers added by a proxy.
273 CURL_RETURN_IF_ERROR(
274 easy_handle_->SetOpt(CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L));
275
276 // Force curl to verify the ssl connection.
277 CURL_RETURN_IF_ERROR(
278 test_cert_path.empty()
279 ? easy_handle_->SetOpt(CURLOPT_CAINFO, nullptr)
280 : easy_handle_->SetOpt(CURLOPT_CAINFO, test_cert_path));
281
282 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_SSL_VERIFYPEER, 2L));
283
284 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_SSL_VERIFYHOST, 1L));
285
286 // Force curl to never timeout.
287 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_TIMEOUT_MS,
288 std::numeric_limits<int>::max()));
289
290 // Called when a response header received.
291 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(
292 CURLOPT_HEADERFUNCTION, &CurlHttpRequestHandle::HeaderCallback));
293
294 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_HEADERDATA, this));
295
296 // Called when a response body received.
297 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(
298 CURLOPT_WRITEFUNCTION, &CurlHttpRequestHandle::DownloadCallback));
299
300 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_WRITEDATA, this));
301
302 // Called to send a request body
303 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(
304 CURLOPT_READFUNCTION, &CurlHttpRequestHandle::UploadCallback));
305
306 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_READDATA, this));
307
308 // Called periodically. We use it to check whether the request is cancelled.
309 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(
310 CURLOPT_XFERINFOFUNCTION, &CurlHttpRequestHandle::ProgressCallback));
311 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_XFERINFODATA, this));
312 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_NOPROGRESS, 0L));
313
314 // Private storage. Used by a multi-handle.
315 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_PRIVATE, this));
316
317 switch (request_->method()) {
318 case HttpRequest::Method::kGet:
319 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_HTTPGET, 1L));
320 break;
321 case HttpRequest::Method::kHead:
322 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_NOBODY, 1L));
323 break;
324 case HttpRequest::Method::kPost:
325 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_POST, 1L));
326 // Forces curl to use the callback.
327 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_POSTFIELDS, nullptr));
328 break;
329 case HttpRequest::Method::kPut:
330 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_UPLOAD, 1L));
331 break;
332 case HttpRequest::Method::kPatch:
333 case HttpRequest::Method::kDelete:
334 FCP_LOG(ERROR) << "Unsupported request type";
335 return CURLE_UNSUPPORTED_PROTOCOL;
336 }
337
338 return InitializeHeaders(request_->extra_headers(), request_->method());
339 }
340
InitializeHeaders(const HeaderList & extra_headers,HttpRequest::Method method)341 CURLcode CurlHttpRequestHandle::InitializeHeaders(
342 const HeaderList& extra_headers, HttpRequest::Method method) {
343 // If no "Accept-Encoding" request header is explicitly specified
344 // advertise an "Accept-Encoding: gzip" else leave decoded.
345 std::optional<std::string> accept_encoding =
346 FindHeader(request_->extra_headers(), kAcceptEncodingHdr);
347 if (!accept_encoding.has_value()) {
348 // Libcurl is responsible for the encoding.
349 CURL_RETURN_IF_ERROR(
350 easy_handle_->SetOpt(CURLOPT_ACCEPT_ENCODING, kGzipEncodingHdrValue));
351 header_parser_.UseCurlEncoding();
352 } else {
353 // The caller is responsible for the encoding.
354 CURL_RETURN_IF_ERROR(
355 easy_handle_->SetOpt(CURLOPT_ACCEPT_ENCODING, nullptr));
356 }
357
358 for (auto& [key, value] : extra_headers) {
359 if (absl::EqualsIgnoreCase(key, kAcceptEncodingHdr)) {
360 continue;
361 } else if (absl::EqualsIgnoreCase(key, kContentLengthHdr)) {
362 if (method == HttpRequest::Method::kPost) {
363 // For post less than 2GB
364 CURL_RETURN_IF_ERROR(
365 easy_handle_->SetOpt(CURLOPT_POSTFIELDSIZE, std::stol(value)));
366
367 // Removes the header to prevent libcurl from setting it
368 // to 'Expect: 100-continue' by default, which causes an additional
369 // and unnecessary network round trip.
370 header_list_ = AddToCurlHeaderList(header_list_, kExpectHdr, "");
371 } else if (method == HttpRequest::Method::kPut) {
372 CURL_RETURN_IF_ERROR(
373 easy_handle_->SetOpt(CURLOPT_INFILESIZE, std::stol(value)));
374 header_list_ = AddToCurlHeaderList(header_list_, kExpectHdr, "");
375 }
376 } else {
377 // A user-defined "Expect" header is not supported.
378 FCP_CHECK(!absl::EqualsIgnoreCase(key, kExpectHdr));
379 header_list_ = AddToCurlHeaderList(header_list_, key, value);
380 }
381 }
382
383 CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_HTTPHEADER, header_list_));
384 return CURLE_OK;
385 }
386 } // namespace fcp::client::http::curl
387