summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_rest/rest.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/rlm_rest/rest.c')
-rw-r--r--src/modules/rlm_rest/rest.c2689
1 files changed, 2689 insertions, 0 deletions
diff --git a/src/modules/rlm_rest/rest.c b/src/modules/rlm_rest/rest.c
new file mode 100644
index 0000000..035f557
--- /dev/null
+++ b/src/modules/rlm_rest/rest.c
@@ -0,0 +1,2689 @@
+/*
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @brief Functions and datatypes for the REST (HTTP) transport.
+ * @file rest.c
+ *
+ * @copyright 2012-2014 Arran Cudbard-Bell <a.cudbard-bell@freeradius.org>
+ */
+
+RCSID("$Id$")
+
+#include <ctype.h>
+#include <string.h>
+#include <time.h>
+
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/libradius.h>
+#include <freeradius-devel/connection.h>
+
+#include "rest.h"
+
+/*
+ * This is a workaround to backward versions.
+ */
+#if defined(HAVE_JSON) && !defined(JSON_C_MINOR_VERSION) /* The versions less then 10, don't declare the 'JSON_C_MINOR_VERSION'*/
+int json_object_object_get_ex(struct json_object* jso, const char *key, struct json_object **value);
+int json_object_object_get_ex(struct json_object* jso, const char *key, struct json_object **value) {
+ *value = json_object_object_get(jso, key);
+
+ return (*value != NULL);
+}
+#endif
+
+/** Table of encoder/decoder support.
+ *
+ * Indexes in this table match the http_body_type_t enum, and should be
+ * updated if additional enum values are added.
+ *
+ * @see http_body_type_t
+ */
+const http_body_type_t http_body_type_supported[HTTP_BODY_NUM_ENTRIES] = {
+ HTTP_BODY_UNKNOWN, // HTTP_BODY_UNKNOWN
+ HTTP_BODY_UNSUPPORTED, // HTTP_BODY_UNSUPPORTED
+ HTTP_BODY_UNSUPPORTED, // HTTP_BODY_UNAVAILABLE
+ HTTP_BODY_UNSUPPORTED, // HTTP_BODY_INVALID
+ HTTP_BODY_NONE, // HTTP_BODY_NONE
+ HTTP_BODY_CUSTOM_XLAT, // HTTP_BODY_CUSTOM_XLAT
+ HTTP_BODY_CUSTOM_LITERAL, // HTTP_BODY_CUSTOM_LITERAL
+ HTTP_BODY_POST, // HTTP_BODY_POST
+#ifdef HAVE_JSON
+ HTTP_BODY_JSON, // HTTP_BODY_JSON
+#else
+ HTTP_BODY_UNAVAILABLE,
+#endif
+ HTTP_BODY_UNSUPPORTED, // HTTP_BODY_XML
+ HTTP_BODY_UNSUPPORTED, // HTTP_BODY_YAML
+ HTTP_BODY_INVALID, // HTTP_BODY_HTML
+ HTTP_BODY_PLAIN // HTTP_BODY_PLAIN
+};
+
+/*
+ * Lib CURL doesn't define symbols for unsupported auth methods
+ */
+#ifndef CURLOPT_TLSAUTH_SRP
+# define CURLOPT_TLSAUTH_SRP 0
+#endif
+#ifndef CURLAUTH_BASIC
+# define CURLAUTH_BASIC 0
+#endif
+#ifndef CURLAUTH_DIGEST
+# define CURLAUTH_DIGEST 0
+#endif
+#ifndef CURLAUTH_DIGEST_IE
+# define CURLAUTH_DIGEST_IE 0
+#endif
+#ifndef CURLAUTH_GSSNEGOTIATE
+# define CURLAUTH_GSSNEGOTIATE 0
+#endif
+#ifndef CURLAUTH_NTLM
+# define CURLAUTH_NTLM 0
+#endif
+#ifndef CURLAUTH_NTLM_WB
+# define CURLAUTH_NTLM_WB 0
+#endif
+
+/*
+ * CURL headers do:
+ *
+ * #define curl_easy_setopt(handle,opt,param) curl_easy_setopt(handle,opt,param)
+ */
+DIAG_OPTIONAL
+DIAG_OFF(disabled-macro-expansion)
+#define SET_OPTION(_x, _y)\
+do {\
+ if ((ret = curl_easy_setopt(candle, _x, _y)) != CURLE_OK) {\
+ option = STRINGIFY(_x);\
+ goto error;\
+ }\
+} while (0)
+
+/*
+ * that macro is originally declared in include/curl/curlver.h
+ * We have to use this as curl uses lots of enums
+ */
+#ifndef CURL_AT_LEAST_VERSION
+# define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
+# define CURL_AT_LEAST_VERSION(x, y, z) (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS(x, y, z))
+#endif
+
+const unsigned long http_curl_auth[HTTP_AUTH_NUM_ENTRIES] = {
+ 0, // HTTP_AUTH_UNKNOWN
+ 0, // HTTP_AUTH_NONE
+ CURLOPT_TLSAUTH_SRP, // HTTP_AUTH_TLS_SRP
+ CURLAUTH_BASIC, // HTTP_AUTH_BASIC
+ CURLAUTH_DIGEST, // HTTP_AUTH_DIGEST
+ CURLAUTH_DIGEST_IE, // HTTP_AUTH_DIGEST_IE
+ CURLAUTH_GSSNEGOTIATE, // HTTP_AUTH_GSSNEGOTIATE
+ CURLAUTH_NTLM, // HTTP_AUTH_NTLM
+ CURLAUTH_NTLM_WB, // HTTP_AUTH_NTLM_WB
+ CURLAUTH_ANY, // HTTP_AUTH_ANY
+ CURLAUTH_ANYSAFE // HTTP_AUTH_ANY_SAFE
+};
+
+
+/** Conversion table for method config values.
+ *
+ * HTTP verb strings for http_method_t enum values. Used by libcurl in the
+ * status line of the outgoing HTTP header, by rest_response_header for decoding
+ * incoming HTTP responses, and by the configuration parser.
+ *
+ * @note must be kept in sync with http_method_t enum.
+ *
+ * @see http_method_t
+ * @see fr_str2int
+ * @see fr_int2str
+ */
+const FR_NAME_NUMBER http_method_table[] = {
+ { "UNKNOWN", HTTP_METHOD_UNKNOWN },
+ { "GET", HTTP_METHOD_GET },
+ { "POST", HTTP_METHOD_POST },
+ { "PUT", HTTP_METHOD_PUT },
+ { "PATCH", HTTP_METHOD_PATCH },
+ { "DELETE", HTTP_METHOD_DELETE },
+
+ { NULL , -1 }
+};
+
+/** Conversion table for type config values.
+ *
+ * Textual names for http_body_type_t enum values, used by the
+ * configuration parser.
+ *
+ * @see http_body_Type_t
+ * @see fr_str2int
+ * @see fr_int2str
+ */
+const FR_NAME_NUMBER http_body_type_table[] = {
+ { "unknown", HTTP_BODY_UNKNOWN },
+ { "unsupported", HTTP_BODY_UNSUPPORTED },
+ { "unavailable", HTTP_BODY_UNAVAILABLE },
+ { "invalid", HTTP_BODY_INVALID },
+ { "none", HTTP_BODY_NONE },
+ { "post", HTTP_BODY_POST },
+ { "json", HTTP_BODY_JSON },
+ { "xml", HTTP_BODY_XML },
+ { "yaml", HTTP_BODY_YAML },
+ { "html", HTTP_BODY_HTML },
+ { "plain", HTTP_BODY_PLAIN },
+
+ { NULL , -1 }
+};
+
+const FR_NAME_NUMBER http_auth_table[] = {
+ { "none", HTTP_AUTH_NONE },
+ { "srp", HTTP_AUTH_TLS_SRP },
+ { "basic", HTTP_AUTH_BASIC },
+ { "digest", HTTP_AUTH_DIGEST },
+ { "digest-ie", HTTP_AUTH_DIGEST_IE },
+ { "gss-negotiate", HTTP_AUTH_GSSNEGOTIATE },
+ { "ntlm", HTTP_AUTH_NTLM },
+ { "ntlm-winbind", HTTP_AUTH_NTLM_WB },
+ { "any", HTTP_AUTH_ANY },
+ { "safe", HTTP_AUTH_ANY_SAFE },
+
+ { NULL , -1 }
+};
+
+/** Conversion table for "Content-Type" header values.
+ *
+ * Used by rest_response_header for parsing incoming headers.
+ *
+ * Values we expect to see in the 'Content-Type:' header of the incoming
+ * response.
+ *
+ * Some data types (like YAML) do no have standard MIME types defined,
+ * so multiple types, are listed here.
+ *
+ * @see http_body_Type_t
+ * @see fr_str2int
+ * @see fr_int2str
+ */
+const FR_NAME_NUMBER http_content_type_table[] = {
+ { "application/x-www-form-urlencoded", HTTP_BODY_POST },
+ { "application/json", HTTP_BODY_JSON },
+ { "text/html", HTTP_BODY_HTML },
+ { "text/plain", HTTP_BODY_PLAIN },
+ { "text/xml", HTTP_BODY_XML },
+ { "text/yaml", HTTP_BODY_YAML },
+ { "text/x-yaml", HTTP_BODY_YAML },
+ { "application/yaml", HTTP_BODY_YAML },
+ { "application/x-yaml", HTTP_BODY_YAML },
+
+ { NULL , -1 }
+};
+
+/** Conversion table for "HTTP" protocol version to use.
+ *
+ * Used by rlm_rest_t for specify the http client version.
+ *
+ * Values we expect to use in curl_easy_setopt()
+ *
+ * @see fr_str2int
+ * @see fr_int2str
+ */
+const FR_NAME_NUMBER http_negotiation_table[] = {
+
+ { "1.0", CURL_HTTP_VERSION_1_0 }, //!< Enforce HTTP 1.0 requests.
+ { "1.1", CURL_HTTP_VERSION_1_1 }, //!< Enforce HTTP 1.1 requests.
+/*
+ * These are all enum values
+ */
+#if CURL_AT_LEAST_VERSION(7,49,0)
+ { "2.0", CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE }, //!< Enforce HTTP 2.0 requests.
+#endif
+#if CURL_AT_LEAST_VERSION(7,33,0)
+ { "2.0+auto", CURL_HTTP_VERSION_2_0 }, //!< Attempt HTTP 2 requests. libcurl will fall back
+ ///< to HTTP 1.1 if HTTP 2 can't be negotiated with the
+ ///< server. (Added in 7.33.0)
+#endif
+#if CURL_AT_LEAST_VERSION(7,47,0)
+ { "2.0+tls", CURL_HTTP_VERSION_2TLS }, //!< Attempt HTTP 2 over TLS (HTTPS) only.
+ ///< libcurl will fall back to HTTP 1.1 if HTTP 2
+ ///< can't be negotiated with the HTTPS server.
+ ///< For clear text HTTP servers, libcurl will use 1.1.
+#endif
+ { "default", CURL_HTTP_VERSION_NONE } //!< We don't care about what version the library uses.
+ ///< libcurl will use whatever it thinks fit.
+};
+
+/*
+ * Encoder specific structures.
+ * @todo split encoders/decoders into submodules.
+ */
+typedef struct rest_custom_data {
+ char const *p; //!< how much text we've sent so far.
+} rest_custom_data_t;
+
+#ifdef HAVE_JSON
+/** Flags to control the conversion of JSON values to VALUE_PAIRs.
+ *
+ * These fields are set when parsing the expanded format for value pairs in
+ * JSON, and control how json_pair_make_leaf and json_pair_make convert the JSON
+ * value, and move the new VALUE_PAIR into an attribute list.
+ *
+ * @see json_pair_make
+ * @see json_pair_make_leaf
+ */
+typedef struct json_flags {
+ int do_xlat; //!< If true value will be expanded with xlat.
+ int is_json; //!< If true value will be inserted as raw JSON
+ // (multiple values not supported).
+ FR_TOKEN op; //!< The operator that determines how the new VP
+ // is processed. @see fr_tokens_table
+
+ int8_t tag; //!< Tag to assign to VP.
+} json_flags_t;
+#endif
+
+/** Initialises libcurl.
+ *
+ * Allocates global variables and memory required for libcurl to function.
+ * MUST only be called once per module instance.
+ *
+ * rest_cleanup must not be called if rest_init fails.
+ *
+ * @see rest_cleanup
+ *
+ * @param[in] instance configuration data.
+ * @return 0 if init succeeded -1 if it failed.
+ */
+int rest_init(rlm_rest_t *instance)
+{
+ static bool version_done;
+ CURLcode ret;
+
+ /* developer sanity */
+ rad_assert((sizeof(http_body_type_supported) / sizeof(*http_body_type_supported)) == HTTP_BODY_NUM_ENTRIES);
+
+ ret = curl_global_init(CURL_GLOBAL_ALL);
+ if (ret != CURLE_OK) {
+ ERROR("rlm_rest (%s): CURL init returned error: %i - %s",
+ instance->xlat_name,
+ ret, curl_easy_strerror(ret));
+
+ curl_global_cleanup();
+ return -1;
+ }
+
+ if (!version_done) {
+ curl_version_info_data *curlversion;
+
+ version_done = true;
+
+ curlversion = curl_version_info(CURLVERSION_NOW);
+ if (strcmp(LIBCURL_VERSION, curlversion->version) != 0) {
+ WARN("rlm_rest: libcurl version changed since the server was built");
+ WARN("rlm_rest: linked: %s built: %s", curlversion->version, LIBCURL_VERSION);
+ }
+
+ INFO("rlm_rest: libcurl version: %s", curl_version());
+ }
+
+ return 0;
+}
+
+/** Cleans up after libcurl.
+ *
+ * Wrapper around curl_global_cleanup, frees any memory allocated by rest_init.
+ * Must only be called once per call of rest_init.
+ *
+ * @see rest_init
+ */
+void rest_cleanup(void)
+{
+ curl_global_cleanup();
+}
+
+
+/** Frees a libcurl handle, and any additional memory used by context data.
+ *
+ * @param[in] randle rlm_rest_handle_t to close and free.
+ * @return returns true.
+ */
+static int _mod_conn_free(rlm_rest_handle_t *randle)
+{
+ curl_easy_cleanup(randle->handle);
+
+ return 0;
+}
+
+/** Creates a new connection handle for use by the FR connection API.
+ *
+ * Matches the fr_connection_create_t function prototype, is passed to
+ * fr_connection_pool_init, and called when a new connection is required by the
+ * connection pool API.
+ *
+ * Creates an instances of rlm_rest_handle_t, and rlm_rest_curl_context_t
+ * which hold the context data required for generating requests and parsing
+ * responses.
+ *
+ * If instance->connect_uri is not NULL libcurl will attempt to open a
+ * TCP socket to the server specified in the URI. This is done so that when the
+ * socket is first used, there will already be a cached TCP connection to the
+ * REST server associated with the curl handle.
+ *
+ * @see fr_connection_pool_init
+ * @see fr_connection_create_t
+ * @see connection.c
+ */
+void *mod_conn_create(TALLOC_CTX *ctx, void *instance)
+{
+ rlm_rest_t *inst = instance;
+
+ rlm_rest_handle_t *randle = NULL;
+ rlm_rest_curl_context_t *curl_ctx = NULL;
+
+ CURL *candle = curl_easy_init();
+
+ CURLcode ret = CURLE_OK;
+ char const *option = "unknown";
+
+ if (!candle) {
+ ERROR("rlm_rest (%s): Failed to create CURL handle", inst->xlat_name);
+ return NULL;
+ }
+
+ SET_OPTION(CURLOPT_CONNECTTIMEOUT_MS, inst->connect_timeout);
+
+ if (inst->connect_uri) {
+ /*
+ * re-establish TCP connection to webserver. This would usually be
+ * done on the first request, but we do it here to minimise
+ * latency.
+ */
+ SET_OPTION(CURLOPT_SSL_VERIFYPEER, 0);
+ SET_OPTION(CURLOPT_SSL_VERIFYHOST, 0);
+ SET_OPTION(CURLOPT_CONNECT_ONLY, 1);
+ SET_OPTION(CURLOPT_URL, inst->connect_uri);
+ SET_OPTION(CURLOPT_NOSIGNAL, 1);
+
+ DEBUG("rlm_rest (%s): Connecting to \"%s\"", inst->xlat_name, inst->connect_uri);
+
+ ret = curl_easy_perform(candle);
+ if (ret != CURLE_OK) {
+ ERROR("rlm_rest (%s): Connection failed: %i - %s", inst->xlat_name, ret, curl_easy_strerror(ret));
+
+ goto connection_error;
+ }
+ } else {
+ DEBUG2("rlm_rest (%s): Skipping pre-connect, connect_uri not specified", inst->xlat_name);
+ }
+
+ /*
+ * Allocate memory for the connection handle abstraction.
+ */
+ randle = talloc_zero(ctx, rlm_rest_handle_t);
+ curl_ctx = talloc_zero(randle, rlm_rest_curl_context_t);
+
+ curl_ctx->headers = NULL; /* CURL needs this to be NULL */
+ curl_ctx->request.instance = inst;
+
+ randle->ctx = curl_ctx;
+ randle->handle = candle;
+ talloc_set_destructor(randle, _mod_conn_free);
+
+ /*
+ * Clear any previously configured options for the first request.
+ */
+ curl_easy_reset(candle);
+
+ return randle;
+
+ /*
+ * Cleanup for error conditions.
+ */
+error:
+ ERROR("rlm_rest (%s): Failed setting curl option %s: %s (%i)", inst->xlat_name, option,
+ curl_easy_strerror(ret), ret);
+
+ /*
+ * So we don't leak CURL handles.
+ */
+connection_error:
+ curl_easy_cleanup(candle);
+ if (randle) talloc_free(randle);
+
+ return NULL;
+}
+
+/** Verifies that the last TCP socket associated with a handle is still active.
+ *
+ * Quieries libcurl to try and determine if the TCP socket associated with a
+ * connection handle is still viable.
+ *
+ * @param[in] instance configuration data.
+ * @param[in] handle to check.
+ * @returns false if the last socket is dead, or if the socket state couldn't be
+ * determined, else true.
+ */
+int mod_conn_alive(void *instance, void *handle)
+{
+ rlm_rest_t *inst = instance;
+ rlm_rest_handle_t *randle = handle;
+ CURL *candle = randle->handle;
+
+ long last_socket;
+ CURLcode ret;
+
+ ret = curl_easy_getinfo(candle, CURLINFO_LASTSOCKET, &last_socket);
+ if (ret != CURLE_OK) {
+ ERROR("rlm_rest (%s): Couldn't determine socket state: %i - %s", inst->xlat_name, ret,
+ curl_easy_strerror(ret));
+
+ return false;
+ }
+
+ if (last_socket == -1) {
+ return false;
+ }
+
+ return true;
+}
+
+/** Copies a pre-expanded xlat string to the output buffer
+ *
+ * @param[out] out Char buffer to write encoded data to.
+ * @param[in] size Multiply by nmemb to get the length of ptr.
+ * @param[in] nmemb Multiply by size to get the length of ptr.
+ * @param[in] userdata rlm_rest_request_t to keep encoding state between calls.
+ * @return length of data (including NULL) written to ptr, or 0 if no more
+ * data to write.
+ */
+static size_t rest_encode_custom(void *out, size_t size, size_t nmemb, void *userdata)
+{
+ rlm_rest_request_t *ctx = userdata;
+ rest_custom_data_t *data = ctx->encoder;
+
+ size_t freespace = (size * nmemb) - 1;
+ size_t len;
+
+ len = strlcpy(out, data->p, freespace);
+ if (is_truncated(len, freespace)) {
+ data->p += (freespace - 1);
+ return freespace - 1;
+ }
+ data->p += len;
+
+ return len;
+}
+
+/** Encodes VALUE_PAIR linked list in POST format
+ *
+ * This is a stream function matching the rest_read_t prototype. Multiple
+ * successive calls will return additional encoded VALUE_PAIRs.
+ * Only complete attribute headers @verbatim '<name>=' @endverbatim and values
+ * will be written to the ptr buffer.
+ *
+ * POST request format is:
+ * @verbatim <attribute0>=<value0>&<attribute1>=<value1>&<attributeN>=<valueN>@endverbatim
+ *
+ * All attributes and values are url encoded. There is currently no support for
+ * nested attributes, or attribute qualifiers.
+ *
+ * Nested attributes may be added in the future using
+ * @verbatim <attribute-outer>:<attribute-inner>@endverbatim
+ * to denotate nesting.
+ *
+ * Requires libcurl for url encoding.
+ *
+ * @see rest_decode_post
+ *
+ * @param[out] out Char buffer to write encoded data to.
+ * @param[in] size Multiply by nmemb to get the length of ptr.
+ * @param[in] nmemb Multiply by size to get the length of ptr.
+ * @param[in] userdata rlm_rest_request_t to keep encoding state between calls.
+ * @return length of data (including NULL) written to ptr, or 0 if no more
+ * data to write.
+ */
+static size_t rest_encode_post(void *out, size_t size, size_t nmemb, void *userdata)
+{
+ rlm_rest_request_t *ctx = userdata;
+ REQUEST *request = ctx->request; /* Used by RDEBUG */
+ VALUE_PAIR *vp;
+
+ char *p = out; /* Position in buffer */
+ char *encoded = p; /* Position in buffer of last fully encoded attribute or value */
+ char *escaped; /* Pointer to current URL escaped data */
+
+ size_t len = 0;
+ size_t freespace = (size * nmemb) - 1;
+
+ /* Allow manual chunking */
+ if ((ctx->chunk) && (ctx->chunk <= freespace)) {
+ freespace = (ctx->chunk - 1);
+ }
+
+ if (ctx->state == READ_STATE_END) return 0;
+
+ /* Post data requires no headers */
+ if (ctx->state == READ_STATE_INIT) ctx->state = READ_STATE_ATTR_BEGIN;
+
+ while (freespace > 0) {
+ vp = fr_cursor_current(&ctx->cursor);
+ if (!vp) {
+ ctx->state = READ_STATE_END;
+
+ break;
+ }
+
+ RDEBUG2("Encoding attribute \"%s\"", vp->da->name);
+
+ if (ctx->state == READ_STATE_ATTR_BEGIN) {
+ escaped = curl_escape(vp->da->name, strlen(vp->da->name));
+ if (!escaped) {
+ REDEBUG("Failed escaping string \"%s\"", vp->da->name);
+ return 0;
+ }
+
+ len = strlen(escaped);
+ if (freespace < (1 + len)) {
+ curl_free(escaped);
+ goto no_space;
+ }
+
+ len = sprintf(p, "%s=", escaped);
+ curl_free(escaped);
+ p += len;
+ freespace -= len;
+
+ /*
+ * We wrote the attribute header, record progress.
+ */
+ encoded = p;
+ ctx->state = READ_STATE_ATTR_CONT;
+ }
+
+ /*
+ * Write out single attribute string.
+ */
+ len = vp_prints_value(p, freespace, vp, 0);
+ if (is_truncated(len, freespace)) goto no_space;
+
+ RINDENT();
+ RDEBUG3("Length : %zd", len);
+ REXDENT();
+ if (len > 0) {
+ escaped = curl_escape(p, len);
+ if (!escaped) {
+ REDEBUG("Failed escaping string \"%s\"", vp->da->name);
+ return 0;
+ }
+ len = strlen(escaped);
+
+ if (freespace < len) {
+ curl_free(escaped);
+ goto no_space;
+ }
+
+ len = strlcpy(p, escaped, len + 1);
+
+ curl_free(escaped);
+
+ RINDENT();
+ RDEBUG3("Value : %s", p);
+ REXDENT();
+
+ p += len;
+ freespace -= len;
+ }
+
+ /*
+ * there are no more attributes, stop
+ */
+ if (!fr_cursor_next_peek(&ctx->cursor)) {
+ ctx->state = READ_STATE_END;
+ break;
+ }
+
+ if (freespace < 1) goto no_space;
+ *p++ = '&';
+ freespace--;
+ /*
+ * Only advance once we have a separator
+ * really we should have an additional
+ * state for encoding the separator,
+ * but, we don't, and v3.0.x is stable
+ * so let's do the easiest fix with the
+ * lowest risk.
+ */
+ fr_cursor_next(&ctx->cursor);
+
+ /*
+ * We wrote one full attribute value pair, record progress.
+ */
+ encoded = p;
+ ctx->state = READ_STATE_ATTR_BEGIN;
+ }
+
+ *p = '\0';
+
+ len = p - (char *)out;
+
+ RDEBUG3("POST Data: %s", (char *)out);
+ RDEBUG3("Returning %zd bytes of POST data", len);
+
+ return len;
+
+ /*
+ * Cleanup for error conditions
+ */
+no_space:
+ *encoded = '\0';
+
+ len = encoded - (char *)out;
+
+ RDEBUG3("POST Data: %s", (char *)out);
+
+ /*
+ * The buffer wasn't big enough to encode a single attribute chunk.
+ */
+ if (len == 0) {
+ REDEBUG("Failed encoding attribute");
+ } else {
+ RDEBUG3("Returning %zd bytes of POST data (buffer full or chunk exceeded)", len);
+ }
+
+ return len;
+}
+
+#ifdef HAVE_JSON
+/** Encodes VALUE_PAIR linked list in JSON format
+ *
+ * This is a stream function matching the rest_read_t prototype. Multiple
+ * successive calls will return additional encoded VALUE_PAIRs.
+ *
+ * Only complete attribute headers
+ * @verbatim "<name>":{"type":"<type>","value":[' @endverbatim
+ * and complete attribute values will be written to ptr.
+ *
+ * If an attribute occurs multiple times in the request the attribute values
+ * will be concatenated into a single value array.
+ *
+ * JSON request format is:
+@verbatim
+{
+ "<attribute0>":{
+ "type":"<type0>",
+ "value":[<value0>,<value1>,<valueN>]
+ },
+ "<attribute1>":{
+ "type":"<type1>",
+ "value":[...]
+ },
+ "<attributeN>":{
+ "type":"<typeN>",
+ "value":[...]
+ },
+}
+@endverbatim
+ *
+ * @param[out] out Char buffer to write encoded data to.
+ * @param[in] size Multiply by nmemb to get the length of ptr.
+ * @param[in] nmemb Multiply by size to get the length of ptr.
+ * @param[in] userdata rlm_rest_request_t to keep encoding state between calls.
+ * @return length of data (including NULL) written to ptr, or 0 if no more
+ * data to write.
+ */
+static size_t rest_encode_json(void *out, size_t size, size_t nmemb, void *userdata)
+{
+ rlm_rest_request_t *ctx = userdata;
+ REQUEST *request = ctx->request; /* Used by RDEBUG */
+ VALUE_PAIR *vp, *next;
+
+ char *p = out; /* Position in buffer */
+ char *encoded = p; /* Position in buffer of last fully encoded attribute or value */
+
+ char const *type;
+
+ size_t len = 0;
+ size_t freespace = (size * nmemb) - 1; /* account for the \0 byte here */
+
+ rad_assert(freespace > 0);
+
+ /* Allow manual chunking */
+ if ((ctx->chunk) && (ctx->chunk <= freespace)) {
+ freespace = (ctx->chunk - 1);
+ }
+
+ if (ctx->state == READ_STATE_END) return 0;
+
+ if (ctx->state == READ_STATE_INIT) {
+ ctx->state = READ_STATE_ATTR_BEGIN;
+
+ if (freespace < 1) goto no_space;
+ *p++ = '{';
+ freespace--;
+ }
+
+ for (;;) {
+ vp = fr_cursor_current(&ctx->cursor);
+
+ /*
+ * We've encoded all the VPs
+ *
+ * The check for READ_STATE_ATTR_BEGIN is needed as we might be in
+ * READ_STATE_ATTR_END, and need to close out the current attribute
+ * array.
+ */
+ if (!vp && (ctx->state == READ_STATE_ATTR_BEGIN)) {
+ if (freespace < 1) goto no_space;
+ *p++ = '}';
+ freespace--;
+
+ ctx->state = READ_STATE_END;
+
+ break;
+ }
+
+ if (ctx->state == READ_STATE_ATTR_BEGIN) {
+ /*
+ * New attribute, write name, type, and beginning of value array.
+ */
+ RDEBUG2("Encoding attribute \"%s\"", vp->da->name);
+
+ type = fr_int2str(dict_attr_types, vp->da->type, "<INVALID>");
+
+ if (ctx->section->attr_num) {
+ len = snprintf(p, freespace + 1, "\"%s\":{\"attr_num\":%d,\"type\":\"%s\",\"value\":[",
+ vp->da->name, vp->da->attr, type);
+ } else {
+ len = snprintf(p, freespace + 1, "\"%s\":{\"type\":\"%s\",\"value\":[", vp->da->name, type);
+ }
+
+ if (len >= freespace) goto no_space;
+ p += len;
+ freespace -= len;
+
+ RINDENT();
+ RDEBUG3("Type : %s", type);
+ REXDENT();
+ /*
+ * We wrote the attribute header, record progress
+ */
+ encoded = p;
+ ctx->state = READ_STATE_ATTR_CONT;
+ }
+
+ if (ctx->state == READ_STATE_ATTR_CONT) {
+ for (;;) {
+ size_t attr_space;
+
+ rad_assert(vp); /* coverity */
+
+ /*
+ * We need at least a single byte to write out the
+ * shortest attribute value.
+ */
+ if (freespace < 1) goto no_space;
+
+ /*
+ * Code below expects length of the buffer, so we
+ * add +1 to freespace.
+ *
+ * If we know we need a comma after the value, we
+ * need to -1 to make sure we have enough room to
+ * write that out.
+ */
+ attr_space = fr_cursor_next_peek(&ctx->cursor) ? freespace - 1 : freespace;
+ len = vp_prints_value_json(p, attr_space + 1, vp, ctx->section->raw_value);
+ if (is_truncated(len, attr_space + 1)) goto no_space;
+
+ /*
+ * Show actual value length minus quotes
+ */
+ RINDENT();
+ RDEBUG3("Length : %zu", (size_t) (*p == '"') ? (len - 2) : len);
+ RDEBUG3("Value : %s", p);
+ REXDENT();
+
+ p += len;
+ freespace -= len;
+ encoded = p;
+
+ /*
+ * Multivalued attribute, we sorted all the attributes earlier, so multiple
+ * instances should occur in a contiguous block.
+ */
+ if ((next = fr_cursor_next(&ctx->cursor)) && (vp->da == next->da)) {
+ rad_assert(freespace >= 1);
+ *p++ = ',';
+ freespace--;
+
+ /*
+ * We wrote one attribute value, record progress.
+ */
+ encoded = p;
+ vp = next;
+ continue;
+ }
+ break;
+ }
+ ctx->state = READ_STATE_ATTR_END;
+ }
+
+ if (ctx->state == READ_STATE_ATTR_END) {
+ next = fr_cursor_current(&ctx->cursor);
+ if (freespace < 2) goto no_space;
+ *p++ = ']';
+ *p++ = '}';
+ freespace -= 2;
+
+ if (next) {
+ if (freespace < 1) goto no_space;
+ *p++ = ',';
+ freespace--;
+ }
+
+ /*
+ * We wrote one full attribute value pair, record progress.
+ */
+ encoded = p;
+ ctx->state = READ_STATE_ATTR_BEGIN;
+ }
+ }
+
+ *p = '\0';
+
+ len = p - (char *)out;
+
+ RDEBUG3("JSON Data: %s", (char *)out);
+ RDEBUG3("Returning %zd bytes of JSON data", len);
+
+ return len;
+
+ /*
+ * Were out of buffer space
+ */
+no_space:
+ *encoded = '\0';
+
+ len = encoded - (char *)out;
+
+ RDEBUG3("JSON Data: %s", (char *)out);
+
+ /*
+ * The buffer wasn't big enough to encode a single attribute chunk.
+ */
+ if (len == 0) {
+ REDEBUG("AVP exceeds buffer length or chunk");
+ } else {
+ RDEBUG2("Returning %zd bytes of JSON data (buffer full or chunk exceeded)", len);
+ }
+
+ return len;
+}
+#endif
+
+/** Emulates successive libcurl calls to an encoding function
+ *
+ * This function is used when the request will be sent to the HTTP server as one
+ * contiguous entity. A buffer of REST_BODY_INIT bytes is allocated and passed
+ * to the stream encoding function.
+ *
+ * If the stream function does not return 0, a new buffer is allocated which is
+ * the size of the previous buffer + REST_BODY_INIT bytes, the data from the
+ * previous buffer is copied, and freed, and another call is made to the stream
+ * function, passing a pointer into the new buffer at the end of the previously
+ * written data.
+ *
+ * This process continues until the stream function signals (by returning 0)
+ * that it has no more data to write.
+ *
+ * @param[out] buffer where the pointer to the alloced buffer should
+ * be written.
+ * @param[in] func Stream function.
+ * @param[in] limit Maximum buffer size to alloc.
+ * @param[in] userdata rlm_rest_request_t to keep encoding state between calls to
+ * stream function.
+ * @return the length of the data written to the buffer (excluding NULL) or -1
+ * if alloc >= limit.
+ */
+static ssize_t rest_request_encode_wrapper(char **buffer, rest_read_t func, size_t limit, void *userdata)
+{
+ char *previous = NULL;
+ char *current = NULL;
+
+ size_t alloc = REST_BODY_INIT; /* Size of buffer to alloc */
+ size_t used = 0; /* Size of data written */
+ size_t len = 0;
+ rlm_rest_request_t *ctx = userdata;
+
+ while (alloc <= limit) {
+ current = rad_malloc(alloc);
+
+ if (previous) {
+ strlcpy(current, previous, used + 1);
+ free(previous);
+ }
+
+ len = func(current + used, alloc - used, 1, userdata);
+ used += len;
+ if (ctx->state == READ_STATE_END || !len) {
+ *buffer = current;
+ return used;
+ }
+
+ alloc = alloc * 2;
+ previous = current;
+ };
+
+ free(current);
+
+ return -1;
+}
+
+/** (Re-)Initialises the data in a rlm_rest_request_t.
+ *
+ * Resets the values of a rlm_rest_request_t to their defaults.
+ *
+ * @param[in] request Current request.
+ * @param[in] ctx to initialise.
+ * @param[in] sort If true VALUE_PAIRs will be sorted within the VALUE_PAIR
+ * pointer array.
+ */
+static void rest_request_init(REQUEST *request, rlm_rest_request_t *ctx, bool sort)
+{
+ /*
+ * Setup stream read data
+ */
+ ctx->request = request;
+ ctx->state = READ_STATE_INIT;
+
+ /*
+ * Sorts pairs in place, oh well...
+ */
+ if (sort) {
+ fr_pair_list_sort(&request->packet->vps, fr_pair_cmp_by_da_tag);
+ }
+ fr_cursor_init(&ctx->cursor, &request->packet->vps);
+}
+
+/** Converts plain response into a single VALUE_PAIR
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] handle rlm_rest_handle_t to use.
+ * @param[in] request Current request.
+ * @param[in] raw buffer containing POST data.
+ * @param[in] rawlen Length of data in raw buffer.
+ * @return the number of VALUE_PAIRs processed or -1 on unrecoverable error.
+ */
+static int rest_decode_plain(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t *section,
+ REQUEST *request, UNUSED void *handle, char *raw, size_t rawlen)
+{
+ VALUE_PAIR *vp;
+
+ /*
+ * Empty response?
+ */
+ if (*raw == '\0') return 0;
+
+ /*
+ * Use rawlen to protect against overrun, and to cope with any binary data
+ */
+ vp = pair_make_reply("REST-HTTP-Body", NULL, T_OP_ADD);
+ fr_pair_value_bstrncpy(vp, raw, rawlen);
+
+ RDEBUG2("Adding reply:REST-HTTP-Body += \"%s\"", vp->vp_strvalue);
+
+ return 1;
+}
+
+/** Converts POST response into VALUE_PAIRs and adds them to the request
+ *
+ * Accepts VALUE_PAIRS in the same format as rest_encode_post, but with the
+ * addition of optional attribute list qualifiers as part of the attribute name
+ * string.
+ *
+ * If no qualifiers are specified, will default to the request list.
+ *
+ * POST response format is:
+ * @verbatim [outer.][<list>:]<attribute0>=<value0>&[outer.][<list>:]<attribute1>=<value1>&[outer.][<list>:]<attributeN>=<valueN> @endverbatim
+ *
+ * @see rest_encode_post
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] handle rlm_rest_handle_t to use.
+ * @param[in] request Current request.
+ * @param[in] raw buffer containing POST data.
+ * @param[in] rawlen Length of data in raw buffer.
+ * @return the number of VALUE_PAIRs processed or -1 on unrecoverable error.
+ */
+static int rest_decode_post(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t *section,
+ REQUEST *request, void *handle, char *raw, size_t rawlen)
+{
+ rlm_rest_handle_t *randle = handle;
+ CURL *candle = randle->handle;
+
+ char const *p = raw, *q;
+
+ char const *attribute;
+ char *name = NULL;
+ char *value = NULL;
+
+ char *expanded = NULL;
+
+ DICT_ATTR const *da;
+ VALUE_PAIR *vp;
+
+ pair_lists_t list_name;
+ request_refs_t request_name;
+ REQUEST *reference = request;
+ VALUE_PAIR **vps;
+ TALLOC_CTX *ctx;
+
+ size_t len;
+ int curl_len; /* Length from last curl_easy_unescape call */
+
+ int count = 0;
+ int ret;
+
+ /*
+ * Empty response?
+ */
+ while (isspace((uint8_t) *p)) p++;
+ if (*p == '\0') return 0;
+
+ while (((q = strchr(p, '=')) != NULL) && (count < REST_BODY_MAX_ATTRS)) {
+ reference = request;
+
+ name = curl_easy_unescape(candle, p, (q - p), &curl_len);
+ p = (q + 1);
+
+ RDEBUG2("Parsing attribute \"%s\"", name);
+
+ /*
+ * The attribute pointer is updated to point to the portion of
+ * the string after the list qualifier.
+ */
+ attribute = name;
+ attribute += radius_request_name(&request_name, attribute, REQUEST_CURRENT);
+ if (request_name == REQUEST_UNKNOWN) {
+ RWDEBUG("Invalid request qualifier, skipping");
+
+ curl_free(name);
+
+ continue;
+ }
+
+ if (radius_request(&reference, request_name) < 0) {
+ RWDEBUG("Attribute name refers to outer request but not in a tunnel, skipping");
+
+ curl_free(name);
+
+ continue;
+ }
+
+ attribute += radius_list_name(&list_name, attribute, PAIR_LIST_REPLY);
+ if (list_name == PAIR_LIST_UNKNOWN) {
+ RWDEBUG("Invalid list qualifier, skipping");
+ curl_free(name);
+
+ continue;
+ }
+
+ da = dict_attrbyname(attribute);
+ if (!da) {
+ RWDEBUG("Attribute \"%s\" unknown, skipping", attribute);
+
+ curl_free(name);
+
+ continue;
+ }
+
+ vps = radius_list(reference, list_name);
+ rad_assert(vps);
+
+ RINDENT();
+ RDEBUG3("Type : %s", fr_int2str(dict_attr_types, da->type, "<INVALID>"));
+
+ ctx = radius_list_ctx(reference, list_name);
+
+ q = strchr(p, '&');
+ len = (!q) ? (rawlen - (p - raw)) : (unsigned)(q - p);
+
+ value = curl_easy_unescape(candle, p, len, &curl_len);
+
+ /*
+ * If we found a delimiter we want to skip over it,
+ * if we didn't we do *NOT* want to skip over the end
+ * of the buffer...
+ */
+ p += (!q) ? len : (len + 1);
+
+ RDEBUG3("Length : %i", curl_len);
+ RDEBUG3("Value : \"%s\"", value);
+ REXDENT();
+
+ RDEBUG2("Performing xlat expansion of response value");
+
+ if (radius_axlat(&expanded, request, value, NULL, NULL) < 0) {
+ goto skip;
+ }
+
+ vp = fr_pair_afrom_da(ctx, da);
+ if (!vp) {
+ REDEBUG("Failed creating valuepair");
+ talloc_free(expanded);
+
+ goto error;
+ }
+
+ ret = fr_pair_value_from_str(vp, expanded, -1);
+ TALLOC_FREE(expanded);
+ if (ret < 0) {
+ RWDEBUG("Incompatible value assignment, skipping");
+ talloc_free(vp);
+ goto skip;
+ }
+
+ fr_pair_add(vps, vp);
+
+ count++;
+
+ skip:
+ curl_free(name);
+ curl_free(value);
+
+ continue;
+
+ error:
+ curl_free(name);
+ curl_free(value);
+
+ return count;
+ }
+
+ if (!count) {
+ REDEBUG("Malformed POST data \"%s\"", raw);
+ }
+
+ return count;
+
+}
+
+#ifdef HAVE_JSON
+/** Converts JSON "value" key into VALUE_PAIR.
+ *
+ * If leaf is not in fact a leaf node, but contains JSON data, the data will
+ * written to the attribute in JSON string format.
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] ctx to allocate new VALUE_PAIRs in.
+ * @param[in] request Current request.
+ * @param[in] da Attribute to create.
+ * @param[in] flags containing the operator other flags controlling value
+ * expansion.
+ * @param[in] leaf object containing the VALUE_PAIR value.
+ * @return The VALUE_PAIR just created, or NULL on error.
+ */
+static VALUE_PAIR *json_pair_make_leaf(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t *section,
+ TALLOC_CTX *ctx, REQUEST *request, DICT_ATTR const *da,
+ json_flags_t *flags, json_object *leaf)
+{
+ char const *value, *to_parse;
+ char *expanded = NULL;
+ int ret;
+
+ VALUE_PAIR *vp;
+
+ if (json_object_is_type(leaf, json_type_null)) {
+ RDEBUG3("Got null value for attribute \"%s\", skipping...", da->name);
+
+ return NULL;
+ }
+
+ /*
+ * Should encode any nested JSON structures into JSON strings.
+ *
+ * "I knew you liked JSON so I put JSON in your JSON!"
+ */
+ value = json_object_get_string(leaf);
+ if (!value) {
+ RWDEBUG("Failed getting string value for attribute \"%s\", skipping...", da->name);
+
+ return NULL;
+ }
+
+ RINDENT();
+ RDEBUG3("Type : %s", fr_int2str(dict_attr_types, da->type, "<INVALID>"));
+ RDEBUG3("Length : %zu", strlen(value));
+ RDEBUG3("Value : \"%s\"", value);
+ REXDENT();
+
+ if (flags->do_xlat) {
+ if (radius_axlat(&expanded, request, value, NULL, NULL) < 0) {
+ return NULL;
+ }
+
+ to_parse = expanded;
+ } else {
+ to_parse = value;
+ }
+
+ vp = fr_pair_afrom_da(ctx, da);
+ if (!vp) {
+ RWDEBUG("Failed creating valuepair for attribute \"%s\", skipping...", da->name);
+ talloc_free(expanded);
+
+ return NULL;
+ }
+
+ ret = fr_pair_value_from_str(vp, to_parse, -1);
+ talloc_free(expanded);
+ if (ret < 0) {
+ RWDEBUG("Incompatible value assignment for attribute \"%s\", skipping...", da->name);
+ talloc_free(vp);
+
+ return NULL;
+ }
+
+ vp->op = flags->op;
+ vp->tag = flags->tag;
+
+ return vp;
+}
+
+/** Processes JSON response and converts it into multiple VALUE_PAIRs
+ *
+ * Processes JSON attribute declarations in the format below. Will recurse when
+ * processing nested attributes. When processing nested attributes flags and
+ * operators from previous attributes are not inherited.
+ *
+ * JSON response format is:
+@verbatim
+{
+ "<attribute0>":{
+ "do_xlat":<bool>,
+ "is_json":<bool>,
+ "op":"<operator>",
+ "value":[<value0>,<value1>,<valueN>]
+ },
+ "<attribute1>":{
+ "value":{
+ "<nested-attribute0>":{
+ "op":"<operator>",
+ "value":<value0>
+ }
+ }
+ },
+ "<attribute2>":"<value0>",
+ "<attributeN>":[<value0>,<value1>,<valueN>]
+}
+@endverbatim
+ *
+ * JSON valuepair flags:
+ * - do_xlat (optional) Controls xlat expansion of values. Defaults to true.
+ * - is_json (optional) If true, any nested JSON data will be copied to the
+ * VALUE_PAIR in string form. Defaults to true.
+ * - op (optional) Controls how the attribute is inserted into
+ * the target list. Defaults to ':=' (T_OP_SET).
+ *
+ * If "op" is ':=' or '=', it will be automagically changed to '+=' for the
+ * second and subsequent values in multivalued attributes. This does not work
+ * between multiple attribute declarations.
+ *
+ * @see fr_tokens
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] request Current request.
+ * @param[in] object containing root node, or parent node.
+ * @param[in] level Current nesting level.
+ * @param[in] max counter, decremented after each VALUE_PAIR is created,
+ * when 0 no more attributes will be processed.
+ * @return number of attributes created or < 0 on error.
+ */
+static int json_pair_make(rlm_rest_t *instance, rlm_rest_section_t *section,
+ REQUEST *request, json_object *object, UNUSED int level, int max)
+{
+ struct lh_entry *entry;
+ int max_attrs = max;
+
+ if (!json_object_is_type(object, json_type_object)) {
+#ifdef HAVE_JSON_TYPE_TO_NAME
+ REDEBUG("Can't process VP container, expected JSON object"
+ "got \"%s\", skipping...",
+ json_type_to_name(json_object_get_type(object)));
+#else
+ REDEBUG("Can't process VP container, expected JSON object"
+ ", skipping...");
+#endif
+ return -1;
+ }
+
+ /*
+ * Process VP container
+ */
+ for (entry = json_object_get_object(object)->head;
+ entry;
+ entry = entry->next) {
+ int i = 0, elements;
+ struct json_object *value, *element, *tmp;
+ TALLOC_CTX *ctx;
+
+ char const *name = (char const *)entry->k;
+
+ json_flags_t flags = {
+ .op = T_OP_SET,
+ .do_xlat = 1,
+ .is_json = 0
+ };
+
+ vp_tmpl_t dst;
+ REQUEST *current = request;
+ VALUE_PAIR **vps, *vp = NULL;
+
+ memset(&dst, 0, sizeof(dst));
+
+ /* Fix the compiler warnings regarding const... */
+ memcpy(&value, &entry->v, sizeof(value));
+
+ /*
+ * Resolve attribute name to a dictionary entry and pairlist.
+ */
+ RDEBUG2("Parsing attribute \"%s\"", name);
+
+ if (tmpl_from_attr_str(&dst, name, REQUEST_CURRENT, PAIR_LIST_REPLY, false, false) <= 0) {
+ RWDEBUG("Failed parsing attribute: %s, skipping...", fr_strerror());
+ continue;
+ }
+
+ if (radius_request(&current, dst.tmpl_request) < 0) {
+ RWDEBUG("Attribute name refers to outer request but not in a tunnel, skipping...");
+ continue;
+ }
+
+ vps = radius_list(current, dst.tmpl_list);
+ if (!vps) {
+ RWDEBUG("List not valid in this context, skipping...");
+ continue;
+ }
+ ctx = radius_list_ctx(current, dst.tmpl_list);
+
+ /*
+ * Alternative JSON structure which allows operator,
+ * and other flags to be specified.
+ *
+ * "<name>":{
+ * "do_xlat":<bool>,
+ * "is_json":<bool>,
+ * "op":"<op>",
+ * "value":<value>
+ * }
+ *
+ * Where value is a:
+ * - [] Multivalued array
+ * - {} Nested Valuepair
+ * - * Integer or string value
+ */
+ if (json_object_is_type(value, json_type_object)) {
+ /*
+ * Process operator if present.
+ */
+ if (json_object_object_get_ex(value, "op", &tmp)) {
+ flags.op = fr_str2int(fr_tokens, json_object_get_string(tmp), 0);
+ if (!flags.op) {
+ RWDEBUG("Invalid operator value \"%s\", skipping...",
+ json_object_get_string(tmp));
+ continue;
+ }
+ }
+
+ /*
+ * Process optional do_xlat bool.
+ */
+ if (json_object_object_get_ex(value, "do_xlat", &tmp)) {
+ flags.do_xlat = json_object_get_boolean(tmp);
+ }
+
+ /*
+ * Process optional is_json bool.
+ */
+ if (json_object_object_get_ex(value, "is_json", &tmp)) {
+ flags.is_json = json_object_get_boolean(tmp);
+ }
+
+ /*
+ * Value key must be present if were using the expanded syntax.
+ */
+ if (!json_object_object_get_ex(value, "value", &value)) {
+ RWDEBUG("Value key missing, skipping...");
+ continue;
+ }
+ }
+
+ /*
+ * Setup fr_pair_make / recursion loop.
+ */
+ if (!flags.is_json && json_object_is_type(value, json_type_array)) {
+ elements = json_object_array_length(value);
+ if (!elements) {
+ RWDEBUG("Zero length value array, skipping...");
+ continue;
+ }
+ element = json_object_array_get_idx(value, 0);
+ } else {
+ elements = 1;
+ element = value;
+ }
+
+ flags.tag = dst.tmpl_tag;
+
+ /*
+ * A JSON 'value' key, may have multiple elements, iterate
+ * over each of them, creating a new VALUE_PAIR.
+ */
+ do {
+ if (max_attrs-- <= 0) {
+ RWDEBUG("At maximum attribute limit");
+ return max;
+ }
+
+ /*
+ * Automagically switch the op for multivalued attributes.
+ */
+ if (((flags.op == T_OP_SET) || (flags.op == T_OP_EQ)) && (i >= 1)) {
+ flags.op = T_OP_ADD;
+ }
+
+ if (json_object_is_type(element, json_type_object) && !flags.is_json) {
+ /* TODO: Insert nested VP into VP structure...*/
+ RWDEBUG("Found nested VP, these are not yet supported, skipping...");
+
+ continue;
+
+ /*
+ vp = json_pair_make(instance, section,
+ request, value,
+ level + 1, max_attrs);*/
+ } else {
+ vp = json_pair_make_leaf(instance, section, ctx, request,
+ dst.tmpl_da, &flags, element);
+ if (!vp) continue;
+ }
+ rdebug_pair(2, request, vp, NULL);
+ radius_pairmove(current, vps, vp, false);
+ /*
+ * If we call json_object_array_get_idx on something that's not an array
+ * the behaviour appears to be to occasionally segfault.
+ */
+ } while ((++i < elements) && (element = json_object_array_get_idx(value, i)));
+ }
+
+ return max - max_attrs;
+}
+
+/** Converts JSON response into VALUE_PAIRs and adds them to the request.
+ *
+ * Converts the raw JSON string into a json-c object tree and passes it to
+ * json_pair_make. After the tree has been parsed json_object_put is called
+ * which decrements the reference count of the root node by one, and frees
+ * the entire tree.
+ *
+ * @see rest_encode_json
+ * @see json_pair_make
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in,out] request Current request.
+ * @param[in] handle REST handle.
+ * @param[in] raw buffer containing JSON data.
+ * @param[in] rawlen Length of data in raw buffer.
+ * @return the number of VALUE_PAIRs processed or -1 on unrecoverable error.
+ */
+static int rest_decode_json(rlm_rest_t *instance, rlm_rest_section_t *section,
+ REQUEST *request, UNUSED void *handle, char *raw, UNUSED size_t rawlen)
+{
+ char const *p = raw;
+
+ struct json_object *json;
+
+ int ret;
+
+ /*
+ * Empty response?
+ */
+ while (isspace((uint8_t) *p)) p++;
+ if (*p == '\0') return 0;
+
+ json = json_tokener_parse(p);
+ if (!json) {
+ REDEBUG("Malformed JSON data \"%s\"", raw);
+ return -1;
+ }
+
+ ret = json_pair_make(instance, section, request, json, 0, REST_BODY_MAX_ATTRS);
+
+ /*
+ * Decrement reference count for root object, should free entire JSON tree.
+ */
+ json_object_put(json);
+
+ return ret;
+}
+#endif
+
+/** Processes incoming HTTP header data from libcurl.
+ *
+ * Processes the status line, and Content-Type headers from the incoming HTTP
+ * response.
+ *
+ * Matches prototype for CURLOPT_HEADERFUNCTION, and will be called directly
+ * by libcurl.
+ *
+ * @param[in] in Char buffer where inbound header data is written.
+ * @param[in] size Multiply by nmemb to get the length of ptr.
+ * @param[in] nmemb Multiply by size to get the length of ptr.
+ * @param[in] userdata rlm_rest_response_t to keep parsing state between calls.
+ * @return Length of data processed, or 0 on error.
+ */
+static size_t rest_response_header(void *in, size_t size, size_t nmemb, void *userdata)
+{
+ rlm_rest_response_t *ctx = userdata;
+ REQUEST *request = ctx->request; /* Used by RDEBUG */
+
+ char const *p = in, *q;
+
+ size_t const t = (size * nmemb);
+ size_t s = t;
+ size_t len;
+
+ http_body_type_t type;
+
+ /*
+ * This seems to be curl's indication there are no more header lines.
+ */
+ if (t == 2 && ((p[0] == '\r') && (p[1] == '\n'))) {
+ /*
+ * If we got a 100 Continue, we need to send additional payload data.
+ * reset the state to WRITE_STATE_INIT, so that when were called again
+ * we overwrite previous header data with that from the proper header.
+ */
+ if (ctx->code == 100) {
+ RDEBUG2("Continuing...");
+ ctx->state = WRITE_STATE_INIT;
+ }
+
+ return t;
+ }
+
+ switch (ctx->state) {
+ case WRITE_STATE_INIT:
+ RDEBUG2("Processing response header");
+
+ /*
+ * HTTP/<version> <reason_code>[ <reason_phrase>]\r\n
+ *
+ * "HTTP/1.1 " (8) + "100 " (4) + "\r\n" (2) = 14
+ * "HTTP/2 " (8) + "100 " (4) + "\r\n" (2) = 12
+ */
+ if (s < 12) {
+ REDEBUG("Malformed HTTP header: Status line too short");
+ goto malformed;
+ }
+ /*
+ * Check start of header matches...
+ */
+ if (strncasecmp("HTTP/", p, 5) != 0) {
+ REDEBUG("Malformed HTTP header: Missing HTTP version");
+ goto malformed;
+ }
+ p += 5;
+ s -= 5;
+
+ /*
+ * Skip the version field, next space should mark start of reason_code.
+ */
+ q = memchr(p, ' ', s);
+ if (!q) {
+ RDEBUG("Malformed HTTP header: Missing reason code");
+ goto malformed;
+ }
+
+ s -= (q - p);
+ p = q;
+
+ /*
+ * Process reason_code.
+ *
+ * " 100" (4) + "\r\n" (2) = 6
+ */
+ if (s < 6) {
+ REDEBUG("Malformed HTTP header: Reason code too short");
+ goto malformed;
+ }
+ p++;
+ s--;
+
+ /*
+ * "xxx( |\r)" status code and terminator.
+ */
+ if (!isdigit(p[0]) || !isdigit(p[1]) || !isdigit(p[2]) || !((p[3] == ' ') || (p[3] == '\r'))) goto malformed;
+
+ ctx->code = atoi(p);
+
+ /*
+ * Process reason_phrase (if present).
+ */
+ RINDENT();
+ if (p[3] == ' ') {
+ p += 4;
+ s -= 4;
+
+ q = memchr(p, '\r', s);
+ if (!q) goto malformed;
+
+ len = (q - p);
+
+ RDEBUG2("Status : %i (%.*s)", ctx->code, (int) len, p);
+ } else {
+ RDEBUG2("Status : %i", ctx->code);
+ }
+ REXDENT();
+
+ ctx->state = WRITE_STATE_PARSE_HEADERS;
+
+ break;
+
+ case WRITE_STATE_PARSE_HEADERS:
+ if ((s >= 14) &&
+ (strncasecmp("Content-Type: ", p, 14) == 0)) {
+ p += 14;
+ s -= 14;
+
+ /*
+ * Check to see if there's a parameter separator.
+ */
+ q = memchr(p, ';', s);
+
+ /*
+ * If there's not, find the end of this header.
+ */
+ if (!q) q = memchr(p, '\r', s);
+
+ len = !q ? s : (size_t) (q - p);
+ type = fr_substr2int(http_content_type_table, p, HTTP_BODY_UNKNOWN, len);
+
+ RINDENT();
+ RDEBUG2("Type : %s (%.*s)", fr_int2str(http_body_type_table, type, "<INVALID>"),
+ (int) len, p);
+ REXDENT();
+
+ /*
+ * Assume the force_to value has already been validated.
+ */
+ if (ctx->force_to != HTTP_BODY_UNKNOWN) {
+ if (ctx->force_to != ctx->type) {
+ RDEBUG3("Forcing body type to \"%s\"",
+ fr_int2str(http_body_type_table, ctx->force_to, "<INVALID>"));
+ ctx->type = ctx->force_to;
+ }
+ /*
+ * Figure out if the type is supported by one of the decoders.
+ */
+ } else {
+ ctx->type = http_body_type_supported[type];
+ switch (ctx->type) {
+ case HTTP_BODY_UNKNOWN:
+ RWDEBUG("Couldn't determine type, using the request's type \"%s\".",
+ fr_int2str(http_body_type_table, type, "<INVALID>"));
+ break;
+
+ case HTTP_BODY_UNSUPPORTED:
+ REDEBUG("Type \"%s\" is currently unsupported",
+ fr_int2str(http_body_type_table, type, "<INVALID>"));
+ break;
+
+ case HTTP_BODY_UNAVAILABLE:
+ REDEBUG("Type \"%s\" is unavailable, please rebuild this module with the required "
+ "library", fr_int2str(http_body_type_table, type, "<INVALID>"));
+ break;
+
+ case HTTP_BODY_INVALID:
+ REDEBUG("Type \"%s\" is not a valid web API data markup format",
+ fr_int2str(http_body_type_table, type, "<INVALID>"));
+ break;
+
+ /* supported type */
+ default:
+ break;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return t;
+
+malformed:
+ {
+ char escaped[1024];
+
+ fr_prints(escaped, sizeof(escaped), (char *) in, t, '\0');
+
+ REDEBUG("Received %zu bytes of response data: %s", t, escaped);
+ ctx->code = -1;
+ }
+
+ return (t - s);
+}
+
+/** Processes incoming HTTP body data from libcurl.
+ *
+ * Writes incoming body data to an intermediary buffer for later parsing by
+ * one of the decode functions.
+ *
+ * @param[in] ptr Char buffer where inbound header data is written
+ * @param[in] size Multiply by nmemb to get the length of ptr.
+ * @param[in] nmemb Multiply by size to get the length of ptr.
+ * @param[in] userdata rlm_rest_response_t to keep parsing state between calls.
+ * @return length of data processed, or 0 on error.
+ */
+static size_t rest_response_body(void *ptr, size_t size, size_t nmemb, void *userdata)
+{
+ rlm_rest_response_t *ctx = userdata;
+ REQUEST *request = ctx->request; /* Used by RDEBUG */
+
+ char const *p = ptr, *q;
+ char *tmp;
+
+ size_t const t = (size * nmemb);
+ size_t needed;
+
+ if (t == 0) return 0;
+
+ /*
+ * Any post processing of headers should go here...
+ */
+ if (ctx->state == WRITE_STATE_PARSE_HEADERS) {
+ ctx->state = WRITE_STATE_PARSE_CONTENT;
+ }
+
+ switch (ctx->type) {
+ case HTTP_BODY_UNSUPPORTED:
+ case HTTP_BODY_UNAVAILABLE:
+ case HTTP_BODY_INVALID:
+ while ((q = memchr(p, '\n', t - (p - (char *)ptr)))) {
+ REDEBUG("%.*s", (int) (q - p), p);
+ p = q + 1;
+ }
+
+ if (*p != '\0') {
+ REDEBUG("%.*s", (int)(t - (p - (char *)ptr)), p);
+ }
+
+ return t;
+
+ case HTTP_BODY_NONE:
+ while ((q = memchr(p, '\n', t - (p - (char *)ptr)))) {
+ RDEBUG3("%.*s", (int) (q - p), p);
+ p = q + 1;
+ }
+
+ if (*p != '\0') {
+ RDEBUG3("%.*s", (int)(t - (p - (char *)ptr)), p);
+ }
+
+ return t;
+
+ default:
+ needed = ctx->used + t + 1;
+ if (needed < REST_BODY_INIT) needed = REST_BODY_INIT;
+
+ if (needed > ctx->alloc) {
+ ctx->alloc = needed;
+
+ tmp = ctx->buffer;
+
+ ctx->buffer = rad_malloc(ctx->alloc);
+
+ /* If data has been written previously */
+ if (tmp) {
+ memcpy(ctx->buffer, tmp, ctx->used);
+ free(tmp);
+ }
+ }
+ strlcpy(ctx->buffer + ctx->used, p, t + 1);
+ ctx->used += t; /* don't include the trailing zero */
+
+ break;
+ }
+
+ return t;
+}
+
+/** Print out the response text as error lines
+ *
+ * @param request The Current request.
+ * @param handle rlm_rest_handle_t used to execute the previous request.
+ */
+void rest_response_error(REQUEST *request, rlm_rest_handle_t *handle)
+{
+ char const *p, *q;
+ size_t len;
+
+ len = rest_get_handle_data(&p, handle);
+ if (len == 0) {
+ RERROR("Server returned no data");
+ return;
+ }
+
+ RERROR("Server returned:");
+ while ((q = strchr(p, '\n'))) {
+ RERROR("%.*s", (int) (q - p), p);
+ p = q + 1;
+ }
+ if (*p != '\0') RERROR("%s", p);
+}
+
+/** (Re-)Initialises the data in a rlm_rest_response_t.
+ *
+ * This resets the values of the a rlm_rest_response_t to their defaults.
+ * Must be called between encoding sessions.
+ *
+ * @see rest_response_body
+ * @see rest_response_header
+ *
+ * @param[in] request Current request.
+ * @param[in] ctx data to initialise.
+ * @param[in] type Default http_body_type to use when decoding raw data, may be
+ * overwritten by rest_response_header.
+ */
+static void rest_response_init(REQUEST *request, rlm_rest_response_t *ctx, http_body_type_t type)
+{
+ ctx->request = request;
+ ctx->type = type;
+ ctx->state = WRITE_STATE_INIT;
+ ctx->alloc = 0;
+ ctx->used = 0;
+ ctx->buffer = NULL;
+}
+
+/** Extracts pointer to buffer containing response data
+ *
+ * @param[out] out Where to write the pointer to the buffer.
+ * @param[in] handle used for the last request.
+ * @return > 0 if data is available.
+ */
+size_t rest_get_handle_data(char const **out, rlm_rest_handle_t *handle)
+{
+ rlm_rest_curl_context_t *ctx = handle->ctx;
+
+ rad_assert(ctx->response.buffer || (!ctx->response.buffer && !ctx->response.used));
+
+ *out = ctx->response.buffer;
+ return ctx->response.used;
+}
+
+/** Configures body specific curlopts.
+ *
+ * Configures libcurl handle to use either chunked mode, where the request
+ * data will be sent using multiple HTTP requests, or contiguous mode where
+ * the request data will be sent in a single HTTP request.
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] request Current request.
+ * @param[in] handle rlm_rest_handle_t to configure.
+ * @param[in] func to pass to libcurl for chunked.
+ * transfers (NULL if not using chunked mode).
+ * @return 0 on success -1 on error.
+ */
+static int rest_request_config_body(UNUSED rlm_rest_t *instance, rlm_rest_section_t *section,
+ REQUEST *request, rlm_rest_handle_t *handle, rest_read_t func)
+{
+ rlm_rest_curl_context_t *ctx = handle->ctx;
+ CURL *candle = handle->handle;
+
+ CURLcode ret = CURLE_OK;
+ char const *option = "unknown";
+
+ ssize_t len;
+
+ /*
+ * We were provided with no read function, assume this means
+ * no body should be sent.
+ */
+ if (!func) {
+ SET_OPTION(CURLOPT_POSTFIELDSIZE, 0);
+ return 0;
+ }
+
+ /*
+ * Chunked transfer encoding means the body will be sent in
+ * multiple parts.
+ */
+ if (section->chunk > 0) {
+ SET_OPTION(CURLOPT_READDATA, &ctx->request);
+ SET_OPTION(CURLOPT_READFUNCTION, func);
+
+ return 0;
+ }
+
+ /*
+ * If were not doing chunked encoding then we read the entire
+ * body into a buffer, and send it in one go.
+ */
+ len = rest_request_encode_wrapper(&ctx->body, func, REST_BODY_MAX_LEN, &ctx->request);
+ if (len <= 0) {
+ REDEBUG("Failed creating HTTP body content");
+ return -1;
+ }
+
+ SET_OPTION(CURLOPT_POSTFIELDS, ctx->body);
+ SET_OPTION(CURLOPT_POSTFIELDSIZE, len);
+
+ return 0;
+
+error:
+ REDEBUG("Failed setting curl option %s: %s (%i)", option, curl_easy_strerror(ret), ret);
+
+ return -1;
+}
+
+/** Configures request curlopts.
+ *
+ * Configures libcurl handle setting various curlopts for things like local
+ * client time, Content-Type, and other FreeRADIUS custom headers.
+ *
+ * Current FreeRADIUS custom headers are:
+ * - X-FreeRADIUS-Section The module section being processed.
+ * - X-FreeRADIUS-Server The current virtual server the REQUEST is
+ * passing through.
+ *
+ * Sets up callbacks for all response processing (buffers and body data).
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] handle to configure.
+ * @param[in] request Current request.
+ * @param[in] method to use (HTTP verbs PUT, POST, DELETE etc...).
+ * @param[in] type Content-Type for request encoding, also sets the default for decoding.
+ * @param[in] username to use for HTTP authentication, may be NULL in which case configured defaults will be used.
+ * @param[in] password to use for HTTP authentication, may be NULL in which case configured defaults will be used.
+ * @param[in] uri buffer containing the expanded URI to send the request to.
+ * @return 0 on success (all opts configured) -1 on error.
+ */
+int rest_request_config(rlm_rest_t *instance, rlm_rest_section_t *section,
+ REQUEST *request, void *handle, http_method_t method,
+ http_body_type_t type,
+ char const *uri, char const *username, char const *password)
+{
+ rlm_rest_handle_t *randle = handle;
+ rlm_rest_curl_context_t *ctx = randle->ctx;
+ CURL *candle = randle->handle;
+
+ http_auth_type_t auth = section->auth;
+
+ CURLcode ret = CURLE_OK;
+ char const *option = "unknown";
+ char const *content_type;
+
+ VALUE_PAIR *header;
+ vp_cursor_t headers;
+
+ char buffer[512];
+
+ rad_assert(candle);
+ rad_assert((!username && !password) || (username && password));
+
+ buffer[(sizeof(buffer) - 1)] = '\0';
+
+ /*
+ * Setup any header options and generic headers.
+ */
+ SET_OPTION(CURLOPT_URL, uri);
+ SET_OPTION(CURLOPT_NOSIGNAL, 1);
+ SET_OPTION(CURLOPT_USERAGENT, "FreeRADIUS " RADIUSD_VERSION_STRING);
+
+ /*
+ * As described in https://curl.se/libcurl/c/CURLOPT_HTTP_VERSION.html,
+ * The libcurl decides which http version should be
+ * used by default accoring by library version.
+ */
+ if (instance->http_negotiation != CURL_HTTP_VERSION_NONE) {
+ RDEBUG3("Set HTTP negotiation for %s", instance->http_negotiation_str);
+ SET_OPTION(CURLOPT_HTTP_VERSION, instance->http_negotiation);
+ }
+
+ content_type = fr_int2str(http_content_type_table, type, section->body_str);
+ snprintf(buffer, sizeof(buffer), "Content-Type: %s", content_type);
+ ctx->headers = curl_slist_append(ctx->headers, buffer);
+ if (!ctx->headers) goto error_header;
+
+ // Pass configuration to the request
+ ctx->request.section = section;
+
+ SET_OPTION(CURLOPT_CONNECTTIMEOUT_MS, instance->connect_timeout);
+ SET_OPTION(CURLOPT_TIMEOUT_MS, section->timeout);
+
+#if CURL_AT_LEAST_VERSION(7,85,0)
+ SET_OPTION(CURLOPT_PROTOCOLS_STR, "http,https");
+#else
+# ifdef CURLOPT_PROTOCOLS
+ SET_OPTION(CURLOPT_PROTOCOLS, (CURLPROTO_HTTP | CURLPROTO_HTTPS));
+# endif
+#endif
+
+ /*
+ * FreeRADIUS custom headers
+ */
+ RDEBUG3("Adding custom headers:");
+ RINDENT();
+ snprintf(buffer, sizeof(buffer), "X-FreeRADIUS-Section: %s", section->name);
+ RDEBUG3("%s", buffer);
+ ctx->headers = curl_slist_append(ctx->headers, buffer);
+ if (!ctx->headers) goto error_header;
+
+ snprintf(buffer, sizeof(buffer), "X-FreeRADIUS-Server: %s", request->server);
+ RDEBUG3("%s", buffer);
+ ctx->headers = curl_slist_append(ctx->headers, buffer);
+ if (!ctx->headers) goto error_header;
+
+ fr_cursor_init(&headers, &request->config);
+ while (fr_cursor_next_by_num(&headers, PW_REST_HTTP_HEADER, 0, TAG_ANY)) {
+ header = fr_cursor_remove(&headers);
+ if (!strchr(header->vp_strvalue, ':')) {
+ RWDEBUG("Invalid HTTP header \"%s\" must be in format '<attribute>: <value>'. Skipping...",
+ header->vp_strvalue);
+ talloc_free(header);
+ continue;
+ }
+ RDEBUG3("%s", header->vp_strvalue);
+ ctx->headers = curl_slist_append(ctx->headers, header->vp_strvalue);
+ talloc_free(header);
+ }
+ REXDENT();
+
+ /*
+ * Configure HTTP verb (GET, POST, PUT, PATCH, DELETE, other...)
+ */
+ switch (method) {
+ case HTTP_METHOD_GET:
+ SET_OPTION(CURLOPT_HTTPGET, 1L);
+ break;
+
+ case HTTP_METHOD_POST:
+ SET_OPTION(CURLOPT_POST, 1L);
+ break;
+
+ case HTTP_METHOD_PUT:
+ /*
+ * Do not set CURLOPT_PUT, this will cause libcurl
+ * to ignore CURLOPT_POSTFIELDs and attempt to read
+ * whatever was set with CURLOPT_READDATA, which by
+ * default is stdin.
+ *
+ * This is many cases will cause the server to block,
+ * indefinitely.
+ */
+ SET_OPTION(CURLOPT_CUSTOMREQUEST, "PUT");
+ break;
+
+ case HTTP_METHOD_PATCH:
+ SET_OPTION(CURLOPT_CUSTOMREQUEST, "PATCH");
+ break;
+
+ case HTTP_METHOD_DELETE:
+ SET_OPTION(CURLOPT_CUSTOMREQUEST, "DELETE");
+ break;
+
+ case HTTP_METHOD_CUSTOM:
+ SET_OPTION(CURLOPT_CUSTOMREQUEST, section->method_str);
+ break;
+
+ default:
+ rad_assert(0);
+ break;
+ };
+
+ /*
+ * Set user based authentication parameters
+ */
+ if (auth) {
+ if ((auth >= HTTP_AUTH_BASIC) &&
+ (auth <= HTTP_AUTH_ANY_SAFE)) {
+ SET_OPTION(CURLOPT_HTTPAUTH, http_curl_auth[auth]);
+
+ if (username) {
+ SET_OPTION(CURLOPT_USERNAME, username);
+ } else if (section->username) {
+ if (radius_xlat(buffer, sizeof(buffer), request, section->username, NULL, NULL) < 0) {
+ option = STRINGIFY(CURLOPT_USERNAME);
+ goto error;
+ }
+ SET_OPTION(CURLOPT_USERNAME, buffer);
+ }
+
+ if (password) {
+ SET_OPTION(CURLOPT_PASSWORD, password);
+ } else if (section->password) {
+ if (radius_xlat(buffer, sizeof(buffer), request, section->password, NULL, NULL) < 0) {
+ option = STRINGIFY(CURLOPT_PASSWORD);
+ goto error;
+ }
+ SET_OPTION(CURLOPT_PASSWORD, buffer);
+ }
+#ifdef CURLOPT_TLSAUTH_USERNAME
+ } else if (auth == HTTP_AUTH_TLS_SRP) {
+ SET_OPTION(CURLOPT_TLSAUTH_TYPE, http_curl_auth[auth]);
+
+ if (username) {
+ SET_OPTION(CURLOPT_TLSAUTH_USERNAME, username);
+ } else if (section->username) {
+ if (radius_xlat(buffer, sizeof(buffer), request, section->username, NULL, NULL) < 0) {
+ option = STRINGIFY(CURLOPT_TLSAUTH_USERNAME);
+ goto error;
+ }
+ SET_OPTION(CURLOPT_TLSAUTH_USERNAME, buffer);
+ }
+
+ if (password) {
+ SET_OPTION(CURLOPT_TLSAUTH_PASSWORD, password);
+ } else if (section->password) {
+ if (radius_xlat(buffer, sizeof(buffer), request, section->password, NULL, NULL) < 0) {
+ option = STRINGIFY(CURLOPT_TLSAUTH_PASSWORD);
+ goto error;
+ }
+ SET_OPTION(CURLOPT_TLSAUTH_PASSWORD, buffer);
+ }
+#endif
+ }
+ }
+
+ /*
+ * Set SSL/TLS authentication parameters
+ */
+ if (section->tls_certificate_file) {
+ SET_OPTION(CURLOPT_SSLCERT, section->tls_certificate_file);
+ }
+
+ if (section->tls_private_key_file) {
+ SET_OPTION(CURLOPT_SSLKEY, section->tls_private_key_file);
+ }
+
+ if (section->tls_private_key_password) {
+ SET_OPTION(CURLOPT_KEYPASSWD, section->tls_private_key_password);
+ }
+
+ if (section->tls_ca_file) {
+ SET_OPTION(CURLOPT_ISSUERCERT, section->tls_ca_file);
+ }
+
+ if (section->tls_ca_info_file) {
+ SET_OPTION(CURLOPT_CAINFO, section->tls_ca_info_file);
+ }
+
+ if (section->tls_ca_path) {
+ SET_OPTION(CURLOPT_CAPATH, section->tls_ca_path);
+ }
+
+#if !CURL_AT_LEAST_VERSION(7,84,0)
+ if (section->tls_random_file) {
+ SET_OPTION(CURLOPT_RANDOM_FILE, section->tls_random_file);
+ }
+#endif
+
+ SET_OPTION(CURLOPT_SSL_VERIFYPEER, (section->tls_check_cert == true) ? 1 : 0);
+ SET_OPTION(CURLOPT_SSL_VERIFYHOST, (section->tls_check_cert_cn == true) ? 2 : 0);
+
+ /*
+ * Tell CURL how to get HTTP body content, and how to process incoming data.
+ */
+ rest_response_init(request, &ctx->response, type);
+
+ SET_OPTION(CURLOPT_HEADERFUNCTION, rest_response_header);
+ SET_OPTION(CURLOPT_HEADERDATA, &ctx->response);
+ SET_OPTION(CURLOPT_WRITEFUNCTION, rest_response_body);
+ SET_OPTION(CURLOPT_WRITEDATA, &ctx->response);
+
+ /*
+ * Force parsing the body text as a particular encoding.
+ */
+ ctx->response.force_to = section->force_to;
+
+ switch (method) {
+ case HTTP_METHOD_GET:
+ case HTTP_METHOD_DELETE:
+ RDEBUG3("Using a HTTP method which does not require a body. Forcing request body type to \"none\"");
+ goto finish;
+
+ case HTTP_METHOD_POST:
+ case HTTP_METHOD_PUT:
+ case HTTP_METHOD_PATCH:
+ case HTTP_METHOD_CUSTOM:
+ if (section->chunk > 0) {
+ ctx->request.chunk = section->chunk;
+
+ ctx->headers = curl_slist_append(ctx->headers, "Expect:");
+ if (!ctx->headers) goto error_header;
+
+ ctx->headers = curl_slist_append(ctx->headers, "Transfer-Encoding: chunked");
+ if (!ctx->headers) goto error_header;
+ }
+
+ RDEBUG3("Request body content-type will be \"%s\"",
+ fr_int2str(http_content_type_table, type, section->body_str));
+ break;
+
+ default:
+ rad_assert(0);
+ };
+
+ /*
+ * Setup encoder specific options
+ */
+ switch (type) {
+ case HTTP_BODY_NONE:
+ if (rest_request_config_body(instance, section, request, handle,
+ NULL) < 0) {
+ return -1;
+ }
+
+ break;
+
+ case HTTP_BODY_CUSTOM_XLAT:
+ {
+ rest_custom_data_t *data;
+ char *expanded = NULL;
+
+ if (radius_axlat(&expanded, request, section->data, NULL, NULL) < 0) {
+ return -1;
+ }
+
+ data = talloc_zero(request, rest_custom_data_t);
+ data->p = expanded;
+
+ /* Use the encoder specific pointer to store the data we need to encode */
+ ctx->request.encoder = data;
+ if (rest_request_config_body(instance, section, request, handle,
+ rest_encode_custom) < 0) {
+ TALLOC_FREE(ctx->request.encoder);
+ return -1;
+ }
+
+ break;
+ }
+
+ case HTTP_BODY_CUSTOM_LITERAL:
+ {
+ rest_custom_data_t *data;
+
+ data = talloc_zero(request, rest_custom_data_t);
+ data->p = section->data;
+
+ /* Use the encoder specific pointer to store the data we need to encode */
+ ctx->request.encoder = data;
+ if (rest_request_config_body(instance, section, request, handle,
+ rest_encode_custom) < 0) {
+ TALLOC_FREE(ctx->request.encoder);
+ return -1;
+ }
+ }
+ break;
+
+#ifdef HAVE_JSON
+ case HTTP_BODY_JSON:
+ rest_request_init(request, &ctx->request, true);
+
+ if (rest_request_config_body(instance, section, request, handle,
+ rest_encode_json) < 0) {
+ return -1;
+ }
+
+ break;
+#endif
+
+ case HTTP_BODY_POST:
+ rest_request_init(request, &ctx->request, false);
+
+ if (rest_request_config_body(instance, section, request, handle,
+ rest_encode_post) < 0) {
+ return -1;
+ }
+
+ break;
+
+ default:
+ rad_assert(0);
+ }
+
+
+finish:
+ SET_OPTION(CURLOPT_HTTPHEADER, ctx->headers);
+
+ return 0;
+
+error:
+ REDEBUG("Failed setting curl option %s: %s (%i)", option, curl_easy_strerror(ret), ret);
+ return -1;
+
+error_header:
+ REDEBUG("Failed creating header");
+ REXDENT();
+ return -1;
+}
+
+/** Sends a REST (HTTP) request.
+ *
+ * Send the actual REST request to the server. The response will be handled by
+ * the numerous callbacks configured in rest_request_config.
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] request Current request.
+ * @param[in] handle to use.
+ * @return 0 on success or -1 on error.
+ */
+int rest_request_perform(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t *section,
+ REQUEST *request, void *handle)
+{
+ rlm_rest_handle_t *randle = handle;
+ CURL *candle = randle->handle;
+ CURLcode ret;
+ VALUE_PAIR *vp;
+
+ ret = curl_easy_perform(candle);
+ if (ret != CURLE_OK) {
+ REDEBUG("Request failed: %i - %s", ret, curl_easy_strerror(ret));
+
+ return -1;
+ }
+
+ /*
+ * Save the HTTP return status code.
+ */
+ vp = pair_make_reply("REST-HTTP-Status-Code", NULL, T_OP_SET);
+ vp->vp_integer = rest_get_handle_code(handle);
+
+ RDEBUG2("Adding reply:REST-HTTP-Status-Code = \"%d\"", vp->vp_integer);
+
+ return 0;
+}
+
+/** Sends the response to the correct decode function.
+ *
+ * Uses the Content-Type information written in rest_response_header to
+ * determine the correct decode function to use. The decode function will
+ * then convert the raw received data into VALUE_PAIRs.
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] request Current request.
+ * @param[in] handle to use.
+ * @return 0 on success or -1 on error.
+ */
+int rest_response_decode(rlm_rest_t *instance, rlm_rest_section_t *section, REQUEST *request, void *handle)
+{
+ rlm_rest_handle_t *randle = handle;
+ rlm_rest_curl_context_t *ctx = randle->ctx;
+
+ int ret = -1; /* -Wsometimes-uninitialized */
+
+ if (!ctx->response.buffer) {
+ RDEBUG2("Skipping attribute processing, no valid body data received");
+ return 0;
+ }
+
+ switch (ctx->response.type) {
+ case HTTP_BODY_NONE:
+ return 0;
+
+ case HTTP_BODY_PLAIN:
+ ret = rest_decode_plain(instance, section, request, handle, ctx->response.buffer, ctx->response.used);
+ break;
+
+ case HTTP_BODY_POST:
+ ret = rest_decode_post(instance, section, request, handle, ctx->response.buffer, ctx->response.used);
+ break;
+
+#ifdef HAVE_JSON
+ case HTTP_BODY_JSON:
+ ret = rest_decode_json(instance, section, request, handle, ctx->response.buffer, ctx->response.used);
+ break;
+#endif
+
+ case HTTP_BODY_UNSUPPORTED:
+ case HTTP_BODY_UNAVAILABLE:
+ case HTTP_BODY_INVALID:
+ return -1;
+
+ default:
+ rad_assert(0);
+ }
+
+ return ret;
+}
+
+/** Cleans up after a REST request.
+ *
+ * Resets all options associated with a CURL handle, and frees any headers
+ * associated with it.
+ *
+ * Calls rest_read_ctx_free and rest_response_free to free any memory used by
+ * context data.
+ *
+ * @param[in] instance configuration data.
+ * @param[in] section configuration data.
+ * @param[in] handle to cleanup.
+ */
+void rest_request_cleanup(UNUSED rlm_rest_t *instance, UNUSED rlm_rest_section_t *section, void *handle)
+{
+ rlm_rest_handle_t *randle = handle;
+ rlm_rest_curl_context_t *ctx = randle->ctx;
+ CURL *candle = randle->handle;
+
+ /*
+ * Clear any previously configured options
+ */
+ curl_easy_reset(candle);
+
+ /*
+ * Free header list
+ */
+ if (ctx->headers != NULL) {
+ curl_slist_free_all(ctx->headers);
+ ctx->headers = NULL;
+ }
+
+ /*
+ * Free body data (only used if chunking is disabled)
+ */
+ if (ctx->body != NULL) {
+ free(ctx->body);
+ ctx->body = NULL;
+ }
+
+ /*
+ * Free response data
+ */
+ if (ctx->response.buffer) {
+ free(ctx->response.buffer);
+ ctx->response.buffer = NULL;
+ }
+
+ TALLOC_FREE(ctx->request.encoder);
+ TALLOC_FREE(ctx->response.decoder);
+}
+
+/** URL encodes a string.
+ *
+ * Encode special chars as per RFC 3986 section 4.
+ *
+ * @param[in] request Current request.
+ * @param[out] out Where to write escaped string.
+ * @param[in] outlen Size of out buffer.
+ * @param[in] raw string to be urlencoded.
+ * @param[in] arg pointer, gives context for escaping.
+ * @return length of data written to out (excluding NULL).
+ */
+size_t rest_uri_escape(UNUSED REQUEST *request, char *out, size_t outlen, char const *raw, UNUSED void *arg)
+{
+ char *escaped;
+
+ escaped = curl_escape(raw, strlen(raw));
+ strlcpy(out, escaped, outlen);
+ curl_free(escaped);
+
+ return strlen(out);
+}
+
+/** Builds URI; performs XLAT expansions and encoding.
+ *
+ * Splits the URI into "http://example.org" and "/%{xlat}/query/?bar=foo"
+ * Both components are expanded, but values expanded for the second component
+ * are also url encoded.
+ *
+ * @param[out] out Where to write the pointer to the new buffer containing the escaped URI.
+ * @param[in] instance configuration data.
+ * @param[in] uri configuration data.
+ * @param[in] request Current request
+ * @return length of data written to buffer (excluding NULL) or < 0 if an error
+ * occurred.
+ */
+ssize_t rest_uri_build(char **out, UNUSED rlm_rest_t *instance, REQUEST *request, char const *uri)
+{
+ char const *p;
+ char *path_exp = NULL;
+
+ char *scheme;
+ char const *path;
+
+ ssize_t len;
+
+ p = uri;
+
+ /*
+ * All URLs must contain at least <scheme>://<server>/
+ */
+ p = strchr(p, ':');
+ if (!p || (*++p != '/') || (*++p != '/')) {
+ malformed:
+ REDEBUG("Error URI is malformed, can't find start of path");
+ return -1;
+ }
+ p = strchr(p + 1, '/');
+ if (!p) {
+ goto malformed;
+ }
+
+ len = (p - uri);
+
+ /*
+ * Allocate a temporary buffer to hold the first part of the URI
+ */
+ scheme = talloc_array(request, char, len + 1);
+ strlcpy(scheme, uri, len + 1);
+
+ path = (uri + len);
+
+ len = radius_axlat(out, request, scheme, NULL, NULL);
+ talloc_free(scheme);
+ if (len < 0) {
+ TALLOC_FREE(*out);
+
+ return 0;
+ }
+
+ len = radius_axlat(&path_exp, request, path, rest_uri_escape, NULL);
+ if (len < 0) {
+ TALLOC_FREE(*out);
+
+ return 0;
+ }
+
+ MEM(*out = talloc_strdup_append(*out, path_exp));
+ talloc_free(path_exp);
+
+ return talloc_array_length(*out) - 1; /* array_length includes \0 */
+}
+
+/** Unescapes the host portion of a URI string
+ *
+ * This is required because the xlat functions which operate on the input string
+ * cannot distinguish between host and path components.
+ *
+ * @param[out] out Where to write the pointer to the new buffer containing the escaped URI.
+ * @param[in] instance configuration data.
+ * @param[in] request Current request
+ * @param[in] handle to use.
+ * @param[in] uri configuration data.
+ * @return length of data written to buffer (excluding NULL) or < 0 if an error
+ * occurred.
+ */
+ssize_t rest_uri_host_unescape(char **out, UNUSED rlm_rest_t *instance, REQUEST *request,
+ void *handle, char const *uri)
+{
+ rlm_rest_handle_t *randle = handle;
+ CURL *candle = randle->handle;
+
+ char const *p, *q;
+
+ char *scheme;
+
+ ssize_t len;
+
+ p = uri;
+
+ /*
+ * All URLs must contain at least <scheme>://<server>/
+ */
+ p = strchr(p, ':');
+ if (!p || (*++p != '/') || (*++p != '/')) {
+ malformed:
+ REDEBUG("Error URI is malformed, can't find start of path");
+ return -1;
+ }
+ p = strchr(p + 1, '/');
+ if (!p) {
+ goto malformed;
+ }
+
+ len = (p - uri);
+
+ /*
+ * Unescape any special sequences in the first part of the URI
+ */
+ scheme = curl_easy_unescape(candle, uri, len, NULL);
+ if (!scheme) {
+ REDEBUG("Error unescaping host");
+ return -1;
+ }
+
+ /*
+ * URIs can't contain spaces, so anything after the space must
+ * be something else.
+ */
+ q = strchr(p, ' ');
+ *out = q ? talloc_typed_asprintf(request, "%s%.*s", scheme, (int)(q - p), p) :
+ talloc_typed_asprintf(request, "%s%s", scheme, p);
+
+ MEM(*out);
+ curl_free(scheme);
+
+ return talloc_array_length(*out) - 1; /* array_length includes \0 */
+}