From: Arran Cudbard-Bell Date: Sat, 20 Jan 2024 02:26:19 +0000 (-0600) Subject: Major rework in rlm_rest X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=25bf94613470be3af3db9e7919cc5a6be66f4a76;p=thirdparty%2Ffreeradius-server.git Major rework in rlm_rest - Remove all synchronous expansions. data, uri, username, password are now passed in as a call_envs - Perform uri escaping within call_env evaluation for module section calls - Split config items into request/response sections, and document which config items can't be used as xlats - Remove legacy uri expansion and escaping - Have test json-api endpoints echo back headers, args, and body data, and fix up xlat tests to check what we sent over - Start of response header parsing and output - Support taking body data, and headers, from ANY tmpl type not just xlats --- diff --git a/raddb/mods-available/rest b/raddb/mods-available/rest index f0add16ea9b..e96ed00fb6a 100644 --- a/raddb/mods-available/rest +++ b/raddb/mods-available/rest @@ -190,34 +190,67 @@ rest { # For example, if you list `rest` in the `authorize` section of a `virtual server`, # the settings from the `authorize` section here will be used. # - # The following config items may be listed in any of the sections: + # The following sections are supported: + # + # - `authorize { ... }` + # - `authenticate { ... }` + # - `accounting { ... }` + # - `post-auth { ... }` + # - `xlat { ... }` + # + # At the top level of each section, the following config items may be listed: # # [options="header,autowidth"] # |=== - # | Option | Description - # | `uri` | To send the request to. - # | `proxy` | The request via this server, supports `socks/http/https` uri and `:port`. + # | Option | Description + # | `request { ... }` | How to create the HTTP request. + # | `response { ... }` | How to decode the response. + # | `tls` | TLS settings for HTTPS. + # | `timeout` | HTTP request timeout in seconds, defaults to 4.0. + # |=== + # + # In the `request { ... }` subsection, the following config items may be listed: + # + # [options="header,autowidth"] + # |=== + # | Option | Description | Allowed in `xlat { ... }` + # | `uri` | To send the request to. | no + # | `proxy` | The request via this server, supports `socks/http/https` uri and `:port`. | no # May be set to "none" to disable proxying, overriding any environmental # variables set like http_proxy. - # | `method` | HTTP method to use, one of 'get', 'post', 'put', 'patch', + # | `method` | HTTP method to use, one of 'get', 'post', 'put', 'patch', | no # 'delete' or any custom HTTP method. - # | `header` | A custom header in the format '
: '. - # | `body` | The format of the HTTP body sent to the remote server. + # | `header` | A custom header in the format '
: '. | yes + # May be specified multiple times. Will be expanded. + # | `body` | The format of the HTTP body sent to the remote server. | yes # May be 'none', 'post' or 'json', defaults to 'none'. - # | `data` | Send custom freeform data in the HTTP body. `Content-type` + # | `data` | Send custom freeform data in the HTTP body. `Content-type` | yes # may be specified with `body`. Will be expanded. # Values from expansion will not be escaped, this should be - # done using the appropriate `xlat` method e.g. `%urlquote()` - # | `force_to` | Force the response to be decoded with this decoder. - # May be 'plain' (creates reply.REST-HTTP-Body), 'post' or 'json'. - # | `tls` | TLS settings for HTTPS. - # | `auth` | HTTP auth method to use, one of 'none', 'srp', 'basic', + # done using the appropriate `xlat` method e.g. + # `%url.quote()` + # | `auth` | HTTP auth method to use, one of 'none', 'srp', 'basic', | yes # 'digest', 'digest-ie', 'gss-negotiate', 'ntlm', # 'ntlm-winbind', 'any', 'safe'. defaults to _'none'_. - # | `username` | User to authenticate as, will be expanded. - # | `password` | Password to use for authentication, will be expanded. - # | `require_auth` | Require HTTP authentication. - # | `timeout` | HTTP request timeout in seconds, defaults to 4.0. + # | `require_auth` | Require HTTP authentication or fail the request. | yes + # | `username` | User to authenticate as. Will be expanded. | yes + # Defaults to `%{User-Name}` in the `authenticate { ... }` section. + # | `password` | Password to use for authentication. Will be expanded. | yes + # Defaults to `%{User-Password}` in the `authenticate { ... }` section. + # |=== + # + # + # In the `response { ... }` subsection, the following config items may be listed: + # + # [options="header,autowidth"] + # |=== + # | Option | Description + # | `header` | Where to write out HTTP headers included in the response. + # Must resolve to a leaf attribute i.e. &reply.REST-HTTP-Header. + # If unspecified, headers will be discarded. + # Values will be in the format '
: '. + # | `force_to` | Force the response to be decoded with this decoder. + # May be 'plain' (creates reply.REST-HTTP-Body), 'post' or 'json'. # | `max_body_in` | Maximum size of incoming HTTP body, defaults to 16k. # |=== # diff --git a/scripts/ci/openresty/json-api.lua b/scripts/ci/openresty/json-api.lua index 5b6c471b75b..f9bdad81363 100644 --- a/scripts/ci/openresty/json-api.lua +++ b/scripts/ci/openresty/json-api.lua @@ -139,7 +139,26 @@ Api.endpoint('GET', '/user//mac/', Api.endpoint('GET', '/user//reflect/', function(body, keyData) local returnData = {} - returnData["station"] = uriArgs.station + + -- Return two Reply-Message attributes, the first with request headers, the second with arguments + returnData["reply.Reply-Message"] = { + op = ":=", + value = { ngx.encode_base64(cjson.encode(ngx.req.get_headers())), ngx.encode_base64(cjson.encode(uriArgs)) } + } + return ngx.say(cjson.encode(returnData)) + end +) + +-- Simple reflection of a URI argument +Api.endpoint('POST', '/user//reflect/', + function(body, keyData) + local returnData = {} + + -- Return three Reply-Message attributes, the first with request headers, the second with arguments, the third with the request body + returnData["reply.Reply-Message"] = { + op = ":=", + value = { ngx.encode_base64(cjson.encode(ngx.req.get_headers())), ngx.encode_base64(cjson.encode(uriArgs)), ngx.encode_base64(cjson.encode(body)) } + } return ngx.say(cjson.encode(returnData)) end ) diff --git a/src/modules/rlm_rest/rest.c b/src/modules/rlm_rest/rest.c index c78390356a7..6c0e2a62d2c 100644 --- a/src/modules/rlm_rest/rest.c +++ b/src/modules/rlm_rest/rest.c @@ -22,8 +22,6 @@ * * @copyright 2012-2021 Arran Cudbard-Bell (a.cudbardb@freeradius.org) */ - - RCSID("$Id$") #define LOG_PREFIX mctx->inst->name @@ -39,6 +37,8 @@ RCSID("$Id$") #include #include +#include + #include "rest.h" /** Table of encoder/decoder support. @@ -54,8 +54,7 @@ const http_body_type_t http_body_type_supported[REST_HTTP_BODY_NUM_ENTRIES] = { REST_HTTP_BODY_UNSUPPORTED, // REST_HTTP_BODY_UNAVAILABLE REST_HTTP_BODY_UNSUPPORTED, // REST_HTTP_BODY_INVALID REST_HTTP_BODY_NONE, // REST_HTTP_BODY_NONE - REST_HTTP_BODY_CUSTOM_XLAT, // REST_HTTP_BODY_CUSTOM_XLAT - REST_HTTP_BODY_CUSTOM_LITERAL, // REST_HTTP_BODY_CUSTOM_LITERAL + REST_HTTP_BODY_CUSTOM, // REST_HTTP_BODY_CUSTOM REST_HTTP_BODY_POST, // REST_HTTP_BODY_POST #ifdef HAVE_JSON REST_HTTP_BODY_JSON, // REST_HTTP_BODY_JSON @@ -1251,7 +1250,7 @@ static int rest_decode_json(rlm_rest_t const *instance, rlm_rest_section_t const static size_t rest_response_header(void *in, size_t size, size_t nmemb, void *userdata) { rlm_rest_response_t *ctx = userdata; - request_t *request = ctx->request; /* Used by RDEBUG */ + request_t *request = ctx->request; /* Used by RDEBUG */ char const *start = (char *)in, *p = start, *end = p + (size * nmemb); char *q; @@ -1510,9 +1509,9 @@ static size_t rest_response_body(void *in, size_t size, size_t nmemb, void *user { char *out_p; - if ((ctx->section->max_body_in > 0) && ((ctx->used + (end - p)) > ctx->section->max_body_in)) { + if ((ctx->section->response.max_body_in > 0) && ((ctx->used + (end - p)) > ctx->section->response.max_body_in)) { REDEBUG("Incoming data (%zu bytes) exceeds max_body_in (%zu bytes). " - "Forcing body to type 'invalid'", ctx->used + (end - p), ctx->section->max_body_in); + "Forcing body to type 'invalid'", ctx->used + (end - p), ctx->section->response.max_body_in); ctx->type = REST_HTTP_BODY_INVALID; TALLOC_FREE(ctx->buffer); break; @@ -1599,9 +1598,10 @@ void rest_response_debug(request_t *request, fr_curl_io_request_t *handle) * @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. + * @param[in] header Where to write out headers, may be NULL. */ static void rest_response_init(rlm_rest_section_t const *section, - request_t *request, rlm_rest_response_t *ctx, http_body_type_t type) + request_t *request, rlm_rest_response_t *ctx, http_body_type_t type, tmpl_t *header) { ctx->section = section; ctx->request = request; @@ -1610,6 +1610,7 @@ static void rest_response_init(rlm_rest_section_t const *section, ctx->alloc = 0; ctx->used = 0; ctx->code = 0; + ctx->header = header; TALLOC_FREE(ctx->buffer); } @@ -1667,7 +1668,7 @@ static int rest_request_config_body(module_ctx_t const *mctx, rlm_rest_section_t * Chunked transfer encoding means the body will be sent in * multiple parts. */ - if (section->chunk > 0) { + if (section->request.chunk > 0) { FR_CURL_REQUEST_SET_OPTION(CURLOPT_READDATA, &uctx->request); FR_CURL_REQUEST_SET_OPTION(CURLOPT_READFUNCTION, func); @@ -1714,11 +1715,10 @@ error: * @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. + * @param[in] body_data (optional) custom body data. Must persist whilst we're + * writing data out to the socket. Must be a talloced buffer + * which is \0 terminated. * @return * - 0 on success (all opts configured). * - -1 on failure. @@ -1726,14 +1726,15 @@ error: int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *section, request_t *request, fr_curl_io_request_t *randle, http_method_t method, http_body_type_t type, - char const *uri, char const *username, char const *password) + char const *uri, char const *body_data) { rlm_rest_t const *inst = talloc_get_type_abort(mctx->inst->data, rlm_rest_t); + rlm_rest_call_env_t *call_env = talloc_get_type_abort(mctx->env_data, rlm_rest_call_env_t); rlm_rest_curl_context_t *ctx = talloc_get_type_abort(randle->uctx, rlm_rest_curl_context_t); CURL *candle = randle->candle; fr_time_delta_t timeout; - http_auth_type_t auth = section->auth; + http_auth_type_t auth = section->request.auth; CURLcode ret = CURLE_OK; char const *option; @@ -1742,7 +1743,6 @@ int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *sect bool content_type_set = false; fr_assert(candle); - fr_assert((!username && !password) || (username && password)); buffer[(sizeof(buffer) - 1)] = '\0'; @@ -1760,11 +1760,11 @@ int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *sect #else FR_CURL_REQUEST_SET_OPTION(CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); #endif - if (section->proxy) { - if (section->proxy == rest_no_proxy) { + if (section->request.proxy) { + if (section->request.proxy == rest_no_proxy) { FR_CURL_REQUEST_SET_OPTION(CURLOPT_NOPROXY, "*"); } else { - FR_CURL_REQUEST_SET_OPTION(CURLOPT_PROXY, section->proxy); + FR_CURL_REQUEST_SET_OPTION(CURLOPT_PROXY, section->request.proxy); } } FR_CURL_REQUEST_SET_OPTION(CURLOPT_NOSIGNAL, 1L); @@ -1798,13 +1798,16 @@ int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *sect /* * Add in the section headers */ - if (section->headers) { - talloc_foreach(section->headers, header) { - RINDENT(); - RDEBUG3("%pV", fr_box_strvalue_buffer(header)); - REXDENT(); + if (call_env->request.header) { + size_t len = talloc_array_length(call_env->request.header), i; + for (i = 0; i < len; i++) { + fr_value_box_list_foreach(&call_env->request.header[i], header) { + RINDENT(); + RDEBUG3("%pV", header); + REXDENT(); - ctx->headers = curl_slist_append(ctx->headers, header); + ctx->headers = curl_slist_append(ctx->headers, header->vb_strvalue); + } } } @@ -1855,7 +1858,7 @@ int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *sect * already from attributes. */ if (type != REST_HTTP_BODY_NONE) { - char const *content_type = fr_table_str_by_value(http_content_type_table, type, section->body_str); + char const *content_type = fr_table_str_by_value(http_content_type_table, type, section->request.body_str); snprintf(buffer, sizeof(buffer), "Content-Type: %s", content_type); ctx->headers = curl_slist_append(ctx->headers, buffer); if (!ctx->headers) { @@ -1902,7 +1905,7 @@ int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *sect break; case REST_HTTP_METHOD_CUSTOM: - FR_CURL_REQUEST_SET_OPTION(CURLOPT_CUSTOMREQUEST, section->method_str); + FR_CURL_REQUEST_SET_OPTION(CURLOPT_CUSTOMREQUEST, section->request.method_str); break; default: @@ -1914,53 +1917,31 @@ int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *sect * Set user based authentication parameters */ if (auth > REST_HTTP_AUTH_NONE) { - TALLOC_CTX *cred_ctx = NULL; #define SET_AUTH_OPTION(_x, _y)\ do {\ if ((ret = curl_easy_setopt(candle, _x, _y)) != CURLE_OK) {\ option = STRINGIFY(_x);\ REDEBUG("Failed setting curl option %s: %s (%i)", option, curl_easy_strerror(ret), ret); \ - talloc_free(cred_ctx);\ goto error;\ }\ } while (0) - - if (!username || !password) cred_ctx = talloc_init_const("cred_ctx"); - - if (!username) { - char *tmp = NULL; - if (xlat_aeval(cred_ctx, &tmp, request, section->username, NULL, NULL) < 0) { - REDEBUG("Failed expanding username"); - talloc_free(cred_ctx); - goto error; - } - username = tmp; - } - - if (!password) { - char *tmp = NULL; - if (xlat_aeval(cred_ctx, &tmp, request, section->password, NULL, NULL) < 0) { - REDEBUG("Failed expanding password"); - talloc_free(cred_ctx); - goto error; - } - password = tmp; - } - RDEBUG3("Configuring HTTP auth type %s, user \"%pV\", password \"%pV\"", fr_table_str_by_value(http_auth_table, auth, ""), - fr_box_strvalue_buffer(username), fr_box_strvalue_buffer(password)); + call_env->request.username ? call_env->request.username : fr_box_strvalue("(none)"), + call_env->request.password ? call_env->request.password : fr_box_strvalue("(none)")); - if ((auth >= REST_HTTP_AUTH_BASIC) && - (auth <= REST_HTTP_AUTH_ANY_SAFE)) { + /* + * FIXME - We probably want to escape \0s here... + */ + if ((auth >= REST_HTTP_AUTH_BASIC) && (auth <= REST_HTTP_AUTH_ANY_SAFE)) { SET_AUTH_OPTION(CURLOPT_HTTPAUTH, http_curl_auth[auth]); - SET_AUTH_OPTION(CURLOPT_USERNAME, username); - SET_AUTH_OPTION(CURLOPT_PASSWORD, password); + if (call_env->request.username) SET_AUTH_OPTION(CURLOPT_USERNAME, call_env->request.username->vb_strvalue); + if (call_env->request.password) SET_AUTH_OPTION(CURLOPT_PASSWORD, call_env->request.password->vb_strvalue); } else if (auth == REST_HTTP_AUTH_TLS_SRP) { SET_AUTH_OPTION(CURLOPT_TLSAUTH_TYPE, http_curl_auth[auth]); - SET_AUTH_OPTION(CURLOPT_TLSAUTH_USERNAME, username); - SET_AUTH_OPTION(CURLOPT_TLSAUTH_PASSWORD, password); + if (call_env->request.username) SET_AUTH_OPTION(CURLOPT_TLSAUTH_USERNAME, call_env->request.username->vb_strvalue); + if (call_env->request.password) SET_AUTH_OPTION(CURLOPT_TLSAUTH_PASSWORD, call_env->request.password->vb_strvalue); } } @@ -1972,7 +1953,7 @@ do {\ /* * Tell CURL how to get HTTP body content, and how to process incoming data. */ - rest_response_init(section, request, &ctx->response, type); + rest_response_init(section, request, &ctx->response, type, call_env->response.header); FR_CURL_REQUEST_SET_OPTION(CURLOPT_HEADERFUNCTION, rest_response_header); FR_CURL_REQUEST_SET_OPTION(CURLOPT_HEADERDATA, &ctx->response); @@ -1982,7 +1963,7 @@ do {\ /* * Force parsing the body text as a particular encoding. */ - ctx->response.force_to = section->force_to; + ctx->response.force_to = section->response.force_to; switch (method) { case REST_HTTP_METHOD_GET: @@ -1994,8 +1975,8 @@ do {\ case REST_HTTP_METHOD_PUT: case REST_HTTP_METHOD_PATCH: case REST_HTTP_METHOD_CUSTOM: - if (section->chunk > 0) { - ctx->request.chunk = section->chunk; + if (section->request.chunk > 0) { + ctx->request.chunk = section->request.chunk; ctx->headers = curl_slist_append(ctx->headers, "Expect:"); if (!ctx->headers) goto error_header; @@ -2019,36 +2000,13 @@ do {\ break; - case REST_HTTP_BODY_CUSTOM_XLAT: + case REST_HTTP_BODY_CUSTOM: { rest_custom_data_t *data; - char *expanded = NULL; - - if (xlat_aeval(request, &expanded, request, section->data, NULL, NULL) < 0) return -1; data = talloc_zero(request, rest_custom_data_t); - data->p = expanded; - data->start = expanded; - data->len = strlen(expanded); // Fix me when we do binary xlat - - /* Use the encoder specific pointer to store the data we need to encode */ - ctx->request.encoder = data; - if (rest_request_config_body(mctx, section, request, randle, rest_encode_custom) < 0) { - TALLOC_FREE(ctx->request.encoder); - return -1; - } - - break; - } - - case REST_HTTP_BODY_CUSTOM_LITERAL: - { - rest_custom_data_t *data; - - data = talloc_zero(request, rest_custom_data_t); - data->p = section->data; - data->start = section->data; - data->len = strlen(section->data); + data->p = data->start = body_data; + data->len = talloc_strlen(body_data); /* Use the encoder specific pointer to store the data we need to encode */ ctx->request.encoder = data; @@ -2175,77 +2133,6 @@ size_t rest_uri_escape(UNUSED request_t *request, char *out, size_t outlen, char 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] inst of rlm_rest. - * @param[in] uri configuration data. - * @param[in] request Current request - * @return - * - Length of data written to buffer (excluding NULL). - * - < 0 if an error occurred. - */ -ssize_t rest_uri_build(char **out, UNUSED rlm_rest_t const *inst, request_t *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 \"%s\" is malformed, can't find start of path", uri); - 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 = xlat_aeval(request, out, request, scheme, NULL, NULL); - talloc_free(scheme); - if (len < 0) { - TALLOC_FREE(*out); - - return 0; - } - - len = xlat_aeval(request, &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 diff --git a/src/modules/rlm_rest/rest.h b/src/modules/rlm_rest/rest.h index 080cad4e254..2e6b27549bf 100644 --- a/src/modules/rlm_rest/rest.h +++ b/src/modules/rlm_rest/rest.h @@ -56,8 +56,7 @@ typedef enum { REST_HTTP_BODY_UNAVAILABLE, REST_HTTP_BODY_INVALID, REST_HTTP_BODY_NONE, - REST_HTTP_BODY_CUSTOM_XLAT, - REST_HTTP_BODY_CUSTOM_LITERAL, + REST_HTTP_BODY_CUSTOM, REST_HTTP_BODY_POST, REST_HTTP_BODY_JSON, REST_HTTP_BODY_XML, @@ -106,41 +105,44 @@ extern size_t http_body_type_table_len; extern fr_table_num_sorted_t const http_content_type_table[]; extern size_t http_content_type_table_len; -/* - * Structure for section configuration - */ typedef struct { - char const *name; //!< Section name. - char const *uri; //!< URI to send HTTP request to. + char const *proxy; //!< Send request via this proxy. - char const *proxy; //!< Send request via this proxy. + char const *method_str; //!< The string version of the HTTP method. + http_method_t method; //!< What HTTP method should be used, GET, POST etc... - char const *method_str; //!< The string version of the HTTP method. - http_method_t method; //!< What HTTP method should be used, GET, POST etc... + char const *body_str; //!< The string version of the encoding/content type. + http_body_type_t body; //!< What encoding type should be used. - char const *body_str; //!< The string version of the encoding/content type. - http_body_type_t body; //!< What encoding type should be used. + bool auth_is_set; //!< Whether a value was provided for auth_str. - char const *force_to_str; //!< Force decoding with this decoder. - http_body_type_t force_to; //!< Override the Content-Type header in the response - //!< to force decoding as a particular type. + http_auth_type_t auth; //!< HTTP auth type. - char const **headers; //!< Custom headers to set (optional). - char const *data; //!< Custom body data (optional). + bool require_auth; //!< Whether HTTP-Auth is required or not. - bool auth_is_set; //!< Whether a value was provided for auth_str. + uint32_t chunk; //!< Max chunk-size (mainly for testing the encoders) +} rlm_rest_section_request_t; - http_auth_type_t auth; //!< HTTP auth type. +typedef struct { + char const *force_to_str; //!< Force decoding with this decoder. + http_body_type_t force_to; //!< Override the Content-Type header in the response + //!< to force decoding as a particular type. - bool require_auth; //!< Whether HTTP-Auth is required or not. - char const *username; //!< Username used for HTTP-Auth - char const *password; //!< Password used for HTTP-Auth + size_t max_body_in; //!< Maximum size of incoming data. +} rlm_rest_section_response_t; - fr_time_delta_t timeout; //!< Timeout timeval. - uint32_t chunk; //!< Max chunk-size (mainly for testing the encoders) - size_t max_body_in; //!< Maximum size of incoming data. +/* + * Structure for section configuration + */ +typedef struct { + char const *name; //!< Section name. + + fr_time_delta_t timeout; //!< Timeout timeval. + + rlm_rest_section_request_t request; //!< Request configuration. + rlm_rest_section_response_t response; //!< Response configuration. - fr_curl_tls_t tls; + fr_curl_tls_t tls; } rlm_rest_section_t; /* @@ -238,6 +240,9 @@ typedef struct { http_body_type_t type; //!< HTTP Content Type. http_body_type_t force_to; //!< Force decoding the body type as a particular encoding. + tmpl_t *header; //!< Where to create pairs representing HTTP response headers. + ///< If NULL no headers will be parsed other than content-type. + void *decoder; //!< Decoder specific data. } rlm_rest_response_t; @@ -263,6 +268,20 @@ typedef struct { fr_curl_io_request_t *handle; //!< curl easy handle servicing our request. } rlm_rest_xlat_rctx_t; +typedef struct { + struct { + fr_value_box_t *uri; //!< URI to send HTTP request to. + fr_value_box_list_t *header; //!< Headers to place in the request + fr_value_box_t *data; //!< Custom data to send in requests. + fr_value_box_t *username; //!< Username to use for authentication + fr_value_box_t *password; //!< Password to use for authentication + } request; + + struct { + tmpl_t *header; //!< Where to write response headers + } response; +} rlm_rest_call_env_t; + extern HIDDEN fr_dict_t const *dict_freeradius; extern HIDDEN fr_dict_attr_t const *attr_rest_http_body; @@ -282,11 +301,10 @@ void *rest_mod_conn_create(TALLOC_CTX *ctx, void *instance, fr_time_delta_t time /* * Request processing API */ -int rest_request_config(module_ctx_t const *mctx, - rlm_rest_section_t const *section, request_t *request, - fr_curl_io_request_t *randle, http_method_t method, - http_body_type_t type, char const *uri, - char const *username, char const *password) CC_HINT(nonnull (1,2,4,7)); +int rest_request_config(module_ctx_t const *mctx, rlm_rest_section_t const *section, + request_t *request, fr_curl_io_request_t *randle, http_method_t method, + http_body_type_t type, + char const *uri, char const *body_data) CC_HINT(nonnull (1,2,4,7)); int rest_response_decode(rlm_rest_t const *instance, UNUSED rlm_rest_section_t const *section, request_t *request, @@ -305,7 +323,6 @@ size_t rest_get_handle_data(char const **out, fr_curl_io_request_t *handle); * Helper functions */ size_t rest_uri_escape(UNUSED request_t *request, char *out, size_t outlen, char const *raw, UNUSED void *arg); -ssize_t rest_uri_build(char **out, rlm_rest_t const *instance, request_t *request, char const *uri); ssize_t rest_uri_host_unescape(char **out, UNUSED rlm_rest_t const *mod_inst, request_t *request, fr_curl_io_request_t *randle, char const *uri); diff --git a/src/modules/rlm_rest/rlm_rest.c b/src/modules/rlm_rest/rlm_rest.c index 8fd9ce032ff..1a5ef323135 100644 --- a/src/modules/rlm_rest/rlm_rest.c +++ b/src/modules/rlm_rest/rlm_rest.c @@ -19,48 +19,71 @@ * @file rlm_rest.c * @brief Integrate FreeRADIUS with RESTfull APIs * - * @copyright 2012-2019 Arran Cudbard-Bell (a.cudbardb@freeradius.org) + * @copyright 2012-2019,2024 Arran Cudbard-Bell (a.cudbardb@freeradius.org) */ +#include "lib/server/tmpl_escape.h" +#include RCSID("$Id$") #include #include +#include #include + #include #include +#include #include #include +#include #include #include #include +#include #include -#include #include "rest.h" +static int uri_part_escape(fr_value_box_t *vb, void *uctx); +static void *uri_part_escape_uctx_alloc(UNUSED request_t *request, void const *uctx); +static void uri_part_escape_uctx_free(void *uctx); + +static fr_uri_part_t const rest_uri_parts[] = { + { .name = "scheme", .terminals = &FR_SBUFF_TERMS(L(":")), .part_adv = { [':'] = 1 }, + .tainted_allowed = false, .extra_skip = 2 }, + { .name = "host", .terminals = &FR_SBUFF_TERMS(L(":"), L("/")), .part_adv = { [':'] = 1, ['/'] = 2 }, + .tainted_allowed = true, .func = uri_part_escape }, + { .name = "port", .terminals = &FR_SBUFF_TERMS(L("/")), .part_adv = { ['/'] = 1 }, + .tainted_allowed = false }, + { .name = "method", .terminals = &FR_SBUFF_TERMS(L("?")), .part_adv = { ['?'] = 1 }, + .tainted_allowed = true, .func = uri_part_escape }, + { .name = "param", .tainted_allowed = true, .func = uri_part_escape }, + XLAT_URI_PART_TERMINATOR +}; + static fr_table_num_sorted_t const http_negotiation_table[] = { - { L("1.0"), CURL_HTTP_VERSION_1_0 }, //!< Enforce HTTP 1.0 requests. - { L("1.1"), CURL_HTTP_VERSION_1_1 }, //!< Enforce HTTP 1.1 requests. + { L("1.0"), CURL_HTTP_VERSION_1_0 }, //!< Enforce HTTP 1.0 requests. + { L("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) - { L("2.0"), CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE }, //!< Enforce HTTP 2.0 requests. + { L("2.0"), CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE }, //!< Enforce HTTP 2.0 requests. #endif #if CURL_AT_LEAST_VERSION(7,33,0) - { L("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) + { L("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) - { L("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. + { L("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 { L("default"), CURL_HTTP_VERSION_NONE } //!< We don't care about what version the library uses. - ///< libcurl will use whatever it thinks fit. + ///< libcurl will use whatever it thinks fit. }; static size_t http_negotiation_table_len = NUM_ELEMENTS(http_negotiation_table); @@ -88,48 +111,50 @@ static int rest_proxy_parse(UNUSED TALLOC_CTX *ctx, void *out, UNUSED void *pare return 0; } +#define SECTION_REQUEST_COMMON \ + { FR_CONF_OFFSET("body", rlm_rest_section_request_t, body_str), .dflt = "none" }, \ + /* User authentication */ \ + { FR_CONF_OFFSET_IS_SET("auth", FR_TYPE_VOID, 0, rlm_rest_section_request_t, auth), \ + .func = cf_table_parse_int, .uctx = &(cf_table_parse_ctx_t){ .table = http_auth_table, .len = &http_auth_table_len }, .dflt = "none" }, \ + { FR_CONF_OFFSET("require_auth", rlm_rest_section_request_t, require_auth), .dflt = "no" }, \ + { FR_CONF_OFFSET("chunk", rlm_rest_section_request_t, chunk), .dflt = "0" } \ + +static const conf_parser_t section_request_config[] = { + { FR_CONF_OFFSET("proxy", rlm_rest_section_request_t, proxy), .func = rest_proxy_parse }, + { FR_CONF_OFFSET("method", rlm_rest_section_request_t, method_str), .dflt = "GET" }, + SECTION_REQUEST_COMMON, + CONF_PARSER_TERMINATOR +}; + +static const conf_parser_t section_response_config[] = { + { FR_CONF_OFFSET("force_to", rlm_rest_section_response_t, force_to_str) }, \ + { FR_CONF_OFFSET_TYPE_FLAGS("max_body_in", FR_TYPE_SIZE, 0, rlm_rest_section_response_t, max_body_in), .dflt = "16k" }, + CONF_PARSER_TERMINATOR +}; + static const conf_parser_t section_config[] = { - { FR_CONF_OFFSET_FLAGS("uri", CONF_FLAG_XLAT, rlm_rest_section_t, uri), .dflt = "" }, - { FR_CONF_OFFSET("proxy", rlm_rest_section_t, proxy), .func = rest_proxy_parse }, - { FR_CONF_OFFSET("method", rlm_rest_section_t, method_str), .dflt = "GET" }, - { FR_CONF_OFFSET_FLAGS("header", CONF_FLAG_MULTI, rlm_rest_section_t, headers) }, - { FR_CONF_OFFSET("body", rlm_rest_section_t, body_str), .dflt = "none" }, - { FR_CONF_OFFSET_FLAGS("data", CONF_FLAG_XLAT, rlm_rest_section_t, data) }, - { FR_CONF_OFFSET("force_to", rlm_rest_section_t, force_to_str) }, - - /* User authentication */ - { FR_CONF_OFFSET_IS_SET("auth", FR_TYPE_VOID, 0, rlm_rest_section_t, auth), - .func = cf_table_parse_int, .uctx = &(cf_table_parse_ctx_t){ .table = http_auth_table, .len = &http_auth_table_len }, .dflt = "none" }, - { FR_CONF_OFFSET_FLAGS("username", CONF_FLAG_XLAT, rlm_rest_section_t, username) }, - { FR_CONF_OFFSET_FLAGS("password", CONF_FLAG_SECRET | CONF_FLAG_XLAT, rlm_rest_section_t, password) }, - { FR_CONF_OFFSET("require_auth", rlm_rest_section_t, require_auth), .dflt = "no" }, + { FR_CONF_OFFSET_SUBSECTION("request", 0, rlm_rest_section_t, request, section_request_config) }, + { FR_CONF_OFFSET_SUBSECTION("response", 0, rlm_rest_section_t, response, section_response_config) }, /* Transfer configuration */ { FR_CONF_OFFSET("timeout", rlm_rest_section_t, timeout), .dflt = "4.0" }, - { FR_CONF_OFFSET("chunk", rlm_rest_section_t, chunk), .dflt = "0" }, - { FR_CONF_OFFSET_TYPE_FLAGS("max_body_in", FR_TYPE_SIZE, 0, rlm_rest_section_t, max_body_in), .dflt = "16k" }, /* TLS Parameters */ { FR_CONF_OFFSET_SUBSECTION("tls", 0, rlm_rest_section_t, tls, fr_curl_tls_config) }, CONF_PARSER_TERMINATOR }; -static const conf_parser_t xlat_config[] = { - { FR_CONF_OFFSET("proxy", rlm_rest_section_t, proxy), .func = rest_proxy_parse }, - - /* User authentication */ - { FR_CONF_OFFSET_IS_SET("auth", FR_TYPE_VOID, 0, rlm_rest_section_t, auth), - .func = cf_table_parse_int, .uctx = &(cf_table_parse_ctx_t){ .table = http_auth_table, .len = &http_auth_table_len }, .dflt = "none" }, - - { FR_CONF_OFFSET_FLAGS("header", CONF_FLAG_MULTI, rlm_rest_section_t, headers) }, +static const conf_parser_t xlat_request_config[] = { + SECTION_REQUEST_COMMON, + CONF_PARSER_TERMINATOR +}; - { FR_CONF_OFFSET_FLAGS("username", CONF_FLAG_XLAT, rlm_rest_section_t, username) }, - { FR_CONF_OFFSET_FLAGS("password", CONF_FLAG_SECRET | CONF_FLAG_XLAT, rlm_rest_section_t, password) }, - { FR_CONF_OFFSET("require_auth", rlm_rest_section_t, require_auth), .dflt = "no" }, +static const conf_parser_t xlat_config[] = { + { FR_CONF_OFFSET_SUBSECTION("request", 0, rlm_rest_section_t, request, xlat_request_config) }, + { FR_CONF_OFFSET_SUBSECTION("response", 0, rlm_rest_section_t, response, section_response_config) }, /* Transfer configuration */ { FR_CONF_OFFSET("timeout", rlm_rest_section_t, timeout), .dflt = "4.0" }, - { FR_CONF_OFFSET("chunk", rlm_rest_section_t, chunk), .dflt = "0" }, /* TLS Parameters */ { FR_CONF_OFFSET_SUBSECTION("tls", 0, rlm_rest_section_t, tls, fr_curl_tls_config) }, @@ -158,6 +183,88 @@ static const conf_parser_t module_config[] = { CONF_PARSER_TERMINATOR }; +#define REST_CALL_ENV_REQUEST_COMMON(_dflt_username, _dflt_password) \ + { FR_CALL_ENV_OFFSET("header", FR_TYPE_STRING, CALL_ENV_FLAG_MULTI, rlm_rest_call_env_t, request.header) }, \ + { FR_CALL_ENV_OFFSET("data", FR_TYPE_STRING, CALL_ENV_FLAG_CONCAT, rlm_rest_call_env_t, request.data) }, \ + { FR_CALL_ENV_OFFSET("username", FR_TYPE_STRING, CALL_ENV_FLAG_SINGLE | CALL_ENV_FLAG_NULLABLE, \ + rlm_rest_call_env_t, request.username), .pair.dflt_quote = T_BARE_WORD, _dflt_username }, \ + { FR_CALL_ENV_OFFSET("password", FR_TYPE_STRING, CALL_ENV_FLAG_SINGLE | CALL_ENV_FLAG_NULLABLE | CALL_ENV_FLAG_SECRET, \ + rlm_rest_call_env_t, request.password), .pair.dflt_quote = T_BARE_WORD, _dflt_password }, \ + +#define REST_CALL_ENV_RESPONSE_COMMON \ + { FR_CALL_ENV_PARSE_ONLY_OFFSET("header", FR_TYPE_STRING, CALL_ENV_FLAG_ATTRIBUTE, rlm_rest_call_env_t, response.header) }, \ + +#define REST_CALL_ENV_SECTION(_var, _section, _dflt_username, _dflt_password) \ +static const call_env_method_t _var = { \ + FR_CALL_ENV_METHOD_OUT(rlm_rest_call_env_t), \ + .env = (call_env_parser_t[]){ \ + { FR_CALL_ENV_SUBSECTION(_section, NULL, CALL_ENV_FLAG_NONE, \ + ((call_env_parser_t[]) { \ + { FR_CALL_ENV_SUBSECTION("request", NULL, CALL_ENV_FLAG_NONE, \ + ((call_env_parser_t[]) { \ + { FR_CALL_ENV_OFFSET("uri", FR_TYPE_STRING, CALL_ENV_FLAG_REQUIRED | CALL_ENV_FLAG_CONCAT, rlm_rest_call_env_t, request.uri), \ + .pair.escape = { \ + .func = fr_uri_escape, \ + .mode = TMPL_ESCAPE_PRE_CONCAT, \ + .uctx = { \ + .func = { \ + .alloc = uri_part_escape_uctx_alloc, \ + .free = uri_part_escape_uctx_free, \ + .uctx = rest_uri_parts \ + } , \ + .type = TMPL_ESCAPE_UCTX_ALLOC_FUNC\ + } \ + }}, /* Do not concat */ \ + REST_CALL_ENV_REQUEST_COMMON(_dflt_username, _dflt_password) \ + CALL_ENV_TERMINATOR \ + })) }, \ + { FR_CALL_ENV_SUBSECTION("response", NULL, CALL_ENV_FLAG_NONE, \ + ((call_env_parser_t[]) { \ + REST_CALL_ENV_RESPONSE_COMMON \ + CALL_ENV_TERMINATOR \ + })) }, \ + CALL_ENV_TERMINATOR \ + }) \ + ) }, \ + CALL_ENV_TERMINATOR \ + } \ +} + +REST_CALL_ENV_SECTION(rest_call_env_authorize, "authorize",,); +REST_CALL_ENV_SECTION(rest_call_env_authenticate, "authenticate", .pair.dflt = "&User-Name", .pair.dflt = "&User-Password"); +REST_CALL_ENV_SECTION(rest_call_env_post_auth, "post-auth",,); +REST_CALL_ENV_SECTION(rest_call_env_accounting, "accounting",,); + +/* + * xlat call env doesn't have the same set of config items as the other sections + * because some values come from the xlat call itself. + * + * If someone can figure out a non-fuggly way of omitting the uri from the + * configuration, please do, and use the REST_CALL_ENV_SECTION macro for this + * too. + */ +static const call_env_method_t rest_call_env_xlat = { \ + FR_CALL_ENV_METHOD_OUT(rlm_rest_call_env_t), \ + .env = (call_env_parser_t[]){ \ + { FR_CALL_ENV_SUBSECTION("xlat", NULL, CALL_ENV_FLAG_NONE, \ + ((call_env_parser_t[]) { \ + { FR_CALL_ENV_SUBSECTION("request", NULL, CALL_ENV_FLAG_NONE, \ + ((call_env_parser_t[]) { \ + REST_CALL_ENV_REQUEST_COMMON(,) \ + CALL_ENV_TERMINATOR \ + })) }, \ + { FR_CALL_ENV_SUBSECTION("response", NULL, CALL_ENV_FLAG_NONE, \ + ((call_env_parser_t[]) { \ + REST_CALL_ENV_RESPONSE_COMMON \ + CALL_ENV_TERMINATOR \ + })) }, \ + CALL_ENV_TERMINATOR \ + }) \ + ) }, \ + CALL_ENV_TERMINATOR \ + } \ +}; + fr_dict_t const *dict_freeradius; static fr_dict_t const *dict_radius; @@ -223,34 +330,98 @@ static int rlm_rest_status_update(request_t *request, void *handle) return 0; } -static int rlm_rest_perform(module_ctx_t const *mctx, - rlm_rest_section_t const *section, fr_curl_io_request_t *randle, - request_t *request, char const *username, char const *password) +/** Allocate an escape uctx to pass to fr_uri_escape + * + * @param[in] request UNUSED. + * @param[in] uctx pointer to the start of the uri_parts array. + * @return A new fr_uri_escape_ctx_t. + */ +static void *uri_part_escape_uctx_alloc(UNUSED request_t *request, void const *uctx) { - rlm_rest_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_rest_t); - rlm_rest_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_rest_thread_t); - ssize_t uri_len; - char *uri = NULL; - int ret; + fr_uri_escape_ctx_t *ctx; + + MEM(ctx = talloc_zero(NULL, fr_uri_escape_ctx_t)); + ctx->uri_part = uctx; + + return ctx; +} - RDEBUG2("Expanding URI components"); +static void uri_part_escape_uctx_free(void *uctx) +{ + talloc_free(uctx); +} + +/** Free the curl easy handle + * + * @param[in] arg curl easy handle to free. + */ +static int _uri_part_escape_free_on_exit(void *arg) +{ + curl_easy_cleanup(arg); + return 0; +} + +/** URL escape a single box forming part of a URL + * + * @param[in] vb to escape + * @param[in] uctx UNUSED context containing CURL handle + * @return + * - 0 on success + * - -1 on failure + */ +static int uri_part_escape(fr_value_box_t *vb, UNUSED void *uctx) +{ + static _Thread_local CURL *t_candle; + char *escaped; /* - * Build xlat'd URI, this allows REST servers to be specified by - * request attributes. + * libcurl doesn't actually use the handle, but we pass one + * in anyway, just in case it does in the future. */ - uri_len = rest_uri_build(&uri, inst, request, section->uri); - if (uri_len <= 0) return -1; + if (unlikely(t_candle == NULL)) { + CURL *candle; - RDEBUG2("Sending HTTP %s to \"%s\"", fr_table_str_by_value(http_method_table, section->method, NULL), uri); + MEM(candle = curl_easy_init()); + fr_atexit_thread_local(t_candle, _uri_part_escape_free_on_exit, candle); + } + + escaped = curl_easy_escape(t_candle, vb->vb_strvalue, vb->vb_length); + if (!escaped) return -1; + + /* + * Returned string the same length - nothing changed + */ + if (strlen(escaped) == vb->vb_length) { + curl_free(escaped); + return 0; + } + + fr_value_box_clear_value(vb); + fr_value_box_strdup(vb, vb, NULL, escaped, vb->tainted); + + curl_free(escaped); + + return 0; +} + +static int rlm_rest_perform(module_ctx_t const *mctx, + rlm_rest_section_t const *section, fr_curl_io_request_t *randle, + request_t *request) +{ + rlm_rest_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_rest_thread_t); + rlm_rest_call_env_t *call_env = talloc_get_type_abort(mctx->env_data, rlm_rest_call_env_t); + int ret; + + RDEBUG2("Sending HTTP %s to \"%pV\"", + fr_table_str_by_value(http_method_table, section->request.method, NULL), call_env->request.uri); /* * Configure various CURL options, and initialise the read/write * context data. */ - ret = rest_request_config(mctx, section, request, randle, section->method, section->body, - uri, username, password); - talloc_free(uri); + ret = rest_request_config(mctx, section, request, randle, section->request.method, section->request.body, + call_env->request.uri->vb_strvalue, + call_env->request.data ? call_env->request.data->vb_strvalue : NULL); if (ret < 0) return -1; /* @@ -338,55 +509,6 @@ finish: return xa; } -/** URL escape a single box forming part of a URL - * - * @param[in] vb to escape - * @param[in] uctx context containing CURL handle - * @return - * - 0 on success - * - -1 on failure - */ -static int uri_part_escape(fr_value_box_t *vb, void *uctx) -{ - char *escaped; - fr_curl_io_request_t *randle = talloc_get_type_abort(uctx, fr_curl_io_request_t); - request_t *request = randle->request; - - escaped = curl_easy_escape(randle->candle, vb->vb_strvalue, vb->vb_length); - if (!escaped) return -1; - - /* - * Returned string the same length - nothing changed - */ - if (strlen(escaped) == vb->vb_length) { - RDEBUG4("Tainted value %pV needed no escaping", vb); - curl_free(escaped); - return 0; - } - - RDEBUG4("Tainted value %pV escaped to %s", vb, escaped); - - fr_value_box_clear_value(vb); - fr_value_box_strdup(vb, vb, NULL, escaped, vb->tainted); - - curl_free(escaped); - - return 0; -} - -static fr_uri_part_t const rest_uri_parts[] = { - { .name = "scheme", .terminals = &FR_SBUFF_TERMS(L(":")), .part_adv = { [':'] = 1 }, - .tainted_allowed = false, .extra_skip = 2 }, - { .name = "host", .terminals = &FR_SBUFF_TERMS(L(":"), L("/")), .part_adv = { [':'] = 1, ['/'] = 2 }, - .tainted_allowed = true, .func = uri_part_escape }, - { .name = "port", .terminals = &FR_SBUFF_TERMS(L("/")), .part_adv = { ['/'] = 1 }, - .tainted_allowed = false }, - { .name = "method", .terminals = &FR_SBUFF_TERMS(L("?")), .part_adv = { ['?'] = 1 }, - .tainted_allowed = true, .func = uri_part_escape }, - { .name = "param", .tainted_allowed = true, .func = uri_part_escape }, - XLAT_URI_PART_TERMINATOR -}; - static xlat_arg_parser_t const rest_xlat_args[] = { { .required = true, .type = FR_TYPE_STRING }, { .variadic = XLAT_ARG_VARIADIC_EMPTY_KEEP, .type = FR_TYPE_STRING }, @@ -443,20 +565,20 @@ static xlat_action_t rest_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, method = fr_table_value_by_substr(http_method_table, uri_vb->vb_strvalue, -1, REST_HTTP_METHOD_UNKNOWN); if (method != REST_HTTP_METHOD_UNKNOWN) { - section->method = method; + section->request.method = method; /* * If the method is unknown, it's a custom verb */ } else { - section->method = REST_HTTP_METHOD_CUSTOM; - MEM(section->method_str = talloc_bstrndup(rctx, uri_vb->vb_strvalue, uri_vb->vb_length)); + section->request.method = REST_HTTP_METHOD_CUSTOM; + MEM(section->request.method_str = talloc_bstrndup(rctx, uri_vb->vb_strvalue, uri_vb->vb_length)); } /* * Move to next argument */ in_vb = fr_value_box_list_pop_head(in); } else { - section->method = REST_HTTP_METHOD_GET; + section->request.method = REST_HTTP_METHOD_GET; } /* @@ -476,7 +598,7 @@ static xlat_action_t rest_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, randle->request = request; /* Populate the request pointer for escape callbacks */ - if (fr_uri_escape_list(&in_vb->vb_group, rest_uri_parts, randle) < 0) { + if (fr_uri_escape_list(&in_vb->vb_group, rest_uri_parts, NULL) < 0) { RPEDEBUG("Failed escaping URI"); error: @@ -506,14 +628,13 @@ static xlat_action_t rest_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, REDEBUG("Failed to concatenate freeform data"); goto error; } - section->body = REST_HTTP_BODY_CUSTOM_LITERAL; - section->data = in_vb->vb_strvalue; + section->request.body = REST_HTTP_BODY_CUSTOM; } RDEBUG2("Sending HTTP %s to \"%pV\"", - (section->method == REST_HTTP_METHOD_CUSTOM) ? - section->method_str : fr_table_str_by_value(http_method_table, section->method, NULL), - uri_vb); + (section->request.method == REST_HTTP_METHOD_CUSTOM) ? + section->request.method_str : fr_table_str_by_value(http_method_table, section->request.method, NULL), + uri_vb); /* * Configure various CURL options, and initialise the read/write @@ -521,9 +642,10 @@ static xlat_action_t rest_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, * * @todo We could extract the User-Name and password from the URL string. */ - ret = rest_request_config(MODULE_CTX(dl_module_instance_by_data(inst), t, NULL, NULL), - section, request, randle, section->method, - section->body, uri_vb->vb_strvalue, NULL, NULL); + ret = rest_request_config(MODULE_CTX(dl_module_instance_by_data(inst), t, xctx->env_data, NULL), + section, request, randle, section->request.method, + section->request.body, + uri_vb->vb_strvalue, in_vb ? in_vb->vb_strvalue : NULL); if (ret < 0) goto error; /* @@ -638,7 +760,7 @@ static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, mod handle = rest_slab_reserve(t->slab); if (!handle) RETURN_MODULE_FAIL; - ret = rlm_rest_perform(mctx, section, handle, request, NULL, NULL); + ret = rlm_rest_perform(mctx, section, handle, request); if (ret < 0) { rest_slab_release(handle); @@ -736,29 +858,24 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, { rlm_rest_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_rest_t); rlm_rest_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_rest_thread_t); + rlm_rest_call_env_t *call_env = talloc_get_type_abort(mctx->env_data, rlm_rest_call_env_t); rlm_rest_section_t const *section = &inst->authenticate; fr_curl_io_request_t *handle; int ret; - fr_pair_t const *username; - fr_pair_t const *password; - if (!section->name) RETURN_MODULE_NOOP; - username = fr_pair_find_by_da(&request->request_pairs, NULL, attr_user_name); - password = fr_pair_find_by_da(&request->request_pairs, NULL, attr_user_password); - /* * We can only authenticate user requests which HAVE * a User-Name attribute. */ - if (!username) { + if (!call_env->request.username) { REDEBUG("Attribute \"User-Name\" is required for authentication"); RETURN_MODULE_INVALID; } - if (!password) { + if (!call_env->request.password) { REDEBUG("Attribute \"User-Password\" is required for authentication"); RETURN_MODULE_INVALID; } @@ -766,7 +883,7 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, /* * Make sure the supplied password isn't empty */ - if (password->vp_length == 0) { + if (call_env->request.password->vb_length == 0) { REDEBUG("User-Password must not be empty"); RETURN_MODULE_INVALID; } @@ -775,7 +892,7 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, * Log the password */ if (RDEBUG_ENABLED3) { - RDEBUG("Login attempt with password \"%pV\"", &password->data); + RDEBUG("Login attempt with password \"%pV\"", &call_env->request.password); } else { RDEBUG2("Login attempt with password"); } @@ -783,8 +900,7 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, handle = rest_slab_reserve(t->slab); if (!handle) RETURN_MODULE_FAIL; - ret = rlm_rest_perform(mctx, section, - handle, request, username->vp_strvalue, password->vp_strvalue); + ret = rlm_rest_perform(mctx, section, handle, request); if (ret < 0) { rest_slab_release(handle); @@ -859,7 +975,7 @@ static unlang_action_t CC_HINT(nonnull) mod_accounting(rlm_rcode_t *p_result, mo handle = rest_slab_reserve(t->slab); if (!handle) RETURN_MODULE_FAIL; - ret = rlm_rest_perform(mctx, section, handle, request, NULL, NULL); + ret = rlm_rest_perform(mctx, section, handle, request); if (ret < 0) { rest_slab_release(handle); @@ -934,7 +1050,7 @@ static unlang_action_t CC_HINT(nonnull) mod_post_auth(rlm_rcode_t *p_result, mod handle = rest_slab_reserve(t->slab); if (!handle) RETURN_MODULE_FAIL; - ret = rlm_rest_perform(mctx, section, handle, request, NULL, NULL); + ret = rlm_rest_perform(mctx, section, handle, request); if (ret < 0) { rest_slab_release(handle); @@ -947,7 +1063,7 @@ static unlang_action_t CC_HINT(nonnull) mod_post_auth(rlm_rcode_t *p_result, mod static int parse_sub_section(rlm_rest_t *inst, CONF_SECTION *parent, conf_parser_t const *config_items, rlm_rest_section_t *config, char const *name) { - CONF_SECTION *cs; + CONF_SECTION *cs, *request_cs; cs = cf_section_find(parent, name, NULL); if (!cs) { @@ -966,35 +1082,35 @@ static int parse_sub_section(rlm_rest_t *inst, CONF_SECTION *parent, conf_parser */ config->name = name; - /* - * Sanity check - */ - if ((config->username && !config->password) || (!config->username && config->password)) { - cf_log_err(cs, "'username' and 'password' must both be set or both be absent"); - - return -1; - } - /* * Convert HTTP method auth and body type strings into their integer equivalents. */ - if ((config->auth != REST_HTTP_AUTH_NONE) && !http_curl_auth[config->auth]) { + if ((config->request.auth != REST_HTTP_AUTH_NONE) && !http_curl_auth[config->request.auth]) { cf_log_err(cs, "Unsupported HTTP auth type \"%s\", check libcurl version, OpenSSL build " "configuration, then recompile this module", - fr_table_str_by_value(http_auth_table, config->auth, "")); + fr_table_str_by_value(http_auth_table, config->request.auth, "")); return -1; } + config->request.method = fr_table_value_by_str(http_method_table, config->request.method_str, REST_HTTP_METHOD_CUSTOM); + /* - * Enable Basic-Auth automatically if username/password were passed + * Custom hackery to figure out if data was set we can't do it any other way because we can't + * parse the tmpl_t execept within a call_env. + * + * We have custom body data so we set REST_HTTP_BODY_CUSTOM, but also need to try and + * figure out what content-type to use. So if they've used the canonical form we + * need to convert it back into a proper HTTP content_type value. */ - if (!config->auth_is_set && config->username && config->password && http_curl_auth[REST_HTTP_AUTH_BASIC]) { - cf_log_debug(cs, "Setting auth = 'basic' as credentials were provided, but no auth method " - "was set"); - config->auth = REST_HTTP_AUTH_BASIC; - } - config->method = fr_table_value_by_str(http_method_table, config->method_str, REST_HTTP_METHOD_CUSTOM); + if ((request_cs = cf_section_find(cs, "request", NULL)) && cf_pair_find(request_cs, "data")) { + http_body_type_t body; + config->request.body = REST_HTTP_BODY_CUSTOM; + + body = fr_table_value_by_str(http_body_type_table, config->request.body_str, REST_HTTP_BODY_UNKNOWN); + if (body != REST_HTTP_BODY_UNKNOWN) { + config->request.body_str = fr_table_str_by_value(http_content_type_table, body, config->request.body_str); + } /* * We don't have any custom user data, so we need to select the right encoder based * on the body type. @@ -1002,72 +1118,58 @@ static int parse_sub_section(rlm_rest_t *inst, CONF_SECTION *parent, conf_parser * To make this slightly more/less confusing, we accept both canonical body_types, * and content_types. */ - if (!config->data) { - config->body = fr_table_value_by_str(http_body_type_table, config->body_str, REST_HTTP_BODY_UNKNOWN); - if (config->body == REST_HTTP_BODY_UNKNOWN) { - config->body = fr_table_value_by_str(http_content_type_table, config->body_str, REST_HTTP_BODY_UNKNOWN); + } else { + config->request.body = fr_table_value_by_str(http_body_type_table, config->request.body_str, REST_HTTP_BODY_UNKNOWN); + if (config->request.body == REST_HTTP_BODY_UNKNOWN) { + config->request.body = fr_table_value_by_str(http_content_type_table, config->request.body_str, REST_HTTP_BODY_UNKNOWN); } - if (config->body == REST_HTTP_BODY_UNKNOWN) { - cf_log_err(cs, "Unknown HTTP body type '%s'", config->body_str); + if (config->request.body == REST_HTTP_BODY_UNKNOWN) { + cf_log_err(cs, "Unknown HTTP body type '%s'", config->request.body_str); return -1; } - switch (http_body_type_supported[config->body]) { + switch (http_body_type_supported[config->request.body]) { case REST_HTTP_BODY_UNSUPPORTED: cf_log_err(cs, "Unsupported HTTP body type \"%s\", please submit patches", - config->body_str); + config->request.body_str); return -1; case REST_HTTP_BODY_INVALID: cf_log_err(cs, "Invalid HTTP body type. \"%s\" is not a valid web API data " - "markup format", config->body_str); + "markup format", config->request.body_str); return -1; case REST_HTTP_BODY_UNAVAILABLE: cf_log_err(cs, "Unavailable HTTP body type. \"%s\" is not available in this " - "build", config->body_str); + "build", config->request.body_str); return -1; default: break; } - /* - * We have custom body data so we set REST_HTTP_BODY_CUSTOM_XLAT, but also need to try and - * figure out what content-type to use. So if they've used the canonical form we - * need to convert it back into a proper HTTP content_type value. - */ - } else { - http_body_type_t body; - - config->body = REST_HTTP_BODY_CUSTOM_XLAT; - - body = fr_table_value_by_str(http_body_type_table, config->body_str, REST_HTTP_BODY_UNKNOWN); - if (body != REST_HTTP_BODY_UNKNOWN) { - config->body_str = fr_table_str_by_value(http_content_type_table, body, config->body_str); - } } - if (config->force_to_str) { - config->force_to = fr_table_value_by_str(http_body_type_table, config->force_to_str, REST_HTTP_BODY_UNKNOWN); - if (config->force_to == REST_HTTP_BODY_UNKNOWN) { - config->force_to = fr_table_value_by_str(http_content_type_table, config->force_to_str, REST_HTTP_BODY_UNKNOWN); + if (config->response.force_to_str) { + config->response.force_to = fr_table_value_by_str(http_body_type_table, config->response.force_to_str, REST_HTTP_BODY_UNKNOWN); + if (config->response.force_to == REST_HTTP_BODY_UNKNOWN) { + config->response.force_to = fr_table_value_by_str(http_content_type_table, config->response.force_to_str, REST_HTTP_BODY_UNKNOWN); } - if (config->force_to == REST_HTTP_BODY_UNKNOWN) { - cf_log_err(cs, "Unknown forced response body type '%s'", config->force_to_str); + if (config->response.force_to == REST_HTTP_BODY_UNKNOWN) { + cf_log_err(cs, "Unknown forced response body type '%s'", config->response.force_to_str); return -1; } - switch (http_body_type_supported[config->force_to]) { + switch (http_body_type_supported[config->response.force_to]) { case REST_HTTP_BODY_UNSUPPORTED: cf_log_err(cs, "Unsupported forced response body type \"%s\", please submit patches", - config->force_to_str); + config->response.force_to_str); return -1; case REST_HTTP_BODY_INVALID: cf_log_err(cs, "Invalid HTTP forced response body type. \"%s\" is not a valid web API data " - "markup format", config->force_to_str); + "markup format", config->response.force_to_str); return -1; default: @@ -1120,6 +1222,7 @@ static int rest_request_cleanup(fr_curl_io_request_t *randle, UNUSED void *uctx) TALLOC_FREE(ctx->response.buffer); TALLOC_FREE(ctx->request.encoder); TALLOC_FREE(ctx->response.decoder); + ctx->response.header = NULL; /* This is owned by the parsed call env and must not be freed */ randle->request = NULL; return 0; @@ -1214,10 +1317,10 @@ static int instantiate(module_inst_ctx_t const *mctx) rlm_rest_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_rest_t); CONF_SECTION *conf = mctx->inst->conf; - inst->xlat.method_str = "GET"; - inst->xlat.body = REST_HTTP_BODY_NONE; - inst->xlat.body_str = "application/x-www-form-urlencoded"; - inst->xlat.force_to_str = "plain"; + inst->xlat.request.method_str = "GET"; + inst->xlat.request.body = REST_HTTP_BODY_NONE; + inst->xlat.request.body_str = "application/x-www-form-urlencoded"; + inst->xlat.response.force_to_str = "plain"; /* * Parse sub-section configs. @@ -1249,6 +1352,7 @@ static int mod_bootstrap(module_inst_ctx_t const *mctx) xlat = xlat_func_register_module(inst, mctx, mctx->inst->name, rest_xlat, FR_TYPE_STRING); xlat_func_args_set(xlat, rest_xlat_args); + xlat_func_call_env_set(xlat, &rest_call_env_xlat); return 0; } @@ -1306,13 +1410,13 @@ module_rlm_t rlm_rest = { /* * Hack to support old configurations */ - { .name1 = "authorize", .name2 = CF_IDENT_ANY, .method = mod_authorize }, + { .name1 = "authorize", .name2 = CF_IDENT_ANY, .method = mod_authorize, .method_env = &rest_call_env_authorize }, - { .name1 = "recv", .name2 = "accounting-request", .method = mod_accounting }, - { .name1 = "recv", .name2 = CF_IDENT_ANY, .method = mod_authorize }, - { .name1 = "accounting", .name2 = CF_IDENT_ANY, .method = mod_accounting }, - { .name1 = "authenticate", .name2 = CF_IDENT_ANY, .method = mod_authenticate }, - { .name1 = "send", .name2 = CF_IDENT_ANY, .method = mod_post_auth }, + { .name1 = "recv", .name2 = "accounting-request", .method = mod_accounting, .method_env = &rest_call_env_accounting }, + { .name1 = "recv", .name2 = CF_IDENT_ANY, .method = mod_authorize, .method_env = &rest_call_env_authorize }, + { .name1 = "accounting", .name2 = CF_IDENT_ANY, .method = mod_accounting, .method_env = &rest_call_env_accounting }, + { .name1 = "authenticate", .name2 = CF_IDENT_ANY, .method = mod_authenticate, .method_env = &rest_call_env_authenticate }, + { .name1 = "send", .name2 = CF_IDENT_ANY, .method = mod_post_auth, .method_env = &rest_call_env_post_auth }, MODULE_NAME_TERMINATOR } }; diff --git a/src/tests/modules/rest/module.conf b/src/tests/modules/rest/module.conf index e137d03f630..c3a49382153 100644 --- a/src/tests/modules/rest/module.conf +++ b/src/tests/modules/rest/module.conf @@ -22,29 +22,43 @@ rest { connect_uri = "http://$ENV{REST_TEST_SERVER}:$ENV{REST_TEST_SERVER_PORT}/" xlat { + request { + header = 'X-Custom-Header: test' + header = "X-Custom-Header: %{User-Name}" + } tls = ${..tls} } authorize { - uri = "${..connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=authorize" - method = "GET" + request { + uri = "${...connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=authorize" + header = "X-Custom-Header: %{User-Name}" + header = "X-Custom-Header: %{Called-Station-ID}" + method = "GET" + } tls = ${..tls} } authenticate { - uri = "https://$ENV{REST_TEST_SERVER}:$ENV{REST_TEST_SERVER_SSL_PORT}/auth?section=authenticate" - method = "POST" + request { + uri = "https://$ENV{REST_TEST_SERVER}:$ENV{REST_TEST_SERVER_SSL_PORT}/auth?section=authenticate" + method = 'POST' + + body = 'post' + data = "user=%{User-Name}" + auth = 'basic' + } + tls = ${..tls} - body = 'post' - data = 'user=%{User-Name}' - auth = 'basic' } accounting { - uri = "https://$ENV{REST_TEST_SERVER}:$ENV{REST_TEST_SERVER_SSL_PORT}/user/%{User-Name}/mac/%{Called-Station-ID}?action=post-auth§ion=accounting" - method = 'POST' - body = 'json' - data = '{"NAS": "%{NAS-IP-Address}", "Password": "%{User-Password}", "Verify": true}' + request { + uri = "https://$ENV{REST_TEST_SERVER}:$ENV{REST_TEST_SERVER_SSL_PORT}/user/%{User-Name}/mac/%{Called-Station-ID}?action=post-auth§ion=accounting" + method = 'POST' + body = 'json' + data = "{\"NAS\": \"%{NAS-IP-Address}\", \"Password\": \"%{User-Password}\", \"Verify\": true}" + } tls = ${..tls} } } diff --git a/src/tests/modules/rest/rest_auth.attrs b/src/tests/modules/rest/rest_auth.attrs new file mode 100644 index 00000000000..14c4cc8ec64 --- /dev/null +++ b/src/tests/modules/rest/rest_auth.attrs @@ -0,0 +1,14 @@ +# +# Input packet +# +Packet-Type = Access-Request +User-Name = 'Bob' +User-Password = 'Saget' +Called-Station-Id = 'aa:bb:cc:dd:ee:ff' +NAS-IP-Address = '192.168.1.1' + +# +# Expected answer +# +Packet-Type == Access-Accept + diff --git a/src/tests/modules/rest/rest_auth.unlang b/src/tests/modules/rest/rest_auth.unlang new file mode 100644 index 00000000000..4bfdf7e0b80 --- /dev/null +++ b/src/tests/modules/rest/rest_auth.unlang @@ -0,0 +1,12 @@ +# Test "authenticate" rest call. Uses http basic authentication +rest.authenticate + +if (!(&REST-HTTP-Status-Code == 200)) { + test_fail +} + +if (!(&REST-HTTP-Body == "Section: authenticate, User: Bob, Authenticated: true\n")) { + test_fail +} + +test_pass diff --git a/src/tests/modules/rest/rest_module.unlang b/src/tests/modules/rest/rest_module.unlang index 91e328b8883..0e0df160856 100644 --- a/src/tests/modules/rest/rest_module.unlang +++ b/src/tests/modules/rest/rest_module.unlang @@ -74,15 +74,4 @@ if (!(&control.NAS-IP-Address[0] == "10.0.0.10") || !(&control.NAS-IP-Address[1] debug_control -# Test "authenticate" rest call. Uses http basic authentication -rest.authenticate - -if (!(&REST-HTTP-Status-Code == 200)) { - test_fail -} - -if (!(&REST-HTTP-Body == "Section: authenticate, User: Bob, Authenticated: true\n")) { - test_fail -} - test_pass diff --git a/src/tests/modules/rest/rest_xlat.unlang b/src/tests/modules/rest/rest_xlat.unlang index ba75842fd62..ffe7891fc2c 100644 --- a/src/tests/modules/rest/rest_xlat.unlang +++ b/src/tests/modules/rest/rest_xlat.unlang @@ -117,29 +117,87 @@ if (!(&result_string == "Section: dummy, User: Bob\n")) { # URI with tainted values in the arguments - input argument includes URI argument # separator - make sure this doesn't end up generating extra arguments, but gets escaped. -&result_string := %rest('GET', "http://%{server_host}:%{server_port}/user/%{User-Name}/reflect/?station=%{Calling-Station-Id}") +group { + string headers + string arguments + string body -if (!(&result_string == "{\"station\":\"dummy&unsafe=escaped\"}\n" )) { - test_fail + map json %rest('POST', "http://%{server_host}:%{server_port}/user/%{User-Name}/reflect/?station=%{Calling-Station-Id}", "{\"test\":\"foo\"}") { + &headers := '$.reply\.Reply-Message.value[0]' + &arguments := '$.reply\.Reply-Message.value[1]' + &body := '$.reply\.Reply-Message.value[2]' + } + + &headers := (string)%base64.decode(%{headers}) + &arguments := (string)%base64.decode(%{arguments}) + &body := (string)%base64.decode(%{body}) + + if (!(&arguments == "{\"station\":\"dummy&unsafe=escaped\"}" )) { + test_fail + } + + if (!(&headers =~ /"user-agent":"FreeRADIUS.*"/)) { + test_fail + } + + if (!(&headers =~ /"x-freeradius-server":"default"/)) { + test_fail + } + + if (!(&headers =~ /"x-custom-header":\["test","Bob"\]/)) { + test_fail + } + + if (!(&body == "{\"test\":\"foo\"}")) { + test_fail + } } # Zero length untainted value - check parsing doesn't break on zero length string -&test_string := "" -&result_string := "%(rest:http://%{server_host}:%{server_port}/user/%{User-Name}/reflect/%{test_string}?station=%{User-Name})" +group { + string headers + string arguments -if (!(&result_string == "{\"station\":\"Bob\"}\n" )) { - test_fail + &test_string := "" + + map json %rest(http://%{server_host}:%{server_port}/user/%{User-Name}/reflect/%{test_string}?station=%{User-Name}) { + &headers := '$.reply\.Reply-Message.value[0]' + &arguments := '$.reply\.Reply-Message.value[1]' + } + + &headers := (string)%base64.decode(%{headers}) + &arguments := (string)%base64.decode(%{arguments}) + + if (!(&arguments == "{\"station\":\"Bob\"}" )) { + test_fail + } + + if (!(&headers =~ /"user-agent":"FreeRADIUS.*"/)) { + test_fail + } + + if (!(&headers =~ /"x-freeradius-server":"default"/)) { + test_fail + } } # Zero length tainted value - check escaping doesn't break on zero length string -&result_string := "%(rest:http://%{server_host}:%{server_port}/user/%{User-Name}/reflect/%{NAS-Identifier}?station=%{Called-Station-Id})" +group { + string arguments -if (!(&result_string == "{\"station\":\"aa:bb:cc:dd:ee:ff\"}\n" )) { - test_fail + map json %rest("http://%{server_host}:%{server_port}/user/%{User-Name}/reflect/%{NAS-Identifier}?station=%{Called-Station-Id}") { + &arguments := '$.reply\.Reply-Message.value[1]' + } + + &arguments := (string)%base64.decode(%{arguments}) + + if (!(&arguments == "{\"station\":\"aa:bb:cc:dd:ee:ff\"}" )) { + test_fail + } } # Test against endpoint which will time out -&result_string := "%(restshorttimeout:http://%{server_host}:%{server_port}/delay)" +&result_string := %restshorttimeout("http://%{server_host}:%{server_port}/delay") if (&REST-HTTP-Status-Code) { test_fail