2 * HTTP GET/PUT IO engine
4 * IO engine to perform HTTP(S) GET/PUT requests via libcurl-easy.
6 * Copyright (C) 2018 SUSE LLC
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License,
10 * version 2 as published by the Free Software Foundation..
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public
18 * License along with this program; if not, write to the Free
19 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
25 #include <curl/curl.h>
26 #include <openssl/hmac.h>
27 #include <openssl/sha.h>
28 #include <openssl/md5.h>
30 #include "../optgroup.h"
40 FIO_HTTPS_INSECURE = 2,
56 char *swift_auth_token;
61 struct http_curl_stream {
67 static struct fio_option options[] = {
72 .help = "Enable https",
73 .off1 = offsetof(struct http_options, https),
77 .oval = FIO_HTTPS_OFF,
82 .help = "Enable HTTPS",
85 .oval = FIO_HTTPS_INSECURE,
86 .help = "Enable HTTPS, disable peer verification",
89 .category = FIO_OPT_C_ENGINE,
90 .group = FIO_OPT_G_HTTP,
95 .type = FIO_OPT_STR_STORE,
96 .help = "Hostname (S3 bucket)",
97 .off1 = offsetof(struct http_options, host),
99 .category = FIO_OPT_C_ENGINE,
100 .group = FIO_OPT_G_HTTP,
104 .lname = "http_user",
105 .type = FIO_OPT_STR_STORE,
106 .help = "HTTP user name",
107 .off1 = offsetof(struct http_options, user),
108 .category = FIO_OPT_C_ENGINE,
109 .group = FIO_OPT_G_HTTP,
113 .lname = "http_pass",
114 .type = FIO_OPT_STR_STORE,
115 .help = "HTTP password",
116 .off1 = offsetof(struct http_options, pass),
117 .category = FIO_OPT_C_ENGINE,
118 .group = FIO_OPT_G_HTTP,
121 .name = "http_s3_key",
122 .lname = "S3 secret key",
123 .type = FIO_OPT_STR_STORE,
124 .help = "S3 secret key",
125 .off1 = offsetof(struct http_options, s3_key),
127 .category = FIO_OPT_C_ENGINE,
128 .group = FIO_OPT_G_HTTP,
131 .name = "http_s3_keyid",
132 .lname = "S3 key id",
133 .type = FIO_OPT_STR_STORE,
135 .off1 = offsetof(struct http_options, s3_keyid),
137 .category = FIO_OPT_C_ENGINE,
138 .group = FIO_OPT_G_HTTP,
141 .name = "http_swift_auth_token",
142 .lname = "Swift auth token",
143 .type = FIO_OPT_STR_STORE,
144 .help = "OpenStack Swift auth token",
145 .off1 = offsetof(struct http_options, swift_auth_token),
147 .category = FIO_OPT_C_ENGINE,
148 .group = FIO_OPT_G_HTTP,
151 .name = "http_s3_region",
152 .lname = "S3 region",
153 .type = FIO_OPT_STR_STORE,
155 .off1 = offsetof(struct http_options, s3_region),
157 .category = FIO_OPT_C_ENGINE,
158 .group = FIO_OPT_G_HTTP,
162 .lname = "Request mode to use",
164 .help = "Whether to use WebDAV, Swift, or S3",
165 .off1 = offsetof(struct http_options, mode),
169 .oval = FIO_HTTP_WEBDAV,
170 .help = "WebDAV server",
174 .help = "S3 storage backend",
177 .oval = FIO_HTTP_SWIFT,
178 .help = "OpenStack Swift storage",
181 .category = FIO_OPT_C_ENGINE,
182 .group = FIO_OPT_G_HTTP,
185 .name = "http_verbose",
186 .lname = "HTTP verbosity level",
188 .help = "increase http engine verbosity",
189 .off1 = offsetof(struct http_options, verbose),
191 .category = FIO_OPT_C_ENGINE,
192 .group = FIO_OPT_G_HTTP,
199 static char *_aws_uriencode(const char *uri)
201 size_t bufsize = 1024;
202 char *r = malloc(bufsize);
205 const char *hex = "0123456789ABCDEF";
208 log_err("malloc failed\n");
213 for (i = 0; (c = uri[i]); i++) {
215 log_err("encoding the URL failed\n");
219 if ( (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
220 || (c >= '0' && c <= '9') || c == '_' || c == '-'
221 || c == '~' || c == '.' || c == '/')
225 r[n++] = hex[(c >> 4 ) & 0xF];
226 r[n++] = hex[c & 0xF];
233 static char *_conv_hex(const unsigned char *p, size_t len)
237 const char *hex = "0123456789abcdef";
238 r = malloc(len * 2 + 1);
240 for (i = 0; i < len; i++) {
241 r[n++] = hex[(p[i] >> 4 ) & 0xF];
242 r[n++] = hex[p[i] & 0xF];
249 static char *_gen_hex_sha256(const char *p, size_t len)
251 unsigned char hash[SHA256_DIGEST_LENGTH];
253 SHA256((unsigned char*)p, len, hash);
254 return _conv_hex(hash, SHA256_DIGEST_LENGTH);
257 static char *_gen_hex_md5(const char *p, size_t len)
259 unsigned char hash[MD5_DIGEST_LENGTH];
261 MD5((unsigned char*)p, len, hash);
262 return _conv_hex(hash, MD5_DIGEST_LENGTH);
265 static void _hmac(unsigned char *md, void *key, int key_len, char *data) {
266 #ifndef CONFIG_HAVE_OPAQUE_HMAC_CTX
270 unsigned int hmac_len;
272 #ifdef CONFIG_HAVE_OPAQUE_HMAC_CTX
273 ctx = HMAC_CTX_new();
276 /* work-around crash in certain versions of libssl */
279 HMAC_Init_ex(ctx, key, key_len, EVP_sha256(), NULL);
280 HMAC_Update(ctx, (unsigned char*)data, strlen(data));
281 HMAC_Final(ctx, md, &hmac_len);
282 #ifdef CONFIG_HAVE_OPAQUE_HMAC_CTX
285 HMAC_CTX_cleanup(ctx);
289 static int _curl_trace(CURL *handle, curl_infotype type,
290 char *data, size_t size,
294 (void)handle; /* prevent compiler warning */
299 fprintf(stderr, "== Info: %s", data);
302 case CURLINFO_SSL_DATA_OUT:
304 case CURLINFO_SSL_DATA_IN:
307 case CURLINFO_HEADER_OUT:
308 text = "=> Send header";
310 case CURLINFO_DATA_OUT:
311 text = "=> Send data";
313 case CURLINFO_HEADER_IN:
314 text = "<= Recv header";
316 case CURLINFO_DATA_IN:
317 text = "<= Recv data";
321 log_info("%s: %s", text, data);
325 /* https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
326 * https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html#signing-request-intro
328 static void _add_aws_auth_header(CURL *curl, struct curl_slist *slist, struct http_options *o,
329 int op, const char *uri, char *buf, size_t len)
338 char *uri_encoded = NULL;
341 char *signature = NULL;
342 const char *service = "s3";
343 const char *aws = "aws4_request";
344 unsigned char md[SHA256_DIGEST_LENGTH];
346 time_t t = time(NULL);
347 struct tm *gtm = gmtime(&t);
349 strftime (date_short, sizeof(date_short), "%Y%m%d", gtm);
350 strftime (date_iso, sizeof(date_iso), "%Y%m%dT%H%M%SZ", gtm);
351 uri_encoded = _aws_uriencode(uri);
353 if (op == DDIR_WRITE) {
354 dsha = _gen_hex_sha256(buf, len);
355 sprintf(method, "PUT");
357 /* DDIR_READ && DDIR_TRIM supply an empty body */
359 sprintf(method, "GET");
361 sprintf(method, "DELETE");
362 dsha = _gen_hex_sha256("", 0);
365 /* Create the canonical request first */
366 snprintf(creq, sizeof(creq),
371 "x-amz-content-sha256:%s\n"
374 "host;x-amz-content-sha256;x-amz-date\n"
377 , uri_encoded, o->host, dsha, date_iso, dsha);
379 csha = _gen_hex_sha256(creq, strlen(creq));
380 snprintf(sts, sizeof(sts), "AWS4-HMAC-SHA256\n%s\n%s/%s/%s/%s\n%s",
381 date_iso, date_short, o->s3_region, service, aws, csha);
383 snprintf((char *)dkey, sizeof(dkey), "AWS4%s", o->s3_key);
384 _hmac(md, dkey, strlen(dkey), date_short);
385 _hmac(md, md, SHA256_DIGEST_LENGTH, o->s3_region);
386 _hmac(md, md, SHA256_DIGEST_LENGTH, (char*) service);
387 _hmac(md, md, SHA256_DIGEST_LENGTH, (char*) aws);
388 _hmac(md, md, SHA256_DIGEST_LENGTH, sts);
390 signature = _conv_hex(md, SHA256_DIGEST_LENGTH);
392 /* Surpress automatic Accept: header */
393 slist = curl_slist_append(slist, "Accept:");
395 snprintf(s, sizeof(s), "x-amz-content-sha256: %s", dsha);
396 slist = curl_slist_append(slist, s);
398 snprintf(s, sizeof(s), "x-amz-date: %s", date_iso);
399 slist = curl_slist_append(slist, s);
401 snprintf(s, sizeof(s), "Authorization: AWS4-HMAC-SHA256 Credential=%s/%s/%s/s3/aws4_request,"
402 "SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=%s",
403 o->s3_keyid, date_short, o->s3_region, signature);
404 slist = curl_slist_append(slist, s);
406 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
414 static void _add_swift_header(CURL *curl, struct curl_slist *slist, struct http_options *o,
415 int op, const char *uri, char *buf, size_t len)
420 if (op == DDIR_WRITE) {
421 dsha = _gen_hex_md5(buf, len);
423 /* Surpress automatic Accept: header */
424 slist = curl_slist_append(slist, "Accept:");
426 snprintf(s, sizeof(s), "etag: %s", dsha);
427 slist = curl_slist_append(slist, s);
429 snprintf(s, sizeof(s), "x-auth-token: %s", o->swift_auth_token);
430 slist = curl_slist_append(slist, s);
432 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
437 static void fio_http_cleanup(struct thread_data *td)
439 struct http_data *http = td->io_ops_data;
442 curl_easy_cleanup(http->curl);
447 static size_t _http_read(void *ptr, size_t size, size_t nmemb, void *stream)
449 struct http_curl_stream *state = stream;
450 size_t len = size * nmemb;
451 /* We're retrieving; nothing is supposed to be read locally */
454 if (len+state->pos > state->max)
455 len = state->max - state->pos;
456 memcpy(ptr, &state->buf[state->pos], len);
461 static size_t _http_write(void *ptr, size_t size, size_t nmemb, void *stream)
463 struct http_curl_stream *state = stream;
464 /* We're just discarding the returned body after a PUT */
468 return CURLE_WRITE_ERROR;
469 if (nmemb + state->pos > state->max)
470 return CURLE_WRITE_ERROR;
471 memcpy(&state->buf[state->pos], ptr, nmemb);
476 static int _http_seek(void *stream, curl_off_t offset, int origin)
478 struct http_curl_stream *state = stream;
479 if (offset < state->max && origin == SEEK_SET) {
481 return CURL_SEEKFUNC_OK;
483 return CURL_SEEKFUNC_FAIL;
486 static enum fio_q_status fio_http_queue(struct thread_data *td,
489 struct http_data *http = td->io_ops_data;
490 struct http_options *o = td->eo;
491 struct http_curl_stream _curl_stream;
492 struct curl_slist *slist = NULL;
499 fio_ro_check(td, io_u);
500 memset(&_curl_stream, 0, sizeof(_curl_stream));
501 snprintf(object, sizeof(object), "%s_%llu_%llu", td->files[0]->file_name,
502 io_u->offset, io_u->xfer_buflen);
503 if (o->https == FIO_HTTPS_OFF)
504 snprintf(url, sizeof(url), "http://%s%s", o->host, object);
506 snprintf(url, sizeof(url), "https://%s%s", o->host, object);
507 curl_easy_setopt(http->curl, CURLOPT_URL, url);
508 _curl_stream.buf = io_u->xfer_buf;
509 _curl_stream.max = io_u->xfer_buflen;
510 curl_easy_setopt(http->curl, CURLOPT_SEEKDATA, &_curl_stream);
511 curl_easy_setopt(http->curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)io_u->xfer_buflen);
513 if (o->mode == FIO_HTTP_S3)
514 _add_aws_auth_header(http->curl, slist, o, io_u->ddir, object,
515 io_u->xfer_buf, io_u->xfer_buflen);
516 else if (o->mode == FIO_HTTP_SWIFT)
517 _add_swift_header(http->curl, slist, o, io_u->ddir, object,
518 io_u->xfer_buf, io_u->xfer_buflen);
520 if (io_u->ddir == DDIR_WRITE) {
521 curl_easy_setopt(http->curl, CURLOPT_READDATA, &_curl_stream);
522 curl_easy_setopt(http->curl, CURLOPT_WRITEDATA, NULL);
523 curl_easy_setopt(http->curl, CURLOPT_UPLOAD, 1L);
524 res = curl_easy_perform(http->curl);
525 if (res == CURLE_OK) {
526 curl_easy_getinfo(http->curl, CURLINFO_RESPONSE_CODE, &status);
527 if (status == 100 || (status >= 200 && status <= 204))
529 log_err("DDIR_WRITE failed with HTTP status code %ld\n", status);
532 } else if (io_u->ddir == DDIR_READ) {
533 curl_easy_setopt(http->curl, CURLOPT_READDATA, NULL);
534 curl_easy_setopt(http->curl, CURLOPT_WRITEDATA, &_curl_stream);
535 curl_easy_setopt(http->curl, CURLOPT_HTTPGET, 1L);
536 res = curl_easy_perform(http->curl);
537 if (res == CURLE_OK) {
538 curl_easy_getinfo(http->curl, CURLINFO_RESPONSE_CODE, &status);
541 else if (status == 404) {
542 /* Object doesn't exist. Pretend we read
544 memset(io_u->xfer_buf, 0, io_u->xfer_buflen);
547 log_err("DDIR_READ failed with HTTP status code %ld\n", status);
550 } else if (io_u->ddir == DDIR_TRIM) {
551 curl_easy_setopt(http->curl, CURLOPT_HTTPGET, 1L);
552 curl_easy_setopt(http->curl, CURLOPT_CUSTOMREQUEST, "DELETE");
553 curl_easy_setopt(http->curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)0);
554 curl_easy_setopt(http->curl, CURLOPT_READDATA, NULL);
555 curl_easy_setopt(http->curl, CURLOPT_WRITEDATA, NULL);
556 res = curl_easy_perform(http->curl);
557 if (res == CURLE_OK) {
558 curl_easy_getinfo(http->curl, CURLINFO_RESPONSE_CODE, &status);
559 if (status == 200 || status == 202 || status == 204 || status == 404)
561 log_err("DDIR_TRIM failed with HTTP status code %ld\n", status);
566 log_err("WARNING: Only DDIR_READ/DDIR_WRITE/DDIR_TRIM are supported!\n");
570 td_verror(td, io_u->error, "transfer");
572 curl_slist_free_all(slist);
573 return FIO_Q_COMPLETED;
576 static struct io_u *fio_http_event(struct thread_data *td, int event)
578 /* sync IO engine - never any outstanding events */
582 int fio_http_getevents(struct thread_data *td, unsigned int min,
583 unsigned int max, const struct timespec *t)
585 /* sync IO engine - never any outstanding events */
589 static int fio_http_setup(struct thread_data *td)
591 struct http_data *http = NULL;
592 struct http_options *o = td->eo;
594 /* allocate engine specific structure to deal with libhttp. */
595 http = calloc(1, sizeof(*http));
597 log_err("calloc failed.\n");
601 http->curl = curl_easy_init();
603 curl_easy_setopt(http->curl, CURLOPT_VERBOSE, 1L);
605 curl_easy_setopt(http->curl, CURLOPT_DEBUGFUNCTION, &_curl_trace);
606 curl_easy_setopt(http->curl, CURLOPT_NOPROGRESS, 1L);
607 curl_easy_setopt(http->curl, CURLOPT_FOLLOWLOCATION, 1L);
608 curl_easy_setopt(http->curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS);
609 if (o->https == FIO_HTTPS_INSECURE) {
610 curl_easy_setopt(http->curl, CURLOPT_SSL_VERIFYPEER, 0L);
611 curl_easy_setopt(http->curl, CURLOPT_SSL_VERIFYHOST, 0L);
613 curl_easy_setopt(http->curl, CURLOPT_READFUNCTION, _http_read);
614 curl_easy_setopt(http->curl, CURLOPT_WRITEFUNCTION, _http_write);
615 curl_easy_setopt(http->curl, CURLOPT_SEEKFUNCTION, &_http_seek);
616 if (o->user && o->pass) {
617 curl_easy_setopt(http->curl, CURLOPT_USERNAME, o->user);
618 curl_easy_setopt(http->curl, CURLOPT_PASSWORD, o->pass);
619 curl_easy_setopt(http->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
622 td->io_ops_data = http;
624 /* Force single process mode. */
625 td->o.use_thread = 1;
629 fio_http_cleanup(td);
633 static int fio_http_open(struct thread_data *td, struct fio_file *f)
637 static int fio_http_invalidate(struct thread_data *td, struct fio_file *f)
642 static struct ioengine_ops ioengine = {
644 .version = FIO_IOOPS_VERSION,
645 .flags = FIO_DISKLESSIO | FIO_SYNCIO,
646 .setup = fio_http_setup,
647 .queue = fio_http_queue,
648 .getevents = fio_http_getevents,
649 .event = fio_http_event,
650 .cleanup = fio_http_cleanup,
651 .open_file = fio_http_open,
652 .invalidate = fio_http_invalidate,
654 .option_struct_size = sizeof(struct http_options),
657 static void fio_init fio_http_register(void)
659 register_ioengine(&ioengine);
662 static void fio_exit fio_http_unregister(void)
664 unregister_ioengine(&ioengine);