From 2ca9a394063baac075e05b14fcc6c027027ab8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20B=C5=99ezina?= Date: Fri, 24 Feb 2017 10:40:43 +0100 Subject: [PATCH 74/90] tcurl: add support for ssl and raw output At first, this patch separates curl_easy handle from the multi-handle processing and makes it encapsulated in custom tcurl_request structure. This allows us to separate protocol initialization from its asynchonous logic which gives us the ability to set different options for each request without over-extending the parameter list. In this patch we implement options for peer verification for TLS-enabled protocols and to return response with body and headers together. Reviewed-by: Simo Sorce Reviewed-by: Jakub Hrozek (cherry picked from commit 300b9e9217ee1ed8d845ed2370c5ccf5c87afb36) --- src/tests/tcurl_test_tool.c | 41 +- src/util/tev_curl.c | 992 +++++++++++++++++++++++++------------------- src/util/tev_curl.h | 172 +++++++- src/util/util_errors.c | 4 + src/util/util_errors.h | 4 + 5 files changed, 755 insertions(+), 458 deletions(-) diff --git a/src/tests/tcurl_test_tool.c b/src/tests/tcurl_test_tool.c index 2af950ebb76a22bdf4a6dfd58442b10486e64293..9a6266f89131ffd3a561e857af85df9854c44949 100644 --- a/src/tests/tcurl_test_tool.c +++ b/src/tests/tcurl_test_tool.c @@ -42,9 +42,7 @@ static void request_done(struct tevent_req *req) struct tool_ctx *tool_ctx = tevent_req_callback_data(req, struct tool_ctx); - tool_ctx->error = tcurl_http_recv(tool_ctx, req, - &http_code, - &outbuf); + tool_ctx->error = tcurl_request_recv(tool_ctx, req, &outbuf, &http_code); talloc_zfree(req); if (tool_ctx->error != EOK) { @@ -87,16 +85,17 @@ int main(int argc, const char *argv[]) "The path to the HTTP server socket", NULL }, { "get", 'g', POPT_ARG_NONE, NULL, 'g', "Perform a HTTP GET (default)", NULL }, { "put", 'p', POPT_ARG_NONE, NULL, 'p', "Perform a HTTP PUT", NULL }, - { "del", 'd', POPT_ARG_NONE, NULL, 'd', "Perform a HTTP DELETE", NULL }, { "post", 'o', POPT_ARG_NONE, NULL, 'o', "Perform a HTTP POST", NULL }, + { "del", 'd', POPT_ARG_NONE, NULL, 'd', "Perform a HTTP DELETE", NULL }, { "verbose", 'v', POPT_ARG_NONE, NULL, 'v', "Print response code and body", NULL }, POPT_TABLEEND }; struct tevent_req *req; struct tevent_context *ev; - enum tcurl_http_request req_type = TCURL_HTTP_GET; + enum tcurl_http_method method = TCURL_HTTP_GET; struct tcurl_ctx *ctx; + struct tcurl_request *tcurl_req; struct tool_ctx *tool_ctx; const char *urls[MAXREQ] = { 0 }; @@ -111,16 +110,16 @@ int main(int argc, const char *argv[]) while ((opt = poptGetNextOpt(pc)) > 0) { switch (opt) { case 'g': - req_type = TCURL_HTTP_GET; + method = TCURL_HTTP_GET; break; case 'p': - req_type = TCURL_HTTP_PUT; - break; - case 'd': - req_type = TCURL_HTTP_DELETE; + method = TCURL_HTTP_PUT; break; case 'o': - req_type = TCURL_HTTP_POST; + method = TCURL_HTTP_POST; + break; + case 'd': + method = TCURL_HTTP_DELETE; break; case 'v': pc_verbose = 1; @@ -146,7 +145,7 @@ int main(int argc, const char *argv[]) } while ((extra_arg_ptr = poptGetArg(pc)) != NULL) { - switch (req_type) { + switch(method) { case TCURL_HTTP_GET: case TCURL_HTTP_DELETE: case TCURL_HTTP_POST: @@ -203,14 +202,16 @@ int main(int argc, const char *argv[]) } for (size_t i = 0; i < n_reqs; i++) { - req = tcurl_http_send(tool_ctx, ev, ctx, - req_type, - socket_path, - urls[i], - headers, - inbufs[i], - 5); - if (req == NULL) { + tcurl_req = tcurl_http(tool_ctx, method, socket_path, + urls[i], headers, inbufs[i]); + if (tcurl_req == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Unable to create TCURL request\n"); + talloc_zfree(tool_ctx); + return 1; + } + + req = tcurl_request_send(tool_ctx, ev, ctx, tcurl_req, 10); + if (ctx == NULL) { DEBUG(SSSDBG_FATAL_FAILURE, "Could not create request\n"); talloc_zfree(tool_ctx); return 1; diff --git a/src/util/tev_curl.c b/src/util/tev_curl.c index 645d1182d10f825f209f48e0ba7e6804dde1971c..c155f4c038d4215933ee30d41c694ad4a14ae132 100644 --- a/src/util/tev_curl.c +++ b/src/util/tev_curl.c @@ -34,8 +34,8 @@ #include "util/util.h" #include "util/tev_curl.h" -#define IOBUF_CHUNK 1024 -#define IOBUF_MAX 4096 +#define TCURL_IOBUF_CHUNK 1024 +#define TCURL_IOBUF_MAX 4096 static bool global_is_curl_initialized; @@ -71,39 +71,12 @@ struct tcurl_sock { struct tevent_fd *fde; /* tevent tracker of the fd events */ }; -/** - * @brief A state of one curl transfer - * - * Intentionally breaking the tevent coding style here and making the struct available - * in the whole module so that the structure is available to curl callbacks that - * need to access the state of the transfer. - * - * @see handle_curlmsg_done() - */ -struct tcurl_http_state { - /* Input parameters */ - struct tcurl_ctx *tctx; - const char *socket_path; - const char *url; - int timeout; - struct sss_iobuf *inbuf; - - /* Internal state */ - CURL *http_handle; - struct curl_slist *curl_headers; - - /* Output data */ - struct sss_iobuf *outbuf; - long http_code; -}; +static void tcurl_request_done(struct tevent_req *req, + errno_t process_error, + int response_code); static errno_t curl_code2errno(CURLcode crv) { - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "curl error %d: %s\n", crv, curl_easy_strerror(crv)); - } - switch (crv) { /* HTTP error does not fail the whole request, just returns the error * separately @@ -121,6 +94,47 @@ static errno_t curl_code2errno(CURLcode crv) return ENOMEM; case CURLE_OPERATION_TIMEDOUT: return ETIMEDOUT; + case CURLE_SSL_ISSUER_ERROR: + case CURLE_SSL_CACERT_BADFILE: + case CURLE_SSL_CACERT: + case CURLE_SSL_CERTPROBLEM: + return ERR_INVALID_CERT; + + case CURLE_SSL_CRL_BADFILE: + case CURLE_SSL_SHUTDOWN_FAILED: + case CURLE_SSL_ENGINE_INITFAILED: + case CURLE_USE_SSL_FAILED: + case CURLE_SSL_CIPHER: + case CURLE_SSL_ENGINE_SETFAILED: + case CURLE_SSL_ENGINE_NOTFOUND: + case CURLE_SSL_CONNECT_ERROR: + return ERR_SSL_FAILURE; + case CURLE_PEER_FAILED_VERIFICATION: + return ERR_UNABLE_TO_VERIFY_PEER; + case CURLE_COULDNT_RESOLVE_HOST: + return ERR_UNABLE_TO_RESOLVE_HOST; + default: + break; + } + + return EIO; +} + +static errno_t curlm_code2errno(CURLcode crv) +{ + switch (crv) { + case CURLM_OK: + return EOK; + case CURLM_BAD_SOCKET: + return EPIPE; + case CURLM_OUT_OF_MEMORY: + return ENOMEM; + case CURLM_BAD_HANDLE: + case CURLM_BAD_EASY_HANDLE: + case CURLM_UNKNOWN_OPTION: + return EINVAL; + case CURLM_INTERNAL_ERROR: + return ERR_INTERNAL; default: break; } @@ -145,22 +159,6 @@ static errno_t tcurl_global_init(void) return EOK; } -static const char *http_req2str(enum tcurl_http_request req) -{ - switch (req) { - case TCURL_HTTP_GET: - return "GET"; - case TCURL_HTTP_PUT: - return "PUT"; - case TCURL_HTTP_DELETE: - return "DELETE"; - case TCURL_HTTP_POST: - return "POST"; - } - - return "Uknown request type"; -} - static int curl2tev_flags(int curlflags) { int flags = 0; @@ -185,9 +183,9 @@ static void handle_curlmsg_done(CURLMsg *message) CURL *easy_handle; CURLcode crv; struct tevent_req *req; + long response_code = 0; char *done_url; errno_t ret; - struct tcurl_http_state *state; easy_handle = message->easy_handle; if (easy_handle == NULL) { @@ -198,9 +196,8 @@ static void handle_curlmsg_done(CURLMsg *message) if (DEBUG_IS_SET(SSSDBG_TRACE_FUNC)) { crv = curl_easy_getinfo(easy_handle, CURLINFO_EFFECTIVE_URL, &done_url); if (crv != CURLE_OK) { - DEBUG(SSSDBG_MINOR_FAILURE, - "Cannot get CURLINFO_EFFECTIVE_URL [%d]: %s\n", - crv, curl_easy_strerror(crv)); + DEBUG(SSSDBG_MINOR_FAILURE, "Cannot get CURLINFO_EFFECTIVE_URL " + "[%d]: %s\n", crv, curl_easy_strerror(crv)); /* not fatal since we need this only for debugging */ } else { DEBUG(SSSDBG_TRACE_FUNC, "Handled %s\n", done_url); @@ -209,38 +206,32 @@ static void handle_curlmsg_done(CURLMsg *message) crv = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (void *) &req); if (crv != CURLE_OK) { - DEBUG(SSSDBG_CRIT_FAILURE, - "Cannot get CURLINFO_PRIVATE [%d]: %s\n", + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot get CURLINFO_PRIVATE [%d]: %s\n", crv, curl_easy_strerror(crv)); - return; - } - - state = tevent_req_data(req, struct tcurl_http_state); - if (state == NULL) { - DEBUG(SSSDBG_CRIT_FAILURE, "BUG: request has no state\n"); - tevent_req_error(req, EFAULT); - return; + ret = curl_code2errno(crv); + goto done; } ret = curl_code2errno(message->data.result); if (ret != EOK) { - DEBUG(SSSDBG_OP_FAILURE, - "curl operation failed [%d]: %s\n", ret, sss_strerror(ret)); - tevent_req_error(req, ret); - return; + DEBUG(SSSDBG_OP_FAILURE, "CURL operation failed [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; } - /* If there was no fatal error, let's read the HTTP error code and mark - * the request as done - */ - crv = curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &state->http_code); + /* If there was no fatal error, let's read the response code + * and mark the request as done */ + crv = curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &response_code); if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, "Cannot get HTTP status code\n"); - tevent_req_error(req, EFAULT); - return; + DEBUG(SSSDBG_OP_FAILURE, "Cannot get response code\n"); + ret = curl_code2errno(crv); + goto done; } - tevent_req_done(req); + ret = EOK; + +done: + tcurl_request_done(req, ret, response_code); } static void process_curl_activity(struct tcurl_ctx *tctx) @@ -551,346 +542,42 @@ fail: return NULL; } -static errno_t tcurl_add_headers(struct tcurl_http_state *state, - const char *headers[]); - -static errno_t tcurl_set_options(struct tcurl_http_state *state, - struct tevent_req *req, - enum tcurl_http_request req_type); - -static int tcurl_http_cleanup_handle(TALLOC_CTX *ptr); - -static size_t tcurl_http_write_data(char *ptr, - size_t size, - size_t nmemb, - void *userdata); - -static size_t tcurl_http_read_data(void *ptr, - size_t size, - size_t nmemb, - void *userdata); - -struct tevent_req *tcurl_http_send(TALLOC_CTX *mem_ctx, - struct tevent_context *ev, - struct tcurl_ctx *tctx, - enum tcurl_http_request req_type, - const char *socket_path, - const char *url, - const char *headers[], - struct sss_iobuf *req_data, - int timeout) -{ - errno_t ret; - struct tevent_req *req; - struct tcurl_http_state *state; - - req = tevent_req_create(mem_ctx, &state, struct tcurl_http_state); - if (req == NULL) { - return NULL; - } - - state->tctx = tctx; - state->socket_path = socket_path; - state->url = url; - state->inbuf = req_data; - state->timeout = timeout; - - state->outbuf = sss_iobuf_init_empty(state, IOBUF_CHUNK, IOBUF_MAX); - if (state->outbuf == NULL) { - ret = ENOMEM; - goto fail; - } - - DEBUG(SSSDBG_TRACE_FUNC, - "HTTP request %s for URL %s\n", http_req2str(req_type), url); - talloc_set_destructor((TALLOC_CTX *) state, tcurl_http_cleanup_handle); - - /* All transfer share the same multi handle, but each trasfer has its own - * easy handle we can use to set per-transfer options - */ - state->http_handle = curl_easy_init(); - if (state->http_handle == NULL) { - DEBUG(SSSDBG_CRIT_FAILURE, "curl_easy_init failed\n"); - ret = EIO; - goto fail; - } - - ret = tcurl_add_headers(state, headers); - if (ret != EOK) { - DEBUG(SSSDBG_CRIT_FAILURE, - "Failed to set CURL headers [%d]: %s\n", ret, sss_strerror(ret)); - goto fail; - } - - ret = tcurl_set_options(state, req, req_type); - if (ret != EOK) { - DEBUG(SSSDBG_CRIT_FAILURE, - "Failed to set CURL options [%d]: %s\n", ret, sss_strerror(ret)); - goto fail; - } - - /* Pass control to the curl handling which will mark the request as - * done - */ - curl_multi_add_handle(tctx->multi_handle, state->http_handle); - - return req; - -fail: - tevent_req_error(req, ret); - tevent_req_post(req, ev); - return req; -} - -static int tcurl_http_cleanup_handle(TALLOC_CTX *ptr) -{ - struct tcurl_http_state *state = talloc_get_type(ptr, struct tcurl_http_state); - - if (state == NULL) { - return 0; - } - - /* it is safe to pass NULL here */ - curl_multi_remove_handle(state->tctx->multi_handle, state->http_handle); - curl_slist_free_all(state->curl_headers); - curl_easy_cleanup(state->http_handle); - return 0; -} - -static errno_t tcurl_add_headers(struct tcurl_http_state *state, - const char *headers[]) -{ - if (headers == NULL) { - return EOK; - } - - /* The headers will be freed later in tcurl_http_cleanup_handle */ - for (int i = 0; headers[i] != NULL; i++) { - state->curl_headers = curl_slist_append(state->curl_headers, headers[i]); - if (state->curl_headers == NULL) { - DEBUG(SSSDBG_CRIT_FAILURE, "Cannot add header %s\n", headers[i]); - return ENOMEM; - } - } - - /* Add a dummy header to suppress libcurl adding Expect 100-continue which - * was causing libcurl to always wait for the internal timeout when sending - * a PUT/PATCH request - */ - state->curl_headers = curl_slist_append(state->curl_headers, "Expect:"); - if (state->curl_headers == NULL) { - DEBUG(SSSDBG_CRIT_FAILURE, "Cannot add the dummy expect header\n"); - return ENOMEM; - } - - return EOK; -} - -static errno_t tcurl_set_common_options(struct tcurl_http_state *state, - struct tevent_req *req) -{ - CURLcode crv; - - crv = curl_easy_setopt(state->http_handle, - CURLOPT_HTTPHEADER, - state->curl_headers); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set HTTP headers [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - crv = curl_easy_setopt(state->http_handle, - CURLOPT_UNIX_SOCKET_PATH, - state->socket_path); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set UNIX socket path %s [%d]: %s\n", - state->socket_path, crv, curl_easy_strerror(crv)); - return EIO; - } - - crv = curl_easy_setopt(state->http_handle, CURLOPT_URL, state->url); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set URL %s [%d]: %s\n", - state->url, crv, curl_easy_strerror(crv)); - return EIO; - } - - crv = curl_easy_setopt(state->http_handle, CURLOPT_PRIVATE, req); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set private data [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - if (state->timeout > 0) { - crv = curl_easy_setopt(state->http_handle, - CURLOPT_TIMEOUT, - state->timeout); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set timeout [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - } - - return EOK; -} - -static errno_t tcurl_set_write_options(struct tcurl_http_state *state) -{ - CURLcode crv; - - crv = curl_easy_setopt(state->http_handle, - CURLOPT_WRITEFUNCTION, - tcurl_http_write_data); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set write function [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - crv = curl_easy_setopt(state->http_handle, - CURLOPT_WRITEDATA, - state->outbuf); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set write data [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - return EOK; -} - -static errno_t tcurl_set_read_options(struct tcurl_http_state *state) -{ - CURLcode crv; - - crv = curl_easy_setopt(state->http_handle, - CURLOPT_READFUNCTION, - tcurl_http_read_data); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set read function [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - crv = curl_easy_setopt(state->http_handle, - CURLOPT_READDATA, - state->inbuf); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set read data [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - return EOK; -} - -static errno_t tcurl_set_options(struct tcurl_http_state *state, - struct tevent_req *req, - enum tcurl_http_request req_type) -{ - CURLcode crv; - errno_t ret; - - ret = tcurl_set_common_options(state, req); - if (ret != EOK) { - return ret; - } - - ret = tcurl_set_write_options(state); - if (ret != EOK) { - DEBUG(SSSDBG_CRIT_FAILURE, - "Failed to set write callbacks [%d]: %s\n", - ret, sss_strerror(ret)); - return ret; - } - - switch (req_type) { - case TCURL_HTTP_POST: - crv = curl_easy_setopt(state->http_handle, - CURLOPT_CUSTOMREQUEST, - "POST"); - break; - case TCURL_HTTP_PUT: - /* CURLOPT_UPLOAD enables HTTP_PUT */ - crv = curl_easy_setopt(state->http_handle, - CURLOPT_UPLOAD, - 1L); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set the uplodad option [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - /* Causes libcurl to add a sane Content-Length header */ - crv = curl_easy_setopt(state->http_handle, - CURLOPT_INFILESIZE_LARGE, - (curl_off_t) sss_iobuf_get_size(state->inbuf)); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set CURLOPT_INFILESIZE_LARGE [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - - ret = tcurl_set_read_options(state); - if (ret != EOK) { - DEBUG(SSSDBG_CRIT_FAILURE, - "Failed to set write callbacks [%d]: %s\n", - ret, sss_strerror(ret)); - return ret; - } - break; - case TCURL_HTTP_GET: - /* GET just needs the write callbacks, nothing to do here.. */ - break; - case TCURL_HTTP_DELETE: - crv = curl_easy_setopt(state->http_handle, - CURLOPT_CUSTOMREQUEST, - "DELETE"); - if (crv != CURLE_OK) { - DEBUG(SSSDBG_OP_FAILURE, - "Failed to set the uplodad option [%d]: %s\n", - crv, curl_easy_strerror(crv)); - return EIO; - } - break; - default: - return EFAULT; - } - - return EOK; -} - -static size_t tcurl_http_write_data(char *ptr, - size_t size, - size_t nmemb, - void *userdata) +#define tcurl_set_option(tcurl_req, option, value) \ +({ \ + CURLcode __curl_code; \ + errno_t __ret; \ + \ + __curl_code = curl_easy_setopt((tcurl_req)->curl_easy_handle, \ + (option), (value)); \ + if (__curl_code == CURLE_OK) { \ + __ret = EOK; \ + } else { \ + DEBUG(SSSDBG_OP_FAILURE, "Failed to set CURL option %s [%d]: %s\n", \ + #option, __curl_code, curl_easy_strerror(__curl_code)); \ + __ret = curl_code2errno(__curl_code); \ + } \ + __ret; \ +}) + +static size_t tcurl_write_data(char *ptr, + size_t size, + size_t nmemb, + void *userdata) { errno_t ret; size_t realsize = size * nmemb; - struct sss_iobuf *outbuf = talloc_get_type(userdata, struct sss_iobuf); + struct sss_iobuf *outbuf; + + outbuf = talloc_get_type(userdata, struct sss_iobuf); DEBUG(SSSDBG_TRACE_INTERNAL, "---> begin libcurl data\n"); DEBUG(SSSDBG_TRACE_INTERNAL, "%s\n", ptr); DEBUG(SSSDBG_TRACE_INTERNAL, "<--- end libcurl data\n"); - ret = sss_iobuf_write_len(outbuf, (uint8_t *) ptr, realsize); + ret = sss_iobuf_write_len(outbuf, (uint8_t *)ptr, realsize); if (ret != EOK) { - DEBUG(SSSDBG_CRIT_FAILURE, - "Failed to write data to buffer [%d]: %s\n", ret, sss_strerror(ret)); + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to write data to buffer [%d]: %s\n", + ret, sss_strerror(ret)); /* zero signifies an EOF */ return 0; } @@ -898,14 +585,16 @@ static size_t tcurl_http_write_data(char *ptr, return realsize; } -static size_t tcurl_http_read_data(void *ptr, - size_t size, - size_t nmemb, - void *userdata) +static size_t tcurl_read_data(void *ptr, + size_t size, + size_t nmemb, + void *userdata) { errno_t ret; size_t readbytes; - struct sss_iobuf *inbuf = (struct sss_iobuf *) userdata; + struct sss_iobuf *inbuf; + + inbuf = talloc_get_type(userdata, struct sss_iobuf); if (inbuf == NULL) { return CURL_READFUNC_ABORT; @@ -919,22 +608,487 @@ static size_t tcurl_http_read_data(void *ptr, return readbytes; } -int tcurl_http_recv(TALLOC_CTX *mem_ctx, - struct tevent_req *req, - int *_http_code, - struct sss_iobuf **_outbuf) + +struct tcurl_request { + CURL *curl_easy_handle; + + struct sss_iobuf *body; + struct curl_slist *headers; + + const char *url; + const char *socket; + + /* Associated tcurl context if this request is in progress. */ + struct tcurl_ctx *tcurl_ctx; +}; + +struct tcurl_request_state { + struct tcurl_request *tcurl_req; + struct sss_iobuf *response; + int response_code; +}; + +struct tevent_req * +tcurl_request_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct tcurl_ctx *tcurl_ctx, + struct tcurl_request *tcurl_req, + long int timeout) { - struct tcurl_http_state *state = tevent_req_data(req, struct tcurl_http_state); + struct tcurl_request_state *state; + struct tevent_req *req; + CURLMcode curl_code; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, struct tcurl_request_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create tevent request!\n"); + return NULL; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Sending TCURL request for %s, at socket %s\n", + tcurl_req->url == NULL ? "" : tcurl_req->url, + tcurl_req->socket == NULL ? "" : tcurl_req->socket); + + state->tcurl_req = talloc_steal(state, tcurl_req); + + state->response = sss_iobuf_init_empty(state, TCURL_IOBUF_CHUNK, TCURL_IOBUF_MAX); + if (state->response == NULL) { + ret = ENOMEM; + goto done; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_PRIVATE, req); + if (ret != EOK) { + goto done; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_TIMEOUT, timeout); + if (ret != EOK) { + goto done; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_WRITEFUNCTION, tcurl_write_data); + if (ret != EOK) { + goto done; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_WRITEDATA, state->response); + if (ret != EOK) { + goto done; + } + + if (tcurl_req->body != NULL) { + ret = tcurl_set_option(tcurl_req, CURLOPT_READFUNCTION, tcurl_read_data); + if (ret != EOK) { + goto done; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_READDATA, tcurl_req->body); + if (ret != EOK) { + goto done; + } + } + + curl_code = curl_multi_add_handle(tcurl_ctx->multi_handle, + tcurl_req->curl_easy_handle); + if (curl_code != CURLM_OK) { + ret = curlm_code2errno(curl_code); + goto done; + } + + tcurl_req->tcurl_ctx = tcurl_ctx; + + ret = EAGAIN; + +done: + if (ret == EOK) { + tevent_req_done(req); + tevent_req_post(req, ev); + } else if (ret != EAGAIN) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void tcurl_request_done(struct tevent_req *req, + errno_t process_error, + int response_code) +{ + struct tcurl_request_state *state; + + DEBUG(SSSDBG_TRACE_FUNC, "TCURL request finished [%d]: %s\n", + process_error, sss_strerror(process_error)); + + if (req == NULL) { + /* To handle case where we fail to obtain request from private data. */ + DEBUG(SSSDBG_MINOR_FAILURE, "No tevent request provided!\n"); + return; + } + + state = tevent_req_data(req, struct tcurl_request_state); + + curl_multi_remove_handle(state->tcurl_req->tcurl_ctx->multi_handle, + state->tcurl_req->curl_easy_handle); + + /* This request is no longer associated with tcurl context. */ + state->tcurl_req->tcurl_ctx = NULL; + + if (process_error != EOK) { + tevent_req_error(req, process_error); + return; + } + + state->response_code = response_code; + + tevent_req_done(req); + return; +} + +errno_t tcurl_request_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_iobuf **_response, + int *_response_code) +{ + struct tcurl_request_state *state; + state = tevent_req_data(req, struct tcurl_request_state); TEVENT_REQ_RETURN_ON_ERROR(req); - if (_http_code != NULL) { - *_http_code = state->http_code; + if (_response != NULL) { + *_response = talloc_steal(mem_ctx, state->response); } - if (_outbuf != NULL) { - *_outbuf = talloc_steal(mem_ctx, state->outbuf); + if (_response_code != NULL) { + *_response_code = state->response_code; + } + + return EOK; +} + +static struct curl_slist * +tcurl_add_header(struct curl_slist *slist, const char *header) +{ + struct curl_slist *new; + + new = curl_slist_append(slist, header); + if (new == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot add header %s\n", header); + if (slist != NULL) { + curl_slist_free_all(slist); + } + + return NULL; + } + + return new; +} + +static errno_t +tcurl_construct_headers(const char **headers, + struct curl_slist **_slist) +{ + struct curl_slist *slist = NULL; + int i; + + if (headers == NULL || headers[0] == NULL) { + *_slist = NULL; + return EOK; + } + + for (i = 0; headers[i] != NULL; i++) { + slist = tcurl_add_header(slist, headers[i]); + if (slist == NULL) { + return ENOMEM; + } + } + + /* Add a dummy header to suppress libcurl adding Expect 100-continue which + * was causing libcurl to always wait for the internal timeout when sending + * a PUT/POST request because secrets responder does not implement this. + */ + slist = tcurl_add_header(slist, "Expect: "); + if (slist == NULL) { + return ENOMEM; + } + + *_slist = slist; + + return EOK; +} + +static int +tcurl_request_destructor(struct tcurl_request *tcurl_req) +{ + if (tcurl_req->tcurl_ctx != NULL) { + DEBUG(SSSDBG_MINOR_FAILURE, "Terminating TCURL request...\n"); + curl_multi_remove_handle(tcurl_req->tcurl_ctx->multi_handle, + tcurl_req->curl_easy_handle); + } + + if (tcurl_req->headers != NULL) { + curl_slist_free_all(tcurl_req->headers); + } + + if (tcurl_req->curl_easy_handle != NULL) { + curl_easy_cleanup(tcurl_req->curl_easy_handle); } return 0; } + +static struct tcurl_request * +tcurl_request_create(TALLOC_CTX *mem_ctx, + const char *socket_path, + const char *url, + const char **headers, + struct sss_iobuf *body) +{ + struct tcurl_request *tcurl_req; + errno_t ret; + + tcurl_req = talloc_zero(mem_ctx, struct tcurl_request); + if (tcurl_req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return NULL; + } + + if (url == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "URL cannot be NULL!\n"); + ret = EINVAL; + goto done; + } + + /* Setup a curl easy handle. This handle contains state for the request + * and is later associated with curl multi handle which performs + * asynchronous processing. */ + tcurl_req->curl_easy_handle = curl_easy_init(); + if (tcurl_req->curl_easy_handle == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to initialize curl easy handle!\n"); + ret = ENOMEM; + goto done; + } + + tcurl_req->url = talloc_strdup(tcurl_req, url); + if (tcurl_req->url == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + if (socket_path != NULL) { + tcurl_req->socket = talloc_strdup(tcurl_req, socket_path); + if (tcurl_req->socket == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + } + + ret = tcurl_construct_headers(headers, &tcurl_req->headers); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to construct headers [%d]: %s\n", + ret, sss_strerror(ret)); + ret = ENOMEM; + goto done; + } + + tcurl_req->body = body; + + talloc_set_destructor(tcurl_req, tcurl_request_destructor); + + ret = tcurl_set_option(tcurl_req, CURLOPT_URL, url); + if (ret != EOK) { + goto done; + } + + if (socket_path != NULL) { + ret = tcurl_set_option(tcurl_req, CURLOPT_UNIX_SOCKET_PATH, socket_path); + if (ret != EOK) { + goto done; + } + } + + if (body != NULL) { + /* Curl will tell the underlying protocol about incoming data length. + * In case of HTTP it will add a sane Content-Length header. */ + ret = tcurl_set_option(tcurl_req, CURLOPT_INFILESIZE_LARGE, + (curl_off_t)sss_iobuf_get_size(body)); + if (ret != EOK) { + goto done; + } + } + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(tcurl_req); + return NULL; + } + + return tcurl_req; +} + +struct tcurl_request *tcurl_http(TALLOC_CTX *mem_ctx, + enum tcurl_http_method method, + const char *socket_path, + const char *url, + const char **headers, + struct sss_iobuf *body) +{ + struct tcurl_request *tcurl_req; + errno_t ret; + + tcurl_req = tcurl_request_create(mem_ctx, socket_path, url, headers, body); + if (tcurl_req == NULL) { + return NULL; + } + + /* Set HTTP specific options. */ + + ret = tcurl_set_option(tcurl_req, CURLOPT_HTTPHEADER, tcurl_req->headers); + if (ret != EOK) { + goto done; + } + + switch (method) { + case TCURL_HTTP_GET: + /* Nothing to do here. GET is default. */ + break; + case TCURL_HTTP_PUT: + ret = tcurl_set_option(tcurl_req, CURLOPT_UPLOAD, 1L); + if (ret != EOK) { + goto done; + } + break; + case TCURL_HTTP_POST: + ret = tcurl_set_option(tcurl_req, CURLOPT_CUSTOMREQUEST, "POST"); + if (ret != EOK) { + goto done; + } + break; + case TCURL_HTTP_DELETE: + ret = tcurl_set_option(tcurl_req, CURLOPT_CUSTOMREQUEST, "DELETE"); + if (ret != EOK) { + goto done; + } + break; + } + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(tcurl_req); + return NULL; + } + + return tcurl_req; +} + +struct tevent_req *tcurl_http_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct tcurl_ctx *tcurl_ctx, + enum tcurl_http_method method, + const char *socket_path, + const char *url, + const char **headers, + struct sss_iobuf *body, + int timeout) +{ + struct tcurl_request *tcurl_req; + struct tevent_req *req; + + tcurl_req = tcurl_http(mem_ctx, method, socket_path, url, headers, body); + if (tcurl_req == NULL) { + return NULL; + } + + req = tcurl_request_send(mem_ctx, ev, tcurl_ctx, tcurl_req, timeout); + if (req == NULL) { + talloc_free(tcurl_req); + } + + return req; +} + +errno_t tcurl_http_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + int *_http_code, + struct sss_iobuf **_response) +{ + return tcurl_request_recv(mem_ctx, req, _response, _http_code); +} + +errno_t tcurl_req_enable_rawoutput(struct tcurl_request *tcurl_req) +{ + return tcurl_set_option(tcurl_req, CURLOPT_HEADER, 1L); +} + +errno_t tcurl_req_verify_peer(struct tcurl_request *tcurl_req, + const char *capath, + const char *cacert, + bool verify_peer, + bool verify_host) +{ + errno_t ret; + + long peer = verify_peer ? 1L : 0L; + long host = verify_host ? 2L : 0L; + + ret = tcurl_set_option(tcurl_req, CURLOPT_SSL_VERIFYPEER, peer); + if (ret != EOK) { + return ret; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_SSL_VERIFYHOST, host); + if (ret != EOK) { + return ret; + } + + if (capath != NULL) { + ret = tcurl_set_option(tcurl_req, CURLOPT_CAPATH, capath); + if (ret != EOK) { + return ret; + } + } + + if (cacert != NULL) { + ret = tcurl_set_option(tcurl_req, CURLOPT_CAINFO, cacert); + if (ret != EOK) { + return ret; + } + } + + return EOK; +} + +errno_t tcurl_req_set_client_cert(struct tcurl_request *tcurl_req, + const char *cert, + const char *key) +{ + errno_t ret; + + if (cert == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "You must specify client certificate!\n"); + return EINVAL; + } + + ret = tcurl_set_option(tcurl_req, CURLOPT_SSLCERT, cert); + if (ret != EOK) { + return ret; + } + + if (key != NULL) { + /* If client's private key is in separate file. */ + ret = tcurl_set_option(tcurl_req, CURLOPT_SSLKEY, key); + if (ret != EOK) { + return ret; + } + } + + return EOK; +} diff --git a/src/util/tev_curl.h b/src/util/tev_curl.h index 444eb286e09d189b4588e2b2152b5202df3914d8..933abcb9b531412737e8fcf391644d828b125cf8 100644 --- a/src/util/tev_curl.h +++ b/src/util/tev_curl.h @@ -27,14 +27,16 @@ #include "util/sss_iobuf.h" +struct tcurl_request; + /** - * @brief Supported HTTP requests + * @brief Supported HTTP methods */ -enum tcurl_http_request { +enum tcurl_http_method { TCURL_HTTP_GET, TCURL_HTTP_PUT, - TCURL_HTTP_DELETE, TCURL_HTTP_POST, + TCURL_HTTP_DELETE, }; /** @@ -46,16 +48,95 @@ struct tcurl_ctx *tcurl_init(TALLOC_CTX *mem_ctx, struct tevent_context *ev); /** + * @brief Run a single asynchronous TCURL request. + * + * If the libcurl processing succeeds but we obtain a protocol error we still + * mark the tevent request as successful. The protocol error is return from + * @tcurl_request_recv as an output parameter. + * + * @param[in] mem_ctx The talloc context that owns the request + * @param[in] ev Event loop context + * @param[in] tctx Use tcurl_init to get this context + * @param[in] tcurl_req TCURL request + * @param[in] timeout The request timeout in seconds. Use 0 if you want + * to use the default libcurl timeout. + * + * @returns A tevent request or NULL on allocation error. On other errors, we + * try to set the errno as event error code and run it to completion so that + * the programmer can use tcurl_request_recv to read the error code. + * + * @see tcurl_init + * @see tcurl_http + * @see tcurl_request_recv + */ +struct tevent_req * +tcurl_request_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct tcurl_ctx *tcurl_ctx, + struct tcurl_request *tcurl_req, + long int timeout); + +/** + * @brief Receive a result of a single asynchronous TCURL request. + * + * @param[in] mem_ctx The talloc context that owns the response + * @param[in] req The request previously obtained with tcurl_request_send + * @param[out] _response Response to the request + * @param[out] _response_code Protocol response code (may indicate a protocl error) + * + * @returns The error code of the curl request (not the HTTP code!) + */ +errno_t tcurl_request_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_iobuf **_response, + int *_response_code); + +/** + * @brief Create a HTTP request. + * + * Use this if you need better control over the request options. + * + * Headers are a NULL-terminated array of strings such as: + * static const char *headers[] = { + * "Content-type: application/octet-stream", + * NULL, + * }; + * + * @param[in] mem_ctx The talloc context that owns the tcurl_request + * @param[in] method TCURL HTTP method + * @param[in] socket_path The path to the UNIX socket to forward the + * request to, may be NULL. + * @param[in] url The request URL, cannot be NULL. + * @param[in] headers A NULL-terminated array of strings to use + * as additional HTTP headers. Pass NULL if you + * don't need any additional headers. + * @param[in] body The HTTP request input data. For some request + * types like DELETE, this is OK to leave as NULL. + * + * @returns A tcurl_request that can be later started with tcurl_request_send + * or NULL on error. + * + * @see tcurl_init + * @see tcurl_request_send + * @see tcurl_request_recv + */ +struct tcurl_request *tcurl_http(TALLOC_CTX *mem_ctx, + enum tcurl_http_method method, + const char *socket_path, + const char *url, + const char **headers, + struct sss_iobuf *body); + +/** * @brief Run a single asynchronous HTTP request. * - * Currently only UNIX sockets at socket_path are supported. + * Use this if you do not need control over additional request options. * * If the request runs into completion, but reports a failure with HTTP return * code, the request will be marked as done. Only if the request cannot run at * all (if e.g. the socket is unreachable), the request will fail completely. * - * Headers are a NULL-terminated - * array of strings such as: + * Headers are a NULL-terminated array of strings such as: * static const char *headers[] = { * "Content-type: application/octet-stream", * NULL, @@ -63,15 +144,15 @@ struct tcurl_ctx *tcurl_init(TALLOC_CTX *mem_ctx, * * @param[in] mem_ctx The talloc context that owns the iobuf * @param[in] ev Event loop context - * @param[in] tctx Use tcurl_init to get this context - * @param[in] req_type The request type + * @param[in] tcurl_ctx Use tcurl_init to get this context + * @param[in] method HTTP method * @param[in] socket_path The path to the UNIX socket to forward the - * request to - * @param[in] url The request URL + * request to, may be NULL. + * @param[in] url The request URL, cannot be NULL. * @param[in] headers A NULL-terminated array of strings to use * as additional HTTP headers. Pass NULL if you * don't need any additional headers. - * @param[in] req_data The HTTP request input data. For some request + * @param[in] body The HTTP request input data. For some request * types like DELETE, this is OK to leave as NULL. * @param[in] timeout The request timeout in seconds. Use 0 if you want * to use the default libcurl timeout. @@ -85,12 +166,12 @@ struct tcurl_ctx *tcurl_init(TALLOC_CTX *mem_ctx, */ struct tevent_req *tcurl_http_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, - struct tcurl_ctx *tctx, - enum tcurl_http_request req_type, + struct tcurl_ctx *tcurl_ctx, + enum tcurl_http_method method, const char *socket_path, const char *url, - const char *headers[], - struct sss_iobuf *req_data, + const char **headers, + struct sss_iobuf *body, int timeout); /** @@ -104,9 +185,62 @@ struct tevent_req *tcurl_http_send(TALLOC_CTX *mem_ctx, * * @returns The error code of the curl request (not the HTTP code!) */ -int tcurl_http_recv(TALLOC_CTX *mem_ctx, - struct tevent_req *req, - int *_http_code, - struct sss_iobuf **_outbuf); +errno_t tcurl_http_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + int *_http_code, + struct sss_iobuf **_response); + +/** + * @brief We are usually interested only in the reply body without protocol + * headers. Call this function on tcurl_request, if you want to include + * complete protocol response in the output buffer. + * + * @param[in] tcurl_request + * + * @returns errno code + * + * @see tcurl_http + */ +errno_t tcurl_req_enable_rawoutput(struct tcurl_request *tcurl_req); + +/** + * @brief TLS is enabled automatically by providing an URL that points to + * TLS-enabled protocol such as https. If you want to provide different + * path to CA directory or disable peer/hostname check explicitly, use + * this function on tcurl_request. + * + * @param[in] tcurl_request + * @param[in] capath Path to directory containing installed CA certificates. + * If not set, libcurl default is used. + * @param[ing cacert CA certificate. If NULL it is found in @capath. + * @param[in] verify_peer If false, the peer certificate is not verified. + * @param[in] verify_host If false, the host name provided in remote + * certificate may differ from the actual host name. + * + * @returns errno code + * + * @see tcurl_http + */ +errno_t tcurl_req_verify_peer(struct tcurl_request *tcurl_req, + const char *capath, + const char *cacert, + bool verify_peer, + bool verify_host); +/** + * @brief Some server require client verification during TLS setup. You can + * provide path to client's certificate file. If this file does not contain + * private key, you can specify a different file the holds the private key. + * + * @param[in] tcurl_request + * @param[in] cert Path to client's certificate. + * @param[in] key Path to client's private key. + * + * @returns errno code + * + * @see tcurl_http + */ +errno_t tcurl_req_set_client_cert(struct tcurl_request *tcurl_req, + const char *cert, + const char *key); #endif /* __TEV_CURL_H */ diff --git a/src/util/util_errors.c b/src/util/util_errors.c index 60c2f439b3e39b1dbff353e429114cb5a3070052..466a3b4062f39b29d831a5d8a62dc8d576eb2e97 100644 --- a/src/util/util_errors.c +++ b/src/util/util_errors.c @@ -111,6 +111,10 @@ struct err_string error_to_str[] = { { "Credential cache name not allowed" }, /* ERR_KCM_WRONG_CCNAME_FORMAT */ { "Cannot encode a JSON object to string" }, /* ERR_JSON_ENCODING */ { "Cannot decode a JSON object from string" }, /* ERR_JSON_DECODING */ + { "Invalid certificate provided" }, /* ERR_INVALID_CERT */ + { "Unable to initialize SSL" }, /* ERR_SSL_FAILURE */ + { "Unable to verify peer" }, /* ERR_UNABLE_TO_VERIFY_PEER */ + { "Unable to resolve host" }, /* ERR_UNABLE_TO_RESOLVE_HOST */ { "ERR_LAST" } /* ERR_LAST */ }; diff --git a/src/util/util_errors.h b/src/util/util_errors.h index 4e9da814702e2cd46edc52fd5c2ae5f640602609..2f90c0a5d65325a431a8e4d9a480170808c9198e 100644 --- a/src/util/util_errors.h +++ b/src/util/util_errors.h @@ -133,6 +133,10 @@ enum sssd_errors { ERR_KCM_WRONG_CCNAME_FORMAT, ERR_JSON_ENCODING, ERR_JSON_DECODING, + ERR_INVALID_CERT, + ERR_SSL_FAILURE, + ERR_UNABLE_TO_VERIFY_PEER, + ERR_UNABLE_TO_RESOLVE_HOST, ERR_LAST /* ALWAYS LAST */ }; -- 2.9.3