]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Major rework in rlm_rest
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Sat, 20 Jan 2024 02:26:19 +0000 (20:26 -0600)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Sat, 20 Jan 2024 02:32:07 +0000 (20:32 -0600)
- 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

raddb/mods-available/rest
scripts/ci/openresty/json-api.lua
src/modules/rlm_rest/rest.c
src/modules/rlm_rest/rest.h
src/modules/rlm_rest/rlm_rest.c
src/tests/modules/rest/module.conf
src/tests/modules/rest/rest_auth.attrs [new file with mode: 0644]
src/tests/modules/rest/rest_auth.unlang [new file with mode: 0644]
src/tests/modules/rest/rest_module.unlang
src/tests/modules/rest/rest_xlat.unlang

index f0add16ea9b0740e248ed364f7412c7b49f83343..e96ed00fb6a1fe4b3fdcb62f13aa2156d9d2bf88 100644 (file)
@@ -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 '<header>: <value>'.
-       #  | `body`         | The format of the HTTP body sent to the remote server.
+       #  | `header`       | A custom header in the format '<header>: <value>'.                        | 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(<attr>)`
-       #  | `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(<attr>)`
+       #  | `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 '<header>: <value>'.
+       #  | `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.
        #  |===
        #
index 5b6c471b75be6437a48b39d2abaae29616ad4adc..f9bdad81363a38cc72fee1acfd50ea226253b58b 100644 (file)
@@ -139,7 +139,26 @@ Api.endpoint('GET', '/user/<username>/mac/<client>',
 Api.endpoint('GET', '/user/<username>/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/<username>/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
 )
index c78390356a79d727a39d6a2add7d69be5a17a9cd..6c0e2a62d2c78ba4fec1de10c285283e2c0f6b49 100644 (file)
@@ -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 <freeradius-devel/unlang/call.h>
 #include <freeradius-devel/util/value.h>
 
+#include <talloc.h>
+
 #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, "<INVALID>"),
-                       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 <scheme>://<server>/
-        */
-       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
index 080cad4e25464765c324a82b8ac9a3b526ea3378..2e6b27549bff96c1e759cf24245aff02f629bb54 100644 (file)
@@ -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);
 
index 8fd9ce032ffab27c4b01028d8100ca192922ba0f..1a5ef323135ba35db86c993286c255054edffddb 100644 (file)
  * @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 <talloc.h>
 RCSID("$Id$")
 
 #include <freeradius-devel/curl/base.h>
 #include <freeradius-devel/server/base.h>
+#include <freeradius-devel/server/cf_parse.h>
 #include <freeradius-devel/server/cf_util.h>
+
 #include <freeradius-devel/server/global_lib.h>
 #include <freeradius-devel/server/module_rlm.h>
+#include <freeradius-devel/server/tmpl.h>
 #include <freeradius-devel/server/pairmove.h>
 #include <freeradius-devel/tls/base.h>
+#include <freeradius-devel/util/atexit.h>
 #include <freeradius-devel/util/debug.h>
 #include <freeradius-devel/util/table.h>
 #include <freeradius-devel/util/uri.h>
+#include <freeradius-devel/unlang/call_env.h>
 #include <freeradius-devel/unlang/xlat_func.h>
 
-#include <ctype.h>
 #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, "<INVALID>"));
+                          fr_table_str_by_value(http_auth_table, config->request.auth, "<INVALID>"));
 
                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
        }
 };
index e137d03f630663775279390f42ee6c44221b4355..c3a49382153ca312201efa14d95bef2998f013bb 100644 (file)
@@ -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&section=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&section=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 (file)
index 0000000..14c4cc8
--- /dev/null
@@ -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 (file)
index 0000000..4bfdf7e
--- /dev/null
@@ -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
index 91e328b8883191904c4beaa4a379d06f523188b8..0e0df160856d2fe8e50755bdeb35ff18d13def82 100644 (file)
@@ -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
index ba75842fd62343ec3b4119f6c2e46e56b9d88f46..ffe7891fc2cf9728eddfeb57e3aef38df9f0f3c9 100644 (file)
@@ -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