xref: /aosp_15_r20/external/federated-compute/fcp/client/http/curl/curl_http_request_handle.cc (revision 14675a029014e728ec732f129a32e299b2da0601)
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