/* * 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 */ RCSID("$Id$") #include #include #include #include #include #include #include #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 '=' @endverbatim and values * will be written to the ptr buffer. * * POST request format is: * @verbatim =&=&=@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 :@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 "":{"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 { "":{ "type":"", "value":[,,] }, "":{ "type":"", "value":[...] }, "":{ "type":"", "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, ""); 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.][:]=&[outer.][:]=&[outer.][:]= @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, "")); 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, "")); 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 { "":{ "do_xlat":, "is_json":, "op":"", "value":[,,] }, "":{ "value":{ "":{ "op":"", "value": } } }, "":"", "":[,,] } @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(¤t, 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. * * "":{ * "do_xlat":, * "is_json":, * "op":"", * "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/ [ ]\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, ""), (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, "")); 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, "")); break; case HTTP_BODY_UNSUPPORTED: REDEBUG("Type \"%s\" is currently unsupported", fr_int2str(http_body_type_table, type, "")); 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, "")); break; case HTTP_BODY_INVALID: REDEBUG("Type \"%s\" is not a valid web API data markup format", fr_int2str(http_body_type_table, type, "")); 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 ': '. 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 :/// */ 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 :/// */ 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 */ }