URL: https://github.com/SSSD/sssd/pull/152 Author: jhrozek Title: #152: Add a tevent wrapper around libcurl's asynchronous interface Action: synchronized
To pull the PR as Git branch: git remote add ghsssd https://github.com/SSSD/sssd git fetch ghsssd pull/152/head:pr152 git checkout pr152
From 5c51222ba533972d56f4ecb43e05b5c2e1db372e Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <jhro...@redhat.com> Date: Fri, 23 Sep 2016 13:41:53 +0200 Subject: [PATCH 1/5] UTIL: Add a new macro SAFEALIGN_MEMCPY_CHECK We will use it later in the KCM server --- src/util/util_safealign.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util/util_safealign.h b/src/util/util_safealign.h index a2cd4dd..0d9a579 100644 --- a/src/util/util_safealign.h +++ b/src/util/util_safealign.h @@ -124,6 +124,12 @@ safealign_memcpy(void *dest, const void *src, size_t n, size_t *counter) safealign_memcpy(dest, CV_MACRO_val, sizeof(char) * length, pctr); \ } while(0) +#define SAFEALIGN_MEMCPY_CHECK(dest, src, srclen, len, pctr) do { \ + if ((*(pctr) + srclen) > (len) || \ + SIZE_T_OVERFLOW(*(pctr), srclen)) { return EINVAL; } \ + safealign_memcpy(dest, src, srclen, pctr); \ +} while(0) + /* Aliases for backward compatibility. */ #define SAFEALIGN_SET_VALUE SAFEALIGN_SETMEM_VALUE #define SAFEALIGN_SET_INT64 SAFEALIGN_SETMEM_INT64 From 30b5f3c8252a819bdd3ee2094562a019e2252bd8 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <jhro...@redhat.com> Date: Tue, 20 Sep 2016 18:46:40 +0200 Subject: [PATCH 2/5] UTIL: Add a generic iobuf module The KCM responder reads bytes and writes bytes from a buffer of bytes. Instead of letting the caller deal with low-level handling using the SAFEALIGN macros, this patch adds a new iobuf.c module with more high-level functions. The core is a iobuf struct that keeps track of the buffer, its total capacity and a current read or write position. There are helper function to read or write a generic buffer with a set length. Later, we will also add convenience functions to read C data types using the SAFEALIGN macros. --- Makefile.am | 22 +++++ src/tests/cmocka/test_iobuf.c | 194 +++++++++++++++++++++++++++++++++++++++ src/util/sss_iobuf.c | 204 ++++++++++++++++++++++++++++++++++++++++++ src/util/sss_iobuf.h | 117 ++++++++++++++++++++++++ 4 files changed, 537 insertions(+) create mode 100644 src/tests/cmocka/test_iobuf.c create mode 100644 src/util/sss_iobuf.c create mode 100644 src/util/sss_iobuf.h diff --git a/Makefile.am b/Makefile.am index e676e18..b612897 100644 --- a/Makefile.am +++ b/Makefile.am @@ -274,6 +274,7 @@ if HAVE_CMOCKA test_ipa_dn \ simple-access-tests \ krb5_common_test \ + test_iobuf \ $(NULL) if HAVE_LIBRESOLV @@ -654,6 +655,7 @@ dist_noinst_HEADERS = \ src/util/util_sss_idmap.h \ src/util/util_creds.h \ src/util/inotify.h \ + src/util/sss_iobuf.h \ src/monitor/monitor.h \ src/monitor/monitor_interfaces.h \ src/monitor/monitor_iface_generated.h \ @@ -1820,6 +1822,7 @@ krb5_utils_tests_SOURCES = \ src/providers/krb5/krb5_common.c \ src/providers/krb5/krb5_opts.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ src/providers/data_provider_fo.c \ src/providers/data_provider_opts.c \ src/providers/data_provider_callbacks.c \ @@ -2095,6 +2098,7 @@ krb5_child_test_SOURCES = \ src/providers/krb5/krb5_common.c \ src/providers/krb5/krb5_opts.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ src/providers/data_provider_fo.c \ src/providers/data_provider_opts.c \ src/providers/data_provider_callbacks.c \ @@ -2765,6 +2769,7 @@ test_copy_ccache_SOURCES = \ src/tests/cmocka/test_copy_ccache.c \ src/providers/krb5/krb5_ccache.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ $(NULL) test_copy_ccache_CFLAGS = \ $(AM_CFLAGS) \ @@ -2783,6 +2788,7 @@ test_copy_keytab_SOURCES = \ src/tests/cmocka/test_copy_keytab.c \ src/providers/krb5/krb5_keytab.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ $(NULL) test_copy_keytab_CFLAGS = \ $(AM_CFLAGS) \ @@ -3145,6 +3151,19 @@ test_ipa_dn_LDADD = \ libsss_test_common.la \ $(NULL) +test_iobuf_SOURCES = \ + src/util/sss_iobuf.c \ + src/tests/cmocka/test_iobuf.c \ + $(NULL) +test_iobuf_CFLAGS = \ + $(AM_CFLAGS) \ + $(NULL) +test_iobuf_LDADD = \ + $(CMOCKA_LIBS) \ + $(SSSD_LIBS) \ + $(NULL) + + EXTRA_simple_access_tests_DEPENDENCIES = \ $(ldblib_LTLIBRARIES) simple_access_tests_SOURCES = \ @@ -3476,6 +3495,7 @@ libsss_krb5_common_la_SOURCES = \ src/providers/krb5/krb5_init_shared.c \ src/providers/krb5/krb5_ccache.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ src/util/become_user.c \ $(NULL) libsss_krb5_common_la_CFLAGS = \ @@ -3705,6 +3725,7 @@ krb5_child_SOURCES = \ src/providers/dp_pam_data_util.c \ src/util/user_info_msg.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ src/util/find_uid.c \ src/util/atomic_io.c \ src/util/authtok.c \ @@ -3738,6 +3759,7 @@ ldap_child_SOURCES = \ src/providers/ldap/ldap_child.c \ src/providers/krb5/krb5_keytab.c \ src/util/sss_krb5.c \ + src/util/sss_iobuf.c \ src/util/atomic_io.c \ src/util/authtok.c \ src/util/authtok-utils.c \ diff --git a/src/tests/cmocka/test_iobuf.c b/src/tests/cmocka/test_iobuf.c new file mode 100644 index 0000000..eaee966 --- /dev/null +++ b/src/tests/cmocka/test_iobuf.c @@ -0,0 +1,194 @@ +/* + SSSD + + test_iobuf - IO buffer tests + + Copyright (C) 2016 Red Hat + + 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 <fcntl.h> +#include <errno.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <stddef.h> +#include <setjmp.h> +#include <cmocka.h> + +#include "util/sss_iobuf.h" + +static void test_sss_iobuf_read(void **state) +{ + errno_t ret; + uint8_t buffer[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', 0 }; + uint8_t readbuf[64] = { 0 }; + size_t nread; + struct sss_iobuf *rb; + + rb = sss_iobuf_init_readonly(NULL, buffer, sizeof(buffer)); + assert_non_null(rb); + + ret = sss_iobuf_read(rb, 5, readbuf, &nread); + assert_int_equal(ret, EOK); + /* There is enough data in the buffer */ + assert_int_equal(nread, 5); + /* The data matches beginning of the buffer */ + assert_int_equal(strncmp((const char *) readbuf, "Hello", 5), 0); + + memset(readbuf, 0, sizeof(readbuf)); + ret = sss_iobuf_read(rb, 3, readbuf, &nread); + assert_int_equal(ret, EOK); + /* There is enough data in the buffer */ + assert_int_equal(nread, 3); + /* The data matches beginning of the buffer */ + assert_int_equal(strncmp((const char *) readbuf, " wo", 3), 0); + + /* Try to read more than the buffer has */ + memset(readbuf, 0, sizeof(readbuf)); + ret = sss_iobuf_read(rb, 10, readbuf, &nread); + /* This is not a fatal error */ + assert_int_equal(ret, EOK); + /* We just see how much there was */ + assert_int_equal(nread, 4); + /* And get the rest of the buffer back. readbuf includes trailing zero now */ + assert_int_equal(strcmp((const char *) readbuf, "rld"), 0); + + /* Reading a depleted buffer will just yield zero bytes read now */ + ret = sss_iobuf_read(rb, 10, readbuf, &nread); + assert_int_equal(ret, EOK); + assert_int_equal(nread, 0); + + /* Failure cases */ + ret = sss_iobuf_read(NULL, 10, readbuf, &nread); + assert_int_equal(ret, EINVAL); + ret = sss_iobuf_read(rb, 10, NULL, &nread); + assert_int_equal(ret, EINVAL); + + talloc_free(rb); +} + +static void test_sss_iobuf_write(void **state) +{ + struct sss_iobuf *wb; + struct sss_iobuf *rb; + size_t hwlen = sizeof("Hello world"); /* Includes trailing zero */ + uint8_t readbuf[64]; + size_t nread; + errno_t ret; + + /* Exactly fill the capacity */ + wb = sss_iobuf_init_empty(NULL, hwlen, hwlen); + assert_non_null(wb); + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("Hello world"), + sizeof("Hello world")); + assert_int_equal(ret, EOK); + + rb = sss_iobuf_init_readonly(NULL, + sss_iobuf_get_data(wb), + sss_iobuf_get_len(wb)); + talloc_free(wb); + assert_non_null(rb); + + ret = sss_iobuf_read(rb, sizeof(readbuf), readbuf, &nread); + assert_int_equal(ret, EOK); + assert_int_equal(nread, hwlen); + assert_int_equal(strcmp((const char *) readbuf, "Hello world"), 0); + talloc_zfree(rb); + + /* Overflow the capacity by one */ + wb = sss_iobuf_init_empty(NULL, hwlen, hwlen); + assert_non_null(wb); + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("Hello world!"), + sizeof("Hello world!")); + assert_int_not_equal(ret, EOK); + talloc_zfree(wb); + + /* Test resizing exactly up to capacity in several writes */ + wb = sss_iobuf_init_empty(NULL, 2, hwlen); + assert_non_null(wb); + + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("Hello "), + sizeof("Hello ")-1); /* Not the null byte now.. */ + assert_int_equal(ret, EOK); + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("world"), + sizeof("world")); + assert_int_equal(ret, EOK); + + rb = sss_iobuf_init_readonly(NULL, + sss_iobuf_get_data(wb), + sss_iobuf_get_len(wb)); + talloc_free(wb); + assert_non_null(rb); + + ret = sss_iobuf_read(rb, sizeof(readbuf), readbuf, &nread); + assert_int_equal(ret, EOK); + assert_int_equal(nread, hwlen); + assert_int_equal(strcmp((const char *) readbuf, "Hello world"), 0); + talloc_zfree(rb); + + /* Overflow the capacity during a resize by one */ + wb = sss_iobuf_init_empty(NULL, 2, hwlen); + assert_non_null(wb); + + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("Hello "), + sizeof("Hello ")-1); /* Not the null byte now.. */ + assert_int_equal(ret, EOK); + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("world!"), + sizeof("world!")); + assert_int_not_equal(ret, EOK); + talloc_zfree(wb); + + /* Test allocating an unlimited buffer */ + wb = sss_iobuf_init_empty(NULL, 2, 0); + assert_non_null(wb); + + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("Hello "), + sizeof("Hello ")-1); /* Not the null byte now.. */ + assert_int_equal(ret, EOK); + ret = sss_iobuf_write_len(wb, + (uint8_t *) discard_const("world"), + sizeof("world")); + assert_int_equal(ret, EOK); + + rb = sss_iobuf_init_readonly(NULL, + sss_iobuf_get_data(wb), + sss_iobuf_get_len(wb)); + talloc_free(wb); + assert_non_null(rb); + + ret = sss_iobuf_read(rb, sizeof(readbuf), readbuf, &nread); + assert_int_equal(ret, EOK); + assert_int_equal(nread, hwlen); + assert_int_equal(strcmp((const char *) readbuf, "Hello world"), 0); + talloc_zfree(rb); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_sss_iobuf_read), + cmocka_unit_test(test_sss_iobuf_write), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/src/util/sss_iobuf.c b/src/util/sss_iobuf.c new file mode 100644 index 0000000..a6223b2 --- /dev/null +++ b/src/util/sss_iobuf.c @@ -0,0 +1,204 @@ +#include <talloc.h> + +#include "util/util.h" +#include "util/sss_iobuf.h" + +/** + * @brief The iobuf structure that holds the data, its capacity and + * a pointer to the data. + * + * @see sss_iobuf_init_empty() + * @see sss_iobuf_init_readonly() + */ +struct sss_iobuf { + uint8_t *data; /* Start of the data buffer */ + + size_t dp; /* Data pointer */ + size_t size; /* Current data buffer size */ + size_t capacity; /* Maximum capacity */ +}; + +struct sss_iobuf *sss_iobuf_init_empty(TALLOC_CTX *mem_ctx, + size_t size, + size_t capacity) +{ + struct sss_iobuf *iobuf; + uint8_t *buf; + + iobuf = talloc_zero(mem_ctx, struct sss_iobuf); + if (iobuf == NULL) { + return NULL; + } + + buf = talloc_zero_array(iobuf, uint8_t, size); + if (buf == NULL) { + talloc_free(iobuf); + return NULL; + } + + if (capacity == 0) { + capacity = SIZE_MAX/2; + } + + iobuf->data = buf; + iobuf->size = size; + iobuf->capacity = capacity; + iobuf->dp = 0; + + return iobuf; +} + +struct sss_iobuf *sss_iobuf_init_readonly(TALLOC_CTX *mem_ctx, + uint8_t *data, + size_t size) +{ + struct sss_iobuf *iobuf; + + iobuf = sss_iobuf_init_empty(mem_ctx, size, size); + if (iobuf == NULL) { + return NULL; + } + + if (data != NULL) { + memcpy(iobuf->data, data, size); + } + + return iobuf; +} + +size_t sss_iobuf_get_len(struct sss_iobuf *iobuf) +{ + if (iobuf == NULL) { + return 0; + } + + return iobuf->dp; +} + +size_t sss_iobuf_get_capacity(struct sss_iobuf *iobuf) +{ + if (iobuf == NULL) { + return 0; + } + + return iobuf->capacity; +} + +size_t sss_iobuf_get_size(struct sss_iobuf *iobuf) +{ + if (iobuf == NULL) { + return 0; + } + + return iobuf->size; +} + +uint8_t *sss_iobuf_get_data(struct sss_iobuf *iobuf) +{ + if (iobuf == NULL) { + return NULL; + } + + return iobuf->data; +} + +static size_t iobuf_get_len(struct sss_iobuf *iobuf) +{ + if (iobuf == NULL) { + return 0; + } + + return (iobuf->size - iobuf->dp); +} + +static errno_t ensure_bytes(struct sss_iobuf *iobuf, + size_t nbytes) +{ + size_t wantsize; + size_t newsize; + uint8_t *newdata; + + if (iobuf == NULL) { + return EINVAL; + } + + wantsize = iobuf->dp + nbytes; + if (wantsize <= iobuf->size) { + /* Enough space already */ + return EOK; + } + + /* Else, try to extend the iobuf */ + if (wantsize > iobuf->capacity) { + /* We will never grow past capacity */ + return ENOBUFS; + } + + /* Double the size until we add at least nbytes, but stop if we double past capacity */ + for (newsize = iobuf->size; + (newsize < wantsize) && (newsize < iobuf->capacity); + newsize *= 2); + + if (newsize > iobuf->capacity) { + newsize = iobuf->capacity; + } + + newdata = talloc_realloc(iobuf, iobuf->data, uint8_t, newsize); + if (newdata == NULL) { + return ENOMEM; + } + + iobuf->data = newdata; + iobuf->size = newsize; + + return EOK; +} + +static inline uint8_t *iobuf_ptr(struct sss_iobuf *iobuf) +{ + return iobuf->data + iobuf->dp; +} + +errno_t sss_iobuf_read(struct sss_iobuf *iobuf, + size_t len, + uint8_t *_buf, + size_t *_read) +{ + size_t remaining; + + if (iobuf == NULL || _buf == NULL) { + return EINVAL; + } + + remaining = iobuf_get_len(iobuf); + if (len > remaining) { + len = remaining; + } + + safealign_memcpy(_buf, iobuf_ptr(iobuf), len, &iobuf->dp); + if (_read != NULL) { + *_read = len; + } + + return EOK; +} + +errno_t sss_iobuf_write_len(struct sss_iobuf *iobuf, + uint8_t *buf, + size_t len) +{ + errno_t ret; + + if (iobuf == NULL || buf == NULL) { + return EINVAL; + } + + ret = ensure_bytes(iobuf, len); + if (ret != EOK) { + return ret; + } + + safealign_memcpy(iobuf_ptr(iobuf), buf, len, &iobuf->dp); + + return EOK; +} diff --git a/src/util/sss_iobuf.h b/src/util/sss_iobuf.h new file mode 100644 index 0000000..e546d59 --- /dev/null +++ b/src/util/sss_iobuf.h @@ -0,0 +1,117 @@ +#ifndef __SSS_IOBUF_H_ +#define __SSS_IOBUF_H_ + +#include <talloc.h> +#include <stdint.h> +#include <errno.h> + +#include "util/util.h" +#include "util/sss_iobuf.h" + +/* + * @brief Allocate an empty IO buffer + * + * @param[in] mem_ctx The talloc context that owns the iobuf + * + * When this buffer is written into, but the capacity is exceeded, the write + * function will return an error. + * + * @param[in] mem_ctx The talloc context that owns the iobuf + * @param[in] size The size of the data buffer + * @param[in] capacity The maximum capacity the buffer can grow into. + * Use 0 for an 'unlimited' buffer that will grow + * until SIZE_MAX/2. + * + * @return The newly created buffer on success or NULL on an error. + * + */ +struct sss_iobuf *sss_iobuf_init_empty(TALLOC_CTX *mem_ctx, + size_t size, + size_t capacity); + +/* + * @brief Allocate an IO buffer with a fixed size + * + * This function is useful for parsing an input buffer from an existing + * buffer pointed to by data. + * + * The iobuf does not assume ownership of the data buffer in talloc terms, + * but copies the data instead. + * + * @param[in] mem_ctx The talloc context that owns the iobuf + * @param[in] data The data to initialize the IO buffer with. This + * data is copied into the iobuf-owned buffer. + * @param[in] size The size of the data buffer + * + * @return The newly created buffer on success or NULL on an error. + */ +struct sss_iobuf *sss_iobuf_init_readonly(TALLOC_CTX *mem_ctx, + uint8_t *data, + size_t size); + +/* + * @brief Returns the number of bytes currently stored in the iobuf + * + * @return The number of bytes (the data pointer offset) + */ +size_t sss_iobuf_get_len(struct sss_iobuf *iobuf); + +/* + * @brief Returns the capacity of the IO buffer + * + * @return The capacity of the IO buffer. Returns zero + * for an unlimited buffer. + */ +size_t sss_iobuf_get_capacity(struct sss_iobuf *iobuf); + +/* + * @brief Returns the current size of the IO buffer + */ +size_t sss_iobuf_get_size(struct sss_iobuf *iobuf); + +/* + * @brief Returns the data pointer of the IO buffer + */ +uint8_t *sss_iobuf_get_data(struct sss_iobuf *iobuf); + +/* + * @brief Read from an IO buffer + * + * Read up to len bytes from an IO buffer. It is not an error to request + * more bytes than the buffer actually has - the function will succeed, but + * return the actual number of bytes read. Reading from an empty buffer just + * returns zero bytes read. + * + * @param[in] iobuf The IO buffer to read from + * @param[in] len The maximum number of bytes to read + * @param[out] _buf The buffer to read data into from iobuf + * @param[out] _read The actual number of bytes read from IO buffer. + * + * @return EOK on success, errno otherwise + */ +errno_t sss_iobuf_read(struct sss_iobuf *iobuf, + size_t len, + uint8_t *_buf, + size_t *_read); + +/* + * @brief Write into an IO buffer + * + * Attempts to write len bytes into the iobuf. If the capacity is exceeded, + * the iobuf module tries to extend the buffer up to the maximum capacity. + * + * If reallocating the internal buffer fails, the data pointers are not + * touched. + * + * @param[in] iobuf The IO buffer to write to + * @param[in] buf The data to write into the buffer + * @param[in] len The number of bytes to write + * + * @return EOK on success, errno otherwise. Notably returns ENOBUFS if + * the buffer capacity is exceeded. + */ +errno_t sss_iobuf_write_len(struct sss_iobuf *iobuf, + uint8_t *buf, + size_t len); + +#endif /* __SSS_IOBUF_H_ */ From 1edf42ca346f8108c30fe581d22db3dbb0e26465 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <jhro...@redhat.com> Date: Thu, 12 Jan 2017 13:00:21 +0100 Subject: [PATCH 3/5] BUILD: Detect libcurl during configure Currently libcurl is optional and if not present, just silently skipped. --- configure.ac | 1 + src/external/libcurl.m4 | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/external/libcurl.m4 diff --git a/configure.ac b/configure.ac index d264abf..e6a3b4e 100644 --- a/configure.ac +++ b/configure.ac @@ -193,6 +193,7 @@ m4_include([src/external/libresolv.m4]) m4_include([src/external/intgcheck.m4]) m4_include([src/external/systemtap.m4]) m4_include([src/external/service.m4]) +m4_include([src/external/libcurl.m4]) if test x$with_secrets = xyes; then m4_include([src/external/libhttp_parser.m4]) diff --git a/src/external/libcurl.m4 b/src/external/libcurl.m4 new file mode 100644 index 0000000..3bc303c --- /dev/null +++ b/src/external/libcurl.m4 @@ -0,0 +1,38 @@ +AC_ARG_ENABLE([curl], + [AS_HELP_STRING([--disable-curl-support], + [do not build with libcurl support])], + [enable_libcurl=$enableval], + [enable_libcurl=yes]) + +found_libcurl="no" +AS_IF([test x$enable_libcurl = xyes], + [PKG_CHECK_MODULES([CURL], + [libcurl], + [found_libcurl=yes], + [AC_MSG_WARN([ +The libcurl development library was not found. Some features will be disabled.]) + ])]) + +AS_IF([test x"$found_libcurl" = xyes], + CFLAGS="$CFLAGS $CURL_CFLAGS" + + AC_MSG_CHECKING([For CURLOPT_UNIX_SOCKET_PATH support in libcurl]) + AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [[#include <curl/curl.h> + CURLoption opt = CURLOPT_UNIX_SOCKET_PATH; + ]])], + [have_curlopt_unix_sockpath=yes] + [AC_MSG_RESULT([yes])], + [have_curlopt_unix_sockpath=no] + [AC_MSG_RESULT([no, libcurl support will be disabled])],) + + CFLAGS=$SAVE_CFLAGS +) + +AC_SUBST(CURL_LIBS) +AC_SUBST(CURL_CFLAGS) + +AM_CONDITIONAL([BUILD_WITH_LIBCURL], + [test x"$have_curlopt_unix_sockpath" = xyes]) +AM_COND_IF([BUILD_WITH_LIBCURL], + [AC_DEFINE_UNQUOTED(HAVE_LIBCURL, 1, [Build with libcurl support])]) From 7ba5383a6c2a31cdaf55359b91cc5d9a6485d3a3 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <jhro...@redhat.com> Date: Thu, 12 Jan 2017 13:00:48 +0100 Subject: [PATCH 4/5] 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. --- Makefile.am | 1 + src/util/tev_curl.c | 893 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/util/tev_curl.h | 111 +++++++ 3 files changed, 1005 insertions(+) create mode 100644 src/util/tev_curl.c create mode 100644 src/util/tev_curl.h diff --git a/Makefile.am b/Makefile.am index b612897..41b08e4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -656,6 +656,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 0000000..149d5be --- /dev/null +++ b/src/util/tev_curl.c @@ -0,0 +1,893 @@ +/* + 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 *initial_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; + int 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, &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_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); + 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 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); + + if (timeout_ms == -1) { + /* man curlmopt_timerfunction(3) says: + * A timeout_ms value of -1 means you should delete your timer. + */ + talloc_zfree(tctx->initial_timer); + } + + tv = tevent_timeval_current_ofs(0, timeout_ms * 10); + + tctx->initial_timer = tevent_add_timer(tctx->ev, tctx, tv, + check_fd_activity, tctx); + if (tctx->initial_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; + } + } + + 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; + } + + 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 0000000..de0601d --- /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 */ From 29295c74dfc2294ed9c46419f0815b0795b5b30a Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <jhro...@redhat.com> Date: Fri, 13 Jan 2017 09:31:03 +0100 Subject: [PATCH 5/5] TESTS: test the curl wrapper with a command-line tool In order to test the curl integration code, this patch adds a command-line tool and tests that it's possible to drive a conversation with the secrets responder using the tool. --- Makefile.am | 21 ++++ contrib/ci/deps.sh | 2 + src/tests/intg/Makefile.am | 1 + src/tests/intg/config.py.m4 | 1 + src/tests/intg/test_secrets.py | 143 +++++++++++++++++++++++++- src/tests/tcurl_test_tool.c | 222 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 src/tests/tcurl_test_tool.c diff --git a/Makefile.am b/Makefile.am index 41b08e4..d6896f1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2119,6 +2119,23 @@ krb5_child_test_LDADD = \ $(SSSD_INTERNAL_LTLIBS) \ libsss_test_common.la +if BUILD_WITH_LIBCURL +tcurl_test_tool_SOURCES = \ + src/tests/tcurl_test_tool.c \ + src/util/tev_curl.c \ + src/util/sss_iobuf.c \ + $(NULL) +tcurl_test_tool_CFLAGS = \ + $(AM_CFLAGS) \ + $(CURL_CFLAGS) \ + $(NULL) +tcurl_test_tool_LDADD = \ + $(CURL_LIBS) \ + $(SSSD_LIBS) \ + $(SSSD_INTERNAL_LTLIBS) \ + $(NULL) +endif + if BUILD_DBUS_TESTS sbus_tests_SOURCES = \ @@ -3227,6 +3244,9 @@ endif if BUILD_AUTOFS noinst_PROGRAMS += autofs_test_client endif +if BUILD_WITH_LIBCURL +noinst_PROGRAMS += tcurl-test-tool +endif pam_test_client_SOURCES = src/sss_client/pam_test_client.c pam_test_client_LDADD = $(PAM_LIBS) $(PAM_MISC_LIBS) @@ -3261,6 +3281,7 @@ intgcheck-prepare: --without-semanage \ $(INTGCHECK_CONFIGURE_FLAGS); \ $(MAKE) $(AM_MAKEFLAGS); \ + $(MAKE) $(AM_MAKEFLAGS) tcurl-test-tool || echo "libcurl tests will be disabled"; \ : Force single-thread install to workaround concurrency issues; \ $(MAKE) $(AM_MAKEFLAGS) -j1 install; \ : Remove .la files from LDB module directory to avoid loader warnings; \ diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh index 387ad1f..c525e62 100644 --- a/contrib/ci/deps.sh +++ b/contrib/ci/deps.sh @@ -46,6 +46,7 @@ if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then rpm-build uid_wrapper python-requests + curl-devel ) _DEPS_LIST_SPEC=` sed -e 's/@PACKAGE_VERSION@/0/g' \ @@ -120,6 +121,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then systemtap-sdt-dev libhttp-parser-dev libjansson-dev + libcurl4-openssl-dev ) DEPS_INTGCHECK_SATISFIED=true fi diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am index 64f90b4..1d36fa0 100644 --- a/src/tests/intg/Makefile.am +++ b/src/tests/intg/Makefile.am @@ -38,6 +38,7 @@ config.py: config.py.m4 -D "secdbpath=\`$(secdbpath)'" \ -D "libexecpath=\`$(libexecdir)'" \ -D "runstatedir=\`$(runstatedir)'" \ + -D "abs_builddir=\`$(abs_builddir)'" \ $< > $@ root: diff --git a/src/tests/intg/config.py.m4 b/src/tests/intg/config.py.m4 index 65e17e5..841aae0 100644 --- a/src/tests/intg/config.py.m4 +++ b/src/tests/intg/config.py.m4 @@ -15,3 +15,4 @@ MCACHE_PATH = "mcpath" SECDB_PATH = "secdbpath" LIBEXEC_PATH = "libexecpath" RUNSTATEDIR = "runstatedir" +ABS_BUILDDIR = "abs_builddir" diff --git a/src/tests/intg/test_secrets.py b/src/tests/intg/test_secrets.py index 062dcb6..a8d2332 100644 --- a/src/tests/intg/test_secrets.py +++ b/src/tests/intg/test_secrets.py @@ -69,6 +69,7 @@ def sec_teardown(): for secdb_file in os.listdir(config.SECDB_PATH): os.unlink(config.SECDB_PATH + "/" + secdb_file) request.addfinalizer(sec_teardown) + return secpid @pytest.fixture @@ -95,13 +96,27 @@ def setup_for_secrets(request): return None +def get_secrets_socket(): + return os.path.join(config.RUNSTATEDIR, "secrets.socket") + + @pytest.fixture def secrets_cli(request): - sock_path = os.path.join(config.RUNSTATEDIR, "secrets.socket") + sock_path = get_secrets_socket() cli = SecretsLocalClient(sock_path=sock_path) return cli +@pytest.fixture +def curlwrap_tool(request): + curlwrap_path = os.path.join(config.ABS_BUILDDIR, + "..", "..", "..", "tcurl-test-tool") + if os.access(curlwrap_path, os.X_OK): + return curlwrap_path + + return None + + def test_crd_ops(setup_for_secrets, secrets_cli): """ Test that the basic Create, Retrieve, Delete operations work @@ -171,6 +186,132 @@ def test_crd_ops(setup_for_secrets, secrets_cli): assert str(err413.value).startswith("413") +def run_curlwrap_tool(args, exp_http_code): + cmd = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, _ = cmd.communicate() + + assert cmd.returncode == 0 + + exp_http_code_str = "Request HTTP code: %d" % exp_http_code + assert exp_http_code_str in out + + return out + + +def test_curlwrap_crd_ops(setup_for_secrets, + curlwrap_tool): + """ + Test that the basic Create, Retrieve, Delete operations work using our + tevent libcurl code + """ + if not curlwrap_tool: + pytest.skip("The tcurl tool is not available, skipping test") + sock_path = get_secrets_socket() + + # listing an empty DB yields a 404 + run_curlwrap_tool([curlwrap_tool, + '-v', '-s', sock_path, + 'http://localhost/secrets/'], + 404) + + # listing a non-existent secret yields a 404 + run_curlwrap_tool([curlwrap_tool, + '-v', '-s', sock_path, + 'http://localhost/secrets/foo'], + 404) + + # set a secret foo:bar + run_curlwrap_tool([curlwrap_tool, '-p', + '-v', '-s', sock_path, + 'http://localhost/secrets/foo', + 'bar'], + 200) + + # list secrets + output = run_curlwrap_tool([curlwrap_tool, + '-v', '-s', sock_path, + 'http://localhost/secrets/'], + 200) + assert "foo" in output + + # get the foo secret + output = run_curlwrap_tool([curlwrap_tool, + '-v', '-s', sock_path, + 'http://localhost/secrets/foo'], + 200) + assert "bar" in output + + # Overwriting a secret is an error + run_curlwrap_tool([curlwrap_tool, '-p', + '-v', '-s', sock_path, + 'http://localhost/secrets/foo', + 'baz'], + 409) + + # Delete a secret + run_curlwrap_tool([curlwrap_tool, '-d', + '-v', '-s', sock_path, + 'http://localhost/secrets/foo'], + 200) + + # Delete a non-existent secret must yield a 404 + run_curlwrap_tool([curlwrap_tool, '-d', + '-v', '-s', sock_path, + 'http://localhost/secrets/foo'], + 404) + + +def test_curlwrap_parallel(setup_for_secrets, + curlwrap_tool): + """ + The tevent libcurl wrapper is meant to be non-blocking. Test + its operation in parallel. + """ + if not curlwrap_tool: + pytest.skip("The tcurl tool is not available, skipping test") + sock_path = get_secrets_socket() + + secrets = dict() + nsecrets = 10 + action = '-g' + + for i in xrange(0, nsecrets): + secrets["key" + str(i)] = "value" + str(i) + + args = [curlwrap_tool, '-p', '-v', '-s', sock_path] + for skey, svalue in secrets.iteritems(): + args.extend(['http://localhost/secrets/%s' % skey, svalue]) + run_curlwrap_tool(args, 200) + + output = run_curlwrap_tool([curlwrap_tool, + '-v', '-s', sock_path, + 'http://localhost/secrets/'], + 200) + for skey in secrets.iterkeys(): + assert skey in output + + args = [curlwrap_tool, '-g', '-v', '-s', sock_path] + for skey in secrets.iterkeys(): + args.extend(['http://localhost/secrets/%s' % skey]) + output = run_curlwrap_tool(args, 200) + + for svalue in secrets.itervalues(): + assert svalue in output + + args = [curlwrap_tool, '-d', '-v', '-s', sock_path] + for skey in secrets.iterkeys(): + args.extend(['http://localhost/secrets/%s' % skey]) + output = run_curlwrap_tool(args, 200) + + run_curlwrap_tool([curlwrap_tool, + '-v', '-s', sock_path, + 'http://localhost/secrets/'], + 404) + + def test_containers(setup_for_secrets, secrets_cli): """ Test that storing secrets inside containers works diff --git a/src/tests/tcurl_test_tool.c b/src/tests/tcurl_test_tool.c new file mode 100644 index 0000000..e1e2e1c --- /dev/null +++ b/src/tests/tcurl_test_tool.c @@ -0,0 +1,222 @@ +/* + SSSD + + libcurl tevent integration test tool + + 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 <popt.h> + +#include "util/util.h" +#include "util/tev_curl.h" + +#define MAXREQ 64 + +struct tool_ctx { + bool verbose; + + errno_t error; + bool done; + + size_t nreqs; +}; + +static void request_done(struct tevent_req *req) +{ + int http_code; + struct sss_iobuf *outbuf; + 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); + talloc_zfree(req); + + if (tool_ctx->error != EOK) { + DEBUG(SSSDBG_FATAL_FAILURE, "HTTP request failed: %d\n", tool_ctx->error); + return; + } else if (tool_ctx->verbose) { + printf("Request HTTP code: %d\n", http_code); + printf("Request HTTP body: \n%s\n", + (const char *) sss_iobuf_get_data(outbuf)); + talloc_zfree(outbuf); + } + + tool_ctx->nreqs--; + if (tool_ctx->nreqs == 0) { + tool_ctx->done = true; + } +} + +int main(int argc, const char *argv[]) +{ + int opt; + poptContext pc; + + int pc_debug = 0; + int pc_verbose = 0; + const char *socket_path = NULL; + const char *extra_arg_ptr; + + static const char *headers[] = { + "Content-type: application/octet-stream", + NULL, + }; + + struct poptOption long_options[] = { + POPT_AUTOHELP + { "debug", '\0', POPT_ARG_INT, &pc_debug, 0, + "The debug level to run with", NULL }, + { "socket-path", 's', POPT_ARG_STRING, &socket_path, 0, + "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 }, + { "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; + struct tcurl_ctx *ctx; + struct tool_ctx *tool_ctx; + + const char *urls[MAXREQ] = { 0 }; + struct sss_iobuf **inbufs; + + size_t n_reqs = 0; + + debug_prg_name = argv[0]; + pc = poptGetContext(NULL, argc, argv, long_options, 0); + poptSetOtherOptionHelp(pc, "HTTPDATA"); + + while ((opt = poptGetNextOpt(pc)) > 0) { + switch(opt) { + case 'g': + req_type = TCURL_HTTP_GET; + break; + case 'p': + req_type = TCURL_HTTP_PUT; + break; + case 'd': + req_type = TCURL_HTTP_DELETE; + break; + case 'v': + pc_verbose = 1; + break; + default: + DEBUG(SSSDBG_FATAL_FAILURE, "Unexpected option\n"); + return 1; + } + } + + DEBUG_CLI_INIT(pc_debug); + + tool_ctx = talloc_zero(NULL, struct tool_ctx); + if (tool_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Could not init tool context\n"); + return 1; + } + + inbufs = talloc_zero_array(tool_ctx, struct sss_iobuf *, MAXREQ); + if (inbufs == NULL) { + talloc_zfree(tool_ctx); + return 1; + } + + while ((extra_arg_ptr = poptGetArg(pc)) != NULL) { + switch(req_type) { + case TCURL_HTTP_GET: + case TCURL_HTTP_DELETE: + urls[n_reqs++] = extra_arg_ptr; + break; + case TCURL_HTTP_PUT: + if (urls[n_reqs] == NULL) { + urls[n_reqs] = extra_arg_ptr; + } else { + inbufs[n_reqs] = sss_iobuf_init_readonly( + inbufs, + (uint8_t *) discard_const(extra_arg_ptr), + strlen(extra_arg_ptr)); + if (inbufs[n_reqs] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Could not init input buffer\n"); + talloc_zfree(tool_ctx); + return 1; + } + n_reqs++; + } + break; + } + } + + if (opt != -1) { + poptPrintUsage(pc, stderr, 0); + fprintf(stderr, "%s", poptStrerror(opt)); + talloc_zfree(tool_ctx); + return 1; + } + + if (!socket_path) { + DEBUG(SSSDBG_FATAL_FAILURE, "Please specify the socket path\n"); + poptPrintUsage(pc, stderr, 0); + talloc_zfree(tool_ctx); + return 1; + } + + tool_ctx->nreqs = n_reqs; + tool_ctx->verbose = !!pc_verbose; + + ev = tevent_context_init(tool_ctx); + if (ev == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Could not init tevent context\n"); + talloc_zfree(tool_ctx); + return 1; + } + + ctx = tcurl_init(tool_ctx, ev); + if (ctx == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Could not init tcurl context\n"); + talloc_zfree(tool_ctx); + return 1; + } + + 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 (ctx == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Could not create request\n"); + talloc_zfree(tool_ctx); + return 1; + } + tevent_req_set_callback(req, request_done, tool_ctx); + } + + while (tool_ctx->done == false) { + tevent_loop_once(ev); + } + + talloc_free(tool_ctx); + poptFreeContext(pc); + return 0; +}
_______________________________________________ sssd-devel mailing list -- sssd-devel@lists.fedorahosted.org To unsubscribe send an email to sssd-devel-le...@lists.fedorahosted.org