-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
internal_http_service_curl.cc
333 lines (291 loc) · 12.3 KB
/
internal_http_service_curl.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#include "drake/geometry/render_gltf_client/internal_http_service_curl.h"
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <regex>
#include <thread>
#include <utility>
#include <vector>
#include <curl/curl.h>
#include <fmt/format.h>
#include "drake/common/drake_assert.h"
#include "drake/common/fmt_ostream.h"
#include "drake/common/never_destroyed.h"
#include "drake/common/text_logging.h"
#include "drake/common/unused.h"
namespace drake {
namespace geometry {
namespace render_gltf_client {
namespace internal {
namespace {
namespace fs = std::filesystem;
// Curl callbacks --------------------------------------------------------------
// Writes callback for libcurl, assumes `userp` points to an std::ofstream.
// See: https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html
size_t WriteFileData(void* buffer, size_t size, size_t nmemb, void* userp) {
const size_t data_size = size * nmemb;
std::ofstream* f = static_cast<std::ofstream*>(userp);
f->write(static_cast<char*>(buffer), data_size);
// Return 0 to indicate a failed write operation to libcurl.
if (!f) return 0;
return data_size;
}
using DebugData =
std::vector<std::pair<const curl_infotype, const std::string>>;
/* Used with CURLOPT_VERBOSE in order to add to drake::log(), see also:
https://curl.se/libcurl/c/CURLOPT_DEBUGFUNCTION.html
`userp` is assumed to be DebugData defined above, this method simply
accumulates the incoming messages from curl, LogCurlDebugData below combines
and adds them to the drake log. The following types are skipped, as these
items should be handled by the write / read callback (binary data should not be
logged to drake::log()):
- CURLINFO_DATA_IN
- CURLINFO_DATA_OUT
- CURLINFO_SSL_DATA_IN
- CURLINFO_SSL_DATA_OUT */
int DebugCallback(CURL* /* handle */, curl_infotype type, char* data,
size_t size, void* userp) {
if (type != CURLINFO_DATA_IN && type != CURLINFO_DATA_OUT &&
type != CURLINFO_SSL_DATA_IN && type != CURLINFO_SSL_DATA_OUT) {
DebugData* debug_data = static_cast<DebugData*>(userp);
debug_data->emplace_back(type, std::string(data, data + size));
}
return 0; // See docs, return of 0 is always required.
}
// Used in logging curl data, returns a human friendly string.
std::string CurlInfoTypeAsString(curl_infotype type) {
if (type == CURLINFO_TEXT)
return "CURLINFO_TEXT";
else if (type == CURLINFO_HEADER_IN)
return "CURLINFO_HEADER_IN";
else if (type == CURLINFO_HEADER_OUT)
return "CURLINFO_HEADER_OUT";
else if (type == CURLINFO_DATA_IN)
return "CURLINFO_DATA_IN";
else if (type == CURLINFO_DATA_OUT)
return "CURLINFO_DATA_OUT";
else if (type == CURLINFO_SSL_DATA_IN)
return "CURLINFO_SSL_DATA_IN";
else if (type == CURLINFO_SSL_DATA_OUT)
return "CURLINFO_SSL_DATA_OUT";
else if (type == CURLINFO_END)
return "CURLINFO_END";
return fmt::format("UNKNOWN_CURLINFO_TYPE={}", fmt_streamed(type));
}
/* Removes leading / trailing whitespace from `message` before logging. Curl
includes newline characters in its entries, which are desirable for the
accumulated interior message parts but the trailing whitespace in particular
leads to cluttered logs (drake::log() adds a trailing newline). */
void LogIfTrimmedWhitespaceNonEmpty(curl_infotype type,
const std::string& message) {
const auto trimmed =
std::regex_replace(message, std::regex(R"(^\s+|\s+$)"), "");
// Some curl messages were just the \n character, don't log "nothing".
if (!trimmed.empty()) {
drake::log()->debug("[{}] {}", CurlInfoTypeAsString(type), trimmed);
}
}
/* Logs the combined messages added to `debug_data` by the DebugCallback.
When curl calls the DebugCallback, it will do so with piecemeal components. In
the below walk-through, the "Accept: ..." statement was modified to add spaces
between * and / to avoid ending the comment.
Call 1: type=CURLINFO_HEADER_OUT
data=" Trying 127.0.0.1:8000...\n"
Call 2: type=CURLINFO_HEADER_OUT
data="TCP_NODELAY set\n"
Call 3: type=CURLINFO_HEADER_OUT
data="Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)\n"
Call 4: type=CURLINFO_TEXT
data="POST /render HTTP/1.1
Host: 127.0.0.1:8000
Accept: * / *
Content-Length: 1448
Content-Type: multipart/form-data; boundary=--...---c13a866cb4280bc1\n"
Call 5: type=CURLINFO_HEADER_IN
data="We are completely uploaded and fine\n"
...
Unlike after CURLINFO_HEADER_OUT, the CURLINFO_TEXT included before
CURLINFO_HEADER_IN is provided one line at a time. As a result, the
drake::log() if added to directly in DebugCallback can become somewhat
cluttered and inconsistent. This method combines any sequential messages that
were appended into a single accumulated log call, trimming preceding and
trailing whitespace to keep the log as orderly as possible. In the example
above, the input vector would look something like:
{
{CURLINFO_HEADER_OUT, " Trying 127.0.0.1:8000...\n"},
{CURLINFO_HEADER_OUT, "TCP_NODELAY set\n"},
{CURLINFO_HEADER_OUT, "Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)\n"},
{CURLINFO_TEXT, "POST /render HTTP/1.1\n...\n...\n"},
{CURLINFO_HEADER_IN, "We are completely uploaded and fine\n"},
}
and the drake::log() will have three total combined calls, the first three
CURLINFO_HEADER_OUT will be combined into one log call, then CURLINFO_TEXT,
then CURLINFO_HEADER_IN.
*/
void LogCurlDebugData(const DebugData& debug_data) {
// NOTE: not very efficient in terms of copies, but messages are small.
std::string accumulator{};
/* The value must be initialized for compiler warnings, prev_type gets set on
the first iteration. Initialize to intentionally invalid value. */
curl_infotype prev_type = static_cast<curl_infotype>(-1);
int counter = 0;
for (const auto& pair : debug_data) {
// On the first iteration, initialize the previous type and accumulator.
if (counter++ == 0) {
prev_type = pair.first;
accumulator += pair.second;
} else {
/* If the current and previous type are the same, accumulate and continue.
Otherwise, log the now complete accumulator and reset for the newly
encountered curl_infotype. */
if (pair.first == prev_type) {
accumulator += pair.second;
} else {
LogIfTrimmedWhitespaceNonEmpty(pair.first, accumulator);
prev_type = pair.first;
accumulator = pair.second;
}
}
}
// Log final results (loop will have accumulated at least the last element).
if (counter > 0) {
LogIfTrimmedWhitespaceNonEmpty(prev_type, accumulator);
}
}
// Helper utilities ------------------------------------------------------------
/* Returns the next unique-per-process integer that can uniquely identify a
temporary file to use in a potential curl transaction. */
int64_t NextTempId() {
static drake::never_destroyed<std::atomic<int64_t>> global_temp_id;
return ++(global_temp_id.access());
}
std::string NextTempFile() {
/* Create e.g., {temp_directory()}/0000000000000000XYZ.curl
NOTE: the maximum number of digits in a int64_t is 19. */
return fmt::format("{:0>19}.curl", NextTempId());
}
} // namespace
HttpServiceCurl::HttpServiceCurl() : HttpService() {
/* libcurl should be initialized exactly once per process, this initialization
is not thread-safe and must be done before potential threads using curl begin
(e.g., threaded renderings). See also: MakeRenderEngineGltfClient
documentation in factory.h */
static CURLcode ignored =
curl_global_init(CURL_GLOBAL_ALL | CURL_GLOBAL_ACK_EINTR);
unused(ignored);
}
HttpServiceCurl::~HttpServiceCurl() {}
HttpResponse HttpServiceCurl::DoPostForm(const std::string& temp_directory,
const std::string& url,
const DataFieldsMap& data_fields,
const FileFieldsMap& file_fields,
bool verbose) {
// Create and fill out a <form> to POST.
CURL* curl{nullptr};
CURLcode result;
curl = curl_easy_init();
DRAKE_DEMAND(curl != nullptr);
curl_mime* form{nullptr};
struct curl_slist* headerlist{nullptr};
form = curl_mime_init(curl);
/* Defined to make cleanup easier before throwing any possible exceptions.
Frees resources for `curl`, `form`, and `headerlist`. */
auto cleanup_curl = [](CURL* c, curl_mime* f, curl_slist* h_list) {
if (f != nullptr) curl_mime_free(f);
if (h_list != nullptr) curl_slist_free_all(h_list);
curl_easy_cleanup(c);
};
// Used when verbose, needed in scope for logging after curl_easy_perform.
DebugData debug_data;
if (verbose) {
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, &DebugCallback);
curl_easy_setopt(curl, CURLOPT_DEBUGDATA, &debug_data);
}
// Setup the POST url.
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// Add all of the data fields.
for (const auto& [field_name, field_data] : data_fields) {
curl_mimepart* field = curl_mime_addpart(form);
curl_mime_name(field, field_name.c_str());
curl_mime_data(field, field_data.c_str(), CURL_ZERO_TERMINATED);
}
// Add all of the file fields.
for (const auto& [field_name, field_data_pair] : file_fields) {
// Add the file to the form.
const auto& file_path = field_data_pair.first;
curl_mimepart* field = curl_mime_addpart(form);
curl_mime_name(field, field_name.c_str());
curl_mime_filedata(field, file_path.c_str());
// Set the mimetype if provided.
const auto& file_mimetype = field_data_pair.second;
if (file_mimetype.has_value()) {
curl_mime_type(field, file_mimetype.value().c_str());
}
}
curl_easy_setopt(curl, CURLOPT_MIMEPOST, form);
// Disable 100-Continue. See:
// http://www.iandennismiller.com/posts/curl-http1-1-100-continue-and-multipartform-data-post.html
headerlist = curl_slist_append(headerlist, "Expect:");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
/* We do not know if the server is going to respond with anything, and if it
does it will be e.g., json or image file response. Write directly to a file
buffer within our temporary directory. */
const auto temp_bin_out = fs::path(temp_directory) / NextTempFile();
const std::string bin_out_path{temp_bin_out.string()};
if (fs::exists(temp_bin_out)) {
cleanup_curl(curl, form, headerlist);
throw std::runtime_error(fmt::format(
"RenderClient: refusing to overwrite temporary file '{}' that "
"already exists, please cleanup temporary directory '{}'.",
bin_out_path, temp_directory));
}
// Open the file for writing, pass it off to curl.
std::ofstream bin_out(bin_out_path, std::ios::binary);
if (!bin_out.good()) {
cleanup_curl(curl, form, headerlist);
throw std::runtime_error(fmt::format(
"RenderClient: unable to open temporary file '{}'.", bin_out_path));
}
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteFileData);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bin_out);
// Perform the POST and drake::log() prior to any potential exceptions.
result = curl_easy_perform(curl);
if (!bin_out.good()) {
cleanup_curl(curl, form, headerlist);
throw std::runtime_error(fmt::format(
"RenderClient: unable to wtite temporary file '{}'.", bin_out_path));
}
if (verbose) {
LogCurlDebugData(debug_data);
}
// Populate the wrapper return struct.
HttpResponse ret;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &ret.http_code);
if (result != CURLE_OK) {
ret.service_error_message = std::string(curl_easy_strerror(result));
}
// Cleanup the curl memory.
cleanup_curl(curl, form, headerlist);
// Close the file after the write callback is complete. Delete the file if
// it's empty.
bin_out.close();
const bool server_gave_data_response = fs::file_size(temp_bin_out) > 0;
if (server_gave_data_response) {
ret.data_path = bin_out_path;
} else {
try {
fs::remove(bin_out_path);
} catch (const std::exception& e) {
drake::log()->debug("HttpServiceCurl unable to delete '{}'. {}",
bin_out_path, e.what());
}
}
return ret;
}
} // namespace internal
} // namespace render_gltf_client
} // namespace geometry
} // namespace drake