diff options
author | Jakub Hrozek <jhrozek@redhat.com> | 2017-01-12 13:00:48 +0100 |
---|---|---|
committer | Lukas Slebodnik <lslebodn@redhat.com> | 2017-03-14 13:31:47 +0100 |
commit | ca90f2102a43a3d49a2ef26610d7b4ff3062a823 (patch) | |
tree | 67773a18be1ce631ebec24e9ae2602a25290e2ec | |
parent | 321ca28277cbf9882769537fd4c0dfaea224c86e (diff) | |
download | sssd-ca90f2102a43a3d49a2ef26610d7b4ff3062a823.tar.gz sssd-ca90f2102a43a3d49a2ef26610d7b4ff3062a823.tar.xz sssd-ca90f2102a43a3d49a2ef26610d7b4ff3062a823.zip |
UTIL: Add a libtevent libcurl wrapper
Adds a request that enables the caller to issue an asynchronous request
with libcurl. Currently only requests towards UNIX sockets are
supported.
Reviewed-by: Pavel Březina <pbrezina@redhat.com>
Reviewed-by: Lukáš Slebodník <lslebodn@redhat.com>
-rw-r--r-- | Makefile.am | 1 | ||||
-rw-r--r-- | src/util/tev_curl.c | 933 | ||||
-rw-r--r-- | src/util/tev_curl.h | 111 |
3 files changed, 1045 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am index 318fd210e..869ac2228 100644 --- a/Makefile.am +++ b/Makefile.am @@ -660,6 +660,7 @@ dist_noinst_HEADERS = \ src/util/util_creds.h \ src/util/inotify.h \ src/util/sss_iobuf.h \ + src/util/tev_curl.h \ src/monitor/monitor.h \ src/monitor/monitor_interfaces.h \ src/monitor/monitor_iface_generated.h \ diff --git a/src/util/tev_curl.c b/src/util/tev_curl.c new file mode 100644 index 000000000..fd436653b --- /dev/null +++ b/src/util/tev_curl.c @@ -0,0 +1,933 @@ +/* + SSSD + + libcurl tevent integration + + Copyright (C) Red Hat, 2016 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <sys/time.h> +#include <unistd.h> +#include <errno.h> + +#include <talloc.h> +#include <tevent.h> + +#include <curl/curl.h> + +#include "util/util.h" +#include "util/tev_curl.h" + +#define IOBUF_CHUNK 1024 +#define IOBUF_MAX 4096 + +static bool global_is_curl_initialized; + +/** + * @brief The main structure of the tcurl module. + * + * Use tcurl_init() to initialize it, then pass to the request. + * Should be kept opaque in the future. + * + * @see tcurl_init() + */ +struct tcurl_ctx { + struct tevent_context *ev; + /* See where we set CURLMOPT_TIMERFUNCTION */ + struct tevent_timer *process_timer; + + /* Since we want the API to be non-blocking, all the transfers use + * the curl's multi interface: + * https://ec.haxx.se/libcurl-drive-multi.html + * and then each transfer also uses an easy interface instance for + * the transfer's private data + */ + CURLM *multi_handle; +}; + +/** + * @brief A tevent wrapper around curl socket + */ +struct tcurl_sock { + struct tcurl_ctx *tctx; /* Backchannel to the main context */ + + curl_socket_t sockfd; /* curl socket is an int typedef on Unix */ + 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 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 + */ + case CURLE_HTTP_RETURNED_ERROR: + case CURLE_OK: + return EOK; + case CURLE_URL_MALFORMAT: + return EBADMSG; + case CURLE_COULDNT_CONNECT: + return EHOSTUNREACH; + case CURLE_REMOTE_ACCESS_DENIED: + return EACCES; + case CURLE_OUT_OF_MEMORY: + return ENOMEM; + case CURLE_OPERATION_TIMEDOUT: + return ETIMEDOUT; + default: + break; + } + + return EIO; +} + +static errno_t tcurl_global_init(void) +{ + errno_t ret; + + if (global_is_curl_initialized == false) { + ret = curl_global_init(CURL_GLOBAL_ALL); + if (ret != CURLE_OK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot initialize global curl options [%d]\n", ret); + return EIO; + } + } + + global_is_curl_initialized = true; + 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"; + } + + return "Uknown request type"; +} + +static int curl2tev_flags(int curlflags) +{ + int flags = 0; + + switch (curlflags) { + case CURL_POLL_IN: + flags |= TEVENT_FD_READ; + break; + case CURL_POLL_OUT: + flags |= TEVENT_FD_WRITE; + break; + case CURL_POLL_INOUT: + flags |= (TEVENT_FD_READ | TEVENT_FD_WRITE); + break; + } + + return flags; +} + +static void handle_curlmsg_done(CURLMsg *message) +{ + CURL *easy_handle; + CURLcode crv; + struct tevent_req *req; + char *done_url; + errno_t ret; + struct tcurl_http_state *state; + + easy_handle = message->easy_handle; + if (easy_handle == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "BUG: NULL handle for message %p\n", message); + return; + } + + 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)); + /* not fatal since we need this only for debugging */ + } else { + DEBUG(SSSDBG_TRACE_FUNC, "Handled %s\n", done_url); + } + } + + 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", + 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(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; + } + + /* 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 (crv != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Cannot get HTTP status code\n"); + tevent_req_error(req, EFAULT); + return; + } + + tevent_req_done(req); +} + +static void process_curl_activity(struct tcurl_ctx *tctx) +{ + CURLMsg *message; + int pending; + + while ((message = curl_multi_info_read(tctx->multi_handle, &pending))) { + switch (message->msg) { + case CURLMSG_DONE: + handle_curlmsg_done(message); + break; + default: + DEBUG(SSSDBG_TRACE_LIBS, + "noop for curl msg %d\n", message->msg); + break; + } + } +} + +static void tcurlsock_input_available(struct tevent_context *ev, + struct tevent_fd *fde, + uint16_t flags, + void *data) +{ + struct tcurl_ctx *tctx; + struct tcurl_sock *tcs = NULL; + int curl_flags = 0; + int running_handles; + + tcs = talloc_get_type(data, struct tcurl_sock); + if (tcs == NULL) { + return; + } + + if (flags & TEVENT_FD_READ) { + curl_flags |= CURL_CSELECT_IN; + } + if (flags & TEVENT_FD_WRITE) { + curl_flags |= CURL_CSELECT_OUT; + } + + /* multi_socket_action might invalidate tcs when the transfer ends, + * so we need to store tctx separately + */ + tctx = tcs->tctx; + + /* https://ec.haxx.se/libcurl-drive-multi-socket.html */ + curl_multi_socket_action(tcs->tctx->multi_handle, + tcs->sockfd, + curl_flags, + &running_handles); + + process_curl_activity(tctx); +} + +/** + * @brief Registers a curl's socket with tevent + * + * Creates a private structure, registers the socket with tevent and finally + * registers the tcurl_sock structure as a private pointer for the curl + * socket for later + */ +static struct tcurl_sock *register_curl_socket(struct tcurl_ctx *tctx, + curl_socket_t sockfd, + int flags) +{ + struct tcurl_sock *tcs; + + tcs = talloc_zero(tctx, struct tcurl_sock); + if (tcs == NULL) { + return NULL; + } + tcs->sockfd = sockfd; + tcs->tctx = tctx; + + tcs->fde = tevent_add_fd(tctx->ev, tcs, sockfd, flags, + tcurlsock_input_available, tcs); + if (tcs->fde == NULL) { + talloc_free(tcs); + return NULL; + } + + curl_multi_assign(tctx->multi_handle, sockfd, (void *) tcs); + return tcs; +} + +/* libcurl informs the application about socket activity to wait for with + * this callback */ +static int handle_socket(CURL *easy, + curl_socket_t s, + int action, + void *userp, + void *socketp) +{ + struct tcurl_ctx *tctx = NULL; + struct tcurl_sock *tcsock; + int flags = 0; + + tctx = talloc_get_type(userp, struct tcurl_ctx); + if (tctx == NULL) { + return 1; + } + + DEBUG(SSSDBG_TRACE_INTERNAL, + "Activity on curl socket %d socket data %p\n", s, socketp); + + switch (action) { + case CURL_POLL_IN: + case CURL_POLL_OUT: + case CURL_POLL_INOUT: + /* There is some activity on a socket */ + + flags = curl2tev_flags(action); + + if (socketp == NULL) { + /* If this socket doesn't have private data, it must be a new one, + * let's start tracking it with tevent + */ + tcsock = register_curl_socket(tctx, s, flags); + if (tcsock == NULL) { + return 1; + } + } else { + /* If we are already tracking this socket, just set the correct + * flags for tevent and pass the control to tevent + */ + tcsock = talloc_get_type(socketp, struct tcurl_sock); + if (tcsock == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "BUG: No private data for socket %d\n", s); + return 1; + } + tevent_fd_set_flags(tcsock->fde, flags); + } + break; + + case CURL_POLL_REMOVE: + /* This socket is being closed by curl, so we need to.. */ + tcsock = talloc_get_type(socketp, struct tcurl_sock); + if (tcsock == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "BUG: Trying to remove an untracked socket %d\n", s); + } + /* ..stop tracking the socket with the multi handle.. */ + curl_multi_assign(tctx->multi_handle, s, NULL); + /* ..and stop tracking the fd with tevent */ + talloc_free(tcsock); + break; + + default: + return 1; + } + + return 0; +} + +static void check_curl_timeouts(struct tcurl_ctx *tctx) +{ + int running_handles; + + curl_multi_socket_action(tctx->multi_handle, + CURL_SOCKET_TIMEOUT, + 0, + &running_handles); + DEBUG(SSSDBG_TRACE_ALL, + "Still tracking %d outstanding requests\n", running_handles); + + /* https://ec.haxx.se/libcurl-drive-multi-socket.html */ + process_curl_activity(tctx); +} + +static void check_fd_activity(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval current_time, + void *private_data) +{ + struct tcurl_ctx *tctx = talloc_get_type(private_data, struct tcurl_ctx); + check_curl_timeouts(tctx); +} + +static int schedule_fd_processing(CURLM *multi, + long timeout_ms, + void *userp) +{ + struct timeval tv = { 0, 0 }; + struct tcurl_ctx *tctx = talloc_get_type(userp, struct tcurl_ctx); + + DEBUG(SSSDBG_TRACE_INTERNAL, "timeout_ms: %ld\n", timeout_ms); + + if (timeout_ms == -1) { + /* man curlmopt_timerfunction(3) says: + * A timeout_ms value of -1 means you should delete your timer. + */ + talloc_zfree(tctx->process_timer); + check_curl_timeouts(tctx); + return 0; + } + + tv = tevent_timeval_current_ofs(0, timeout_ms * 1000); + + /* There is only one timer per multi handle, so it makes sense to cancel + * the previous one. + * + * From https://ec.haxx.se/libcurl-drive-multi-socket.html: + * There is only one timeout for the application to handle for the + * entire multi handle, no matter how many individual easy handles + * that have been added or transfers that are in progress. The timer + * callback will be updated with the current nearest-in-time period to + * wait. + */ + talloc_zfree(tctx->process_timer); + tctx->process_timer = tevent_add_timer(tctx->ev, tctx, tv, + check_fd_activity, tctx); + if (tctx->process_timer == NULL) { + return -1; + } + + return 0; +} + +static int tcurl_ctx_destroy(struct tcurl_ctx *ctx) +{ + if (ctx == NULL) { + return 0; + } + + curl_multi_cleanup(ctx->multi_handle); + return 0; +} + +struct tcurl_ctx *tcurl_init(TALLOC_CTX *mem_ctx, + struct tevent_context *ev) +{ + errno_t ret; + struct tcurl_ctx *tctx = NULL; + CURLMcode cmret; + + /* Per the manpage it is safe to call the initialization multiple + * times, as long as this is done before any other curl calls to + * make sure we don't mangle the global curl environment + */ + ret = tcurl_global_init(); + if (ret != EOK) { + goto fail; + } + + tctx = talloc_zero(mem_ctx, struct tcurl_ctx); + if (tctx == NULL) { + goto fail; + } + tctx->ev = ev; + + tctx->multi_handle = curl_multi_init(); + if (tctx->multi_handle == NULL) { + goto fail; + } + talloc_set_destructor(tctx, tcurl_ctx_destroy); + + cmret = curl_multi_setopt(tctx->multi_handle, + CURLMOPT_SOCKETDATA, tctx); + if (cmret != CURLM_OK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot set CURLMOPT_SOCKETDATA [%d]: %s\n", + cmret, curl_multi_strerror(cmret)); + goto fail; + } + + /* + * When there is some activity on a socket associated with the multi + * handle, then the handle_socket() function will be called with the + * global context as private data + */ + cmret = curl_multi_setopt(tctx->multi_handle, + CURLMOPT_SOCKETFUNCTION, handle_socket); + if (cmret != CURLM_OK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot set CURLMOPT_SOCKETFUNCTION [%d]: %s\n", + cmret, curl_multi_strerror(cmret)); + goto fail; + } + + /* When integrated in a mainloop, the curl multi interface must + * kick off the communication in another eventloop tick. Similar + * to the handle_socet function, the tcurl context is passed in + * as private data + */ + cmret = curl_multi_setopt(tctx->multi_handle, + CURLMOPT_TIMERFUNCTION, schedule_fd_processing); + if (cmret != CURLM_OK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot set CURLMOPT_TIMERFUNCTION [%d]: %s\n", + cmret, curl_multi_strerror(cmret)); + goto fail; + } + + cmret = curl_multi_setopt(tctx->multi_handle, CURLMOPT_TIMERDATA, tctx); + if (cmret != CURLM_OK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot set CURLMOPT_TIMERDATA [%d]: %s\n", + cmret, curl_multi_strerror(cmret)); + } + + return tctx; + +fail: + talloc_free(tctx); + 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_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) +{ + errno_t ret; + size_t realsize = size * nmemb; + struct sss_iobuf *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); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to write data to buffer [%d]: %s\n", ret, sss_strerror(ret)); + /* zero signifies an EOF */ + return 0; + } + + return realsize; +} + +static size_t tcurl_http_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; + + if (inbuf == NULL) { + return CURL_READFUNC_ABORT; + } + + ret = sss_iobuf_read(inbuf, size * nmemb, ptr, &readbytes); + if (ret != EOK) { + return CURL_READFUNC_ABORT; + } + + return readbytes; +} + +int tcurl_http_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + int *_http_code, + struct sss_iobuf **_outbuf) +{ + struct tcurl_http_state *state = tevent_req_data(req, struct tcurl_http_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + if (_http_code != NULL) { + *_http_code = state->http_code; + } + + if (_outbuf != NULL) { + *_outbuf = talloc_steal(mem_ctx, state->outbuf); + } + + return 0; +} diff --git a/src/util/tev_curl.h b/src/util/tev_curl.h new file mode 100644 index 000000000..de0601df4 --- /dev/null +++ b/src/util/tev_curl.h @@ -0,0 +1,111 @@ +/* + SSSD + + libcurl tevent integration + + Copyright (C) Red Hat, 2016 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef __TEV_CURL_H +#define __TEV_CURL_H + +#include <talloc.h> +#include <tevent.h> + +#include "util/sss_iobuf.h" + +/** + * @brief Supported HTTP requests + */ +enum tcurl_http_request { + TCURL_HTTP_GET, + TCURL_HTTP_PUT, + TCURL_HTTP_DELETE, +}; + +/** + * @brief Initialize the tcurl tevent wrapper. + * + * @returns the opaque context or NULL on error + */ +struct tcurl_ctx *tcurl_init(TALLOC_CTX *mem_ctx, + struct tevent_context *ev); + +/** + * @brief Run a single asynchronous HTTP request. + * + * Currently only UNIX sockets at socket_path are supported. + * + * 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: + * static const char *headers[] = { + * "Content-type: application/octet-stream", + * NULL, + * }; + * + * @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] socket_path The path to the UNIX socket to forward the + * request to + * @param[in] url The request URL + * @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 + * 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. + * + * @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_http_recv to read the error code. + * + * @see tcurl_init + * @see tcurl_http_recv + */ +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); + +/** + * @brief Receive a result of a single asynchronous HTTP request. + * + * @param[in] mem_ctx The talloc context that owns the outbuf + * @param[in] req The request previously obtained with + * tcurl_http_send + * @param[out] _http_code The HTTP code that the transfer ended with + * @param[out] _outbuf The raw data the HTTP request returned + * + * @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); + +#endif /* __TEV_CURL_H */ |