Add custom body encoder
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Wed, 26 Mar 2014 16:39:15 +0000 (16:39 +0000)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Wed, 26 Mar 2014 16:39:51 +0000 (16:39 +0000)
This allows admin to specify an arbitrary content-type and body data

src/modules/rlm_rest/rest.c
src/modules/rlm_rest/rest.h
src/modules/rlm_rest/rlm_rest.c

index fe0f423..504829b 100644 (file)
@@ -50,6 +50,7 @@ const http_body_type_t http_body_type_supported[HTTP_BODY_NUM_ENTRIES] = {
        HTTP_BODY_UNSUPPORTED,  // HTTP_BODY_UNAVAILABLE
        HTTP_BODY_UNSUPPORTED,  // HTTP_BODY_INVALID
        HTTP_BODY_NONE,         // HTTP_BODY_NONE
+       HTTP_BODY_CUSTOM,       // HTTP_BODY_CUSTOM
        HTTP_BODY_POST,         // HTTP_BODY_POST
 #ifdef HAVE_JSON
        HTTP_BODY_JSON,         // HTTP_BODY_JSON
@@ -193,9 +194,18 @@ const FR_NAME_NUMBER http_content_type_table[] = {
        { "text/x-yaml",                        HTTP_BODY_YAML          },
        { "application/yaml",                   HTTP_BODY_YAML          },
        { "application/x-yaml",                 HTTP_BODY_YAML          },
+
        {  NULL , -1 }
 };
 
+/*
+ *     Encoder specific structures.
+ *     @todo split encoders/decoders into submodules.
+ */
+typedef struct rest_custom_data {
+       char *p;                //!< how much text we've sent so far.
+} rest_custom_data_t;
+
 #ifdef HAVE_JSON
 /** Flags to control the conversion of JSON values to VALUE_PAIRs.
  *
@@ -412,6 +422,33 @@ int mod_conn_delete(UNUSED void *instance, void *handle)
        return true;
 }
 
+/** Copies a pre-expanded xlat string to the output buffer
+ *
+ * @param[out] out Char buffer to write encoded data to.
+ * @param[in] size Multiply by nmemb to get the length of ptr.
+ * @param[in] nmemb Multiply by size to get the length of ptr.
+ * @param[in] userdata rlm_rest_request_t to keep encoding state between calls.
+ * @return length of data (including NULL) written to ptr, or 0 if no more
+ *     data to write.
+ */
+static size_t rest_encode_custom(void *out, size_t size, size_t nmemb, void *userdata)
+{
+       rlm_rest_request_t *ctx = userdata;
+       rest_custom_data_t *data = ctx->encoder;
+
+       size_t  freespace = (size * nmemb) - 1;
+       size_t  len;
+
+       len = strlcpy(out, data->p, freespace);
+       if (is_truncated(len, freespace)) {
+               data->p += (freespace - 1);
+               return freespace - 1;
+       }
+       data->p += len;
+
+       return len;
+}
+
 /** Encodes VALUE_PAIR linked list in POST format
  *
  * This is a stream function matching the rest_read_t prototype. Multiple
@@ -617,8 +654,8 @@ no_space:
 static size_t rest_encode_json(void *out, size_t size, size_t nmemb, void *userdata)
 {
        rlm_rest_request_t      *ctx = userdata;
-       REQUEST         *request = ctx->request; /* Used by RDEBUG */
-       VALUE_PAIR      *vp, *next;
+       REQUEST                 *request = ctx->request; /* Used by RDEBUG */
+       VALUE_PAIR              *vp, *next;
 
        char *p = out;          /* Position in buffer */
        char *encoded = p;      /* Position in buffer of last fully encoded attribute or value */
@@ -1803,12 +1840,10 @@ int rest_request_config(rlm_rest_t *instance, rlm_rest_section_t *section,
 
        SET_OPTION(CURLOPT_USERAGENT, "FreeRADIUS");
 
-       content_type = fr_int2str(http_content_type_table, type, NULL);
-       if (content_type) {
-               snprintf(buffer, (sizeof(buffer) - 1), "Content-Type: %s", content_type);
-               ctx->headers = curl_slist_append(ctx->headers, buffer);
-               if (!ctx->headers) goto error_header;
-       }
+       content_type = fr_int2str(http_content_type_table, type, section->body_str);
+       snprintf(buffer, (sizeof(buffer) - 1), "Content-Type: %s", content_type);
+       ctx->headers = curl_slist_append(ctx->headers, buffer);
+       if (!ctx->headers) goto error_header;
 
        if (section->timeout) {
                SET_OPTION(CURLOPT_TIMEOUT, section->timeout);
@@ -1981,46 +2016,74 @@ int rest_request_config(rlm_rest_t *instance, rlm_rest_section_t *section,
 
                RDEBUG3("Request body content-type will be \"%s\"",
                        fr_int2str(http_content_type_table, type, section->body_str));
-               switch (type) {
-               case HTTP_BODY_NONE:
-                       if (rest_request_config_body(instance, section, request, handle,
-                                                    NULL) < 0) {
-                               return -1;
-                       }
+               break;
 
-                       break;
+       default:
+               rad_assert(0);
+       };
 
-#ifdef HAVE_JSON
-               case HTTP_BODY_JSON:
-                       rest_request_init(request, &ctx->request, 1);
+       /*
+        *  Setup encoder specific options
+        */
+       switch (type) {
+       case HTTP_BODY_NONE:
+               if (rest_request_config_body(instance, section, request, handle,
+                                            NULL) < 0) {
+                       return -1;
+               }
 
-                       if (rest_request_config_body(instance, section, request, handle,
-                                                    rest_encode_json) < 0) {
-                               return -1;
-                       }
+               break;
 
-                       break;
-#endif
+       case HTTP_BODY_CUSTOM:
+       {
+               rest_custom_data_t *data;
+               char *expanded = NULL;
 
-               case HTTP_BODY_POST:
-                       rest_request_init(request, &ctx->request, 0);
+               if (radius_axlat(&expanded, request, section->data, NULL, NULL) < 0) {
+                       return -1;
+               }
 
-                       if (rest_request_config_body(instance, section, request, handle,
-                                                    rest_encode_post) < 0) {
-                               return -1;
-                       }
+               data = talloc_zero(request, rest_custom_data_t);
+               data->p = expanded;
 
-                       break;
+               /* Use the encoder specific pointer to store the data we need to encode */
+               ctx->request.encoder = data;
+               if (rest_request_config_body(instance, section, request, handle,
+                                            rest_encode_custom) < 0) {
+                       TALLOC_FREE(ctx->request.encoder);
+                       return -1;
+               }
 
-               default:
-                       assert(0);
+               break;
+       }
+
+#ifdef HAVE_JSON
+       case HTTP_BODY_JSON:
+               rest_request_init(request, &ctx->request, true);
+
+               if (rest_request_config_body(instance, section, request, handle,
+                                            rest_encode_json) < 0) {
+                       return -1;
+               }
+
+               break;
+#endif
+
+       case HTTP_BODY_POST:
+               rest_request_init(request, &ctx->request, false);
+
+               if (rest_request_config_body(instance, section, request, handle,
+                                            rest_encode_post) < 0) {
+                       return -1;
                }
 
                break;
 
        default:
-               rad_assert(0);
-       };
+               assert(0);
+       }
+
+
 finish:
        SET_OPTION(CURLOPT_HTTPHEADER, ctx->headers);
 
index 46b4c2c..1104c98 100644 (file)
@@ -58,6 +58,7 @@ typedef enum {
        HTTP_BODY_UNAVAILABLE,
        HTTP_BODY_INVALID,
        HTTP_BODY_NONE,
+       HTTP_BODY_CUSTOM,
        HTTP_BODY_POST,
        HTTP_BODY_JSON,
        HTTP_BODY_XML,
index 42663c3..b5532eb 100644 (file)
@@ -66,6 +66,7 @@ static const CONF_PARSER section_config[] = {
        { "uri", PW_TYPE_STRING_PTR, offsetof(rlm_rest_section_t, uri), NULL, ""  },
        { "method", PW_TYPE_STRING_PTR, offsetof(rlm_rest_section_t, method_str), NULL, "GET" },
        { "body", PW_TYPE_STRING_PTR, offsetof(rlm_rest_section_t, body_str), NULL, "none" },
+       { "data", PW_TYPE_STRING_PTR, offsetof(rlm_rest_section_t, data), NULL, NULL },
 
        /* User authentication */
        { "auth", PW_TYPE_STRING_PTR, offsetof(rlm_rest_section_t, auth_str), NULL, "none" },
@@ -166,7 +167,7 @@ static ssize_t rest_xlat(void *instance, REQUEST *request,
        if (!handle) return -1;
 
        /*
-        *  Build xlat'd URI, this allows REST servers to be specified by
+        *  Unescape parts of xlat'd URI, this allows REST servers to be specified by
         *  request attributes.
         */
        len = rest_uri_host_unescape(&uri, instance, request, handle, fmt);
@@ -495,6 +496,7 @@ static int parse_sub_section(CONF_SECTION *parent, rlm_rest_section_t *config, r
        config->auth = fr_str2int(http_auth_table, config->auth_str, HTTP_AUTH_UNKNOWN);
        if (config->auth == HTTP_AUTH_UNKNOWN) {
                cf_log_err_cs(cs, "Unknown HTTP auth type '%s'", config->auth_str);
+
                return -1;
        } else if ((config->auth != HTTP_AUTH_NONE) && !http_curl_auth[config->auth]) {
                cf_log_err_cs(cs, "Unsupported HTTP auth type \"%s\", check libcurl version, OpenSSL build "
@@ -504,19 +506,57 @@ static int parse_sub_section(CONF_SECTION *parent, rlm_rest_section_t *config, r
        }
 
        config->method = fr_str2int(http_method_table, config->method_str, HTTP_METHOD_CUSTOM);
-       config->body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
 
-       if (config->body == HTTP_BODY_UNKNOWN) {
-               cf_log_err_cs(cs, "Unknown HTTP body type '%s'", config->body_str);
-               return -1;
-       }
+       /*
+        *  We don't have any custom user data, so we need to select the right encoder based
+        *  on the body type.
+        *
+        *  To make this slightly more/less confusing, we accept both canonical body_types,
+        *  and content_types.
+        */
+       if (!config->data) {
+               config->body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
+               if (config->body == HTTP_BODY_UNKNOWN) {
+                       config->body = fr_str2int(http_content_type_table, config->body_str, HTTP_BODY_UNKNOWN);
+               }
 
-       if (http_body_type_supported[config->body] == HTTP_BODY_UNSUPPORTED) {
-               cf_log_err_cs(cs, "Unsupported HTTP body type \"%s\", please submit patches", config->body_str);
-               return -1;
+               if (config->body == HTTP_BODY_UNKNOWN) {
+                       cf_log_err_cs(cs, "Unknown HTTP body type '%s'", config->body_str);
+                       return -1;
+               }
+
+               switch (http_body_type_supported[config->body])
+               {
+                       case HTTP_BODY_UNSUPPORTED:
+                               cf_log_err_cs(cs, "Unsupported HTTP body type \"%s\", please submit patches",
+                                             config->body_str);
+                               return -1;
+
+                       case HTTP_BODY_INVALID:
+                               cf_log_err_cs(cs, "Invalid HTTP body type.  \"%s\" is not a valid web API data "
+                                             "markup format", config->body_str);
+                               return -1;
+
+                       default:
+                               break;
+               }
+       /*
+        *  We have custom body data so we set 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.
+        */
+       } else {
+               http_body_type_t body;
+
+               config->body = HTTP_BODY_CUSTOM;
+
+               body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
+               if (body != HTTP_BODY_UNKNOWN) {
+                       config->body_str = fr_int2str(http_content_type_table, body, config->body_str);
+               }
        }
 
-       return 1;
+       return 0;
 }
 
 /*