From: Arran Cudbard-Bell Date: Mon, 10 Oct 2011 18:20:44 +0000 (+0200) Subject: Initial commit of rlm_rest module X-Git-Tag: release_3_0_0_beta0~298^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9de781226751ca2a3e39fac50b70a0092f459a89;p=thirdparty%2Ffreeradius-server.git Initial commit of rlm_rest module Add library independent streaming JSON generator Add library independent streaming POST generator Add support for parsing JSON and POST responses Add support for parsing do_xlat and is_json flags in JSON responses Add function to emulate CURLs multiple calls to the JSON generator when in stream mode, to allow transfer of data as a single contiguous block Deduplicate truncation detection code. Improvde consistancy of documentation. Replace 1, 0 return codes with TRUE/FALSE macros. Slightly better version of rest_uri_build Add tables for auth types Add a bunch of formatting fixes and extra options for SSL certs in curl --- diff --git a/raddb/modules/rest b/raddb/modules/rest new file mode 100644 index 00000000000..dca280cbf43 --- /dev/null +++ b/raddb/modules/rest @@ -0,0 +1,39 @@ +rest { + # rlm_rest will open a connection to the server specified in connect_uri + # to populate the connection cache, ready for the first request. + # The server will not start if the server specified is unreachable. + # + # If you wish to disable this pre-caching and reachability check, + # comment out the configuration item below. + connect_uri = "http://power.freeradius.org" + + pool { + start = 5 + max = 10 + spare = 3 + uses = 0 + idle_timeout = 100 + lifetime = 0 + } + + authorize { + uri = "${..connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=authorize" + method = "get"` + } + authenticate { + uri = "${..connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=authenticate" + method = "get" + } + accounting { + uri = "${..connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=accounting" + method = "post" + } + session { + uri = "${..connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=checksimul" + method = "post" + } + post-auth { + uri = "${..connect_uri}/user/%{User-Name}/mac/%{Called-Station-ID}?section=post-auth" + method = "post" + } +} diff --git a/src/modules/rlm_rest/Makefile b/src/modules/rlm_rest/Makefile new file mode 100644 index 00000000000..c66b8c05198 --- /dev/null +++ b/src/modules/rlm_rest/Makefile @@ -0,0 +1,37 @@ +####################################################################### +# +# TARGET should be set by autoconf only. Don't touch it. +# +# The SRCS definition should list ALL source files. +# +# The HEADERS definition should list ALL header files +# +# RLM_CFLAGS defines addition C compiler flags. You usually don't +# want to modify this, though. Get it from autoconf. +# +# The RLM_LIBS definition should list ALL required libraries. +# These libraries really should be pulled from the 'config.mak' +# definitions, if at all possible. These definitions are also +# echoed into another file in ../lib, where they're picked up by +# ../main/Makefile for building the version of the server with +# statically linked modules. Get it from autoconf. +# +# RLM_INSTALL is the names of additional rules you need to install +# some particular portion of the module. Usually, leave it blank. +# +####################################################################### +TARGET = rlm_rest +SRCS = rlm_rest.c rest.c +HEADERS = rest.h +RLM_CFLAGS = +RLM_LIBS = -ljson -lcurl +RLM_INSTALL = install-rest + +## this uses the RLM_CFLAGS and RLM_LIBS and SRCS defs to make TARGET. +include ../rules.mak + +$(LT_OBJS): $(HEADERS) + +## the rule that RLM_INSTALL tells the parent rules.mak to use. +install-rest: + touch . diff --git a/src/modules/rlm_rest/Makefile.clean b/src/modules/rlm_rest/Makefile.clean new file mode 100644 index 00000000000..0e07944d393 --- /dev/null +++ b/src/modules/rlm_rest/Makefile.clean @@ -0,0 +1,11 @@ +TARGET = rlm_rest +SRCS = rlm_rest.c rest.c +HEADERS = rest.h +RLM_CFLAGS = +RLM_LIBS = + +include ../rules.mak + +$(STATIC_OBJS): $(HEADERS) + +$(DYNAMIC_OBJS): $(HEADERS) diff --git a/src/modules/rlm_rest/Makefile.in b/src/modules/rlm_rest/Makefile.in new file mode 100644 index 00000000000..7684e4a541f --- /dev/null +++ b/src/modules/rlm_rest/Makefile.in @@ -0,0 +1,37 @@ +####################################################################### +# +# TARGET should be set by autoconf only. Don't touch it. +# +# The SRCS definition should list ALL source files. +# +# The HEADERS definition should list ALL header files +# +# RLM_CFLAGS defines addition C compiler flags. You usually don't +# want to modify this, though. Get it from autoconf. +# +# The RLM_LIBS definition should list ALL required libraries. +# These libraries really should be pulled from the 'config.mak' +# definitions, if at all possible. These definitions are also +# echoed into another file in ../lib, where they're picked up by +# ../main/Makefile for building the version of the server with +# statically linked modules. Get it from autoconf. +# +# RLM_INSTALL is the names of additional rules you need to install +# some particular portion of the module. Usually, leave it blank. +# +####################################################################### +TARGET = @targetname@ +SRCS = rlm_rest.c rest.c +HEADERS = rest.h +RLM_CFLAGS = @rest_cflags@ +RLM_LIBS = @rest_ldflags@ +RLM_INSTALL = install-rest + +## this uses the RLM_CFLAGS and RLM_LIBS and SRCS defs to make TARGET. +include ../rules.mak + +$(LT_OBJS): $(HEADERS) + +## the rule that RLM_INSTALL tells the parent rules.mak to use. +install-rest: + touch . diff --git a/src/modules/rlm_rest/all.mk b/src/modules/rlm_rest/all.mk new file mode 100644 index 00000000000..a4035615aa1 --- /dev/null +++ b/src/modules/rlm_rest/all.mk @@ -0,0 +1,3 @@ +SOURCES := rlm_rest.c rest.c + +TARGET := rlm_rest.a diff --git a/src/modules/rlm_rest/config.h.in b/src/modules/rlm_rest/config.h.in new file mode 100644 index 00000000000..7c783cc99f3 --- /dev/null +++ b/src/modules/rlm_rest/config.h.in @@ -0,0 +1,52 @@ +/* config.h.in. Generated from configure.in by autoheader. */ + +/* Define to 1 if you have the header file. */ +#undef HAVE_INTTYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_MEMORY_H + +/* Define to 1 if you have the `printf' function. */ +#undef HAVE_PRINTF + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDINT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDIO_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDLIB_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRINGS_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRING_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_STAT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_UNISTD_H + +/* Define to the address where bug reports for this package should be sent. */ +#undef PACKAGE_BUGREPORT + +/* Define to the full name of this package. */ +#undef PACKAGE_NAME + +/* Define to the full name and version of this package. */ +#undef PACKAGE_STRING + +/* Define to the one symbol short name of this package. */ +#undef PACKAGE_TARNAME + +/* Define to the version of this package. */ +#undef PACKAGE_VERSION + +/* Define to 1 if you have the ANSI C header files. */ +#undef STDC_HEADERS diff --git a/src/modules/rlm_rest/configure.in b/src/modules/rlm_rest/configure.in new file mode 100644 index 00000000000..e431f98d09d --- /dev/null +++ b/src/modules/rlm_rest/configure.in @@ -0,0 +1,177 @@ +AAC_PREREQ([2.61]) +AC_INIT(rlm_rest.c) +AC_REVISION($Revision$) +AC_DEFUN(modname,[rlm_rest]) + +fail= +SMART_LIBS= +SMART_CLFAGS= +if test x$with_[]modname != xno; then + + dnl ############################################################ + dnl # Check for command line options + dnl ############################################################ + + dnl extra argument: --with-curl-include-dir=DIR + curl_include_dir= + AC_ARG_WITH(curl-include-dir, + [AS_HELP_STRING([--with-curl-include-dir=DIR], + [Directory where the curl includes may be found])], + [case "$withval" in + no) + AC_MSG_ERROR(Need curl-include-dir) + ;; + yes) + ;; + *) + curl_include_dir="$withval" + ;; + esac]) + + dnl extra argument: --with-curl-lib-dir=DIR + curl_lib_dir= + AC_ARG_WITH(curl-lib-dir, + [AS_HELP_STRING([--with-curl-lib-dir=DIR], + [Directory where the curl libraries may be found])], + [case "$withval" in + no) + AC_MSG_ERROR(Need curl-lib-dir) + ;; + yes) + ;; + *) + curl_lib_dir="$withval" + ;; + esac]) + + dnl extra argument: --with-curl-dir=DIR + AC_ARG_WITH(curl-dir, + [AS_HELP_STRING([--with-curl-dir=DIR], + [Base directory where curl is installed])], + [case "$withval" in + no) + AC_MSG_ERROR(Need curl-dir) + ;; + yes) + ;; + *) + curl_lib_dir="$withval/lib" + curl_include_dir="$withval/include" + ;; + esac]) + + dnl extra argument: --with-json-include-dir=DIR + json_include_dir= + AC_ARG_WITH(json-include-dir, + [AS_HELP_STRING([--with-json-include-dir=DIR], + [Directory where the json includes may be found])], + [case "$withval" in + no) + AC_MSG_ERROR(Need json-include-dir) + ;; + yes) + ;; + *) + json_include_dir="$withval" + ;; + esac]) + + dnl extra argument: --with-json-lib-dir=DIR + json_lib_dir= + AC_ARG_WITH(json-lib-dir, + [AS_HELP_STRING([--with-json-lib-dir=DIR], + [Directory where the json-c libraries may be found])], + [case "$withval" in + no) + AC_MSG_ERROR(Need json-lib-dir) + ;; + yes) + ;; + *) + json_lib_dir="$withval" + ;; + esac]) + + dnl extra argument: --with-json-dir=DIR + AC_ARG_WITH(json-dir, + [AS_HELP_STRING([--with-json-dir=DIR], + [Base directory where json-c is installed])], + [case "$withval" in + no) + AC_MSG_ERROR(Need json-dir) + ;; + yes) + ;; + *) + json_lib_dir="$withval/lib" + json_include_dir="$withval/include" + ;; + esac]) + + dnl ############################################################ + dnl # Check for programs + dnl ############################################################ + + AC_PROG_CC + + dnl ############################################################ + dnl # Check for libraries + dnl ############################################################ + + smart_try_dir="$curl_lib_dir" + FR_SMART_CHECK_LIB(curl, curl_easy_init) + if test "x$ac_cv_lib_curl_curl_easy_init" != "xyes" + then + AC_MSG_WARN([curl libraries not found. Use --with-curl-lib-dir=.]) + fail="$fail libcurl" + fi + + smart_try_dir="$json_lib_dir" + FR_SMART_CHECK_LIB(json, json_tokener_parse) + if test "x$ac_cv_lib_json_json_tokener_parse" != "xyes" + then + AC_MSG_WARN([json-c libraries not found. Use --with-json-lib-dir=.]) + fail="$fail json" + fi + + dnl ############################################################ + dnl # Check for header files + dnl ############################################################ + + smart_try_dir="$curl_include_dir" + FR_SMART_CHECK_INCLUDE(curl/curl.h) + if test "x$ac_cv_header_curl_curl_h" != "xyes"; then + AC_MSG_WARN([curl headers not found. Use --with-curl-include-dir=.]) + fail="$fail curl/curl.h" + fi + + smart_try_dir="$json_include_dir" + FR_SMART_CHECK_INCLUDE(json/json.h) + if test "x$ac_cv_header_json_json_h" != "xyes"; then + AC_MSG_WARN([json-c headers not found. Use --with-json-include-dir=.]) + fail="$fail json/json.h" + fi + + targetname=modname +else + targetname= + echo \*\*\* module modname is disabled. +fi + +dnl Don't change this section. +if test "x$fail" != x; then + if test "x${enable_strict_dependencies}" = xyes; then + AC_MSG_ERROR([set --without-]modname[ to disable it explicitly.]) + else + AC_MSG_WARN([silently not building ]modname[.]) + AC_MSG_WARN([FAILURE: ]modname[ requires:$fail.]); + targetname= + fi +fi + +rest_ldflags="$SMART_LIBS" +rest_cflags="$SMART_CFLAGS" +AC_SUBST(rest_ldflags) +AC_SUBST(rest_cflags) +AC_SUBST(targetname) +AC_OUTPUT(Makefile) diff --git a/src/modules/rlm_rest/demo.pl b/src/modules/rlm_rest/demo.pl new file mode 100755 index 00000000000..805f7c3edd0 --- /dev/null +++ b/src/modules/rlm_rest/demo.pl @@ -0,0 +1,33 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use HTTP::Daemon; +use HTTP::Status; +use HTTP::Response; + +my $d = new HTTP::Daemon(LocalAddr => '127.0.0.1', LocalPort => 9090); +print "Please contact me at: url, ">\n"; +while (my $c = $d->accept) { + while (my $r = $c->get_request) { + print "Got " . $r->method . " request\n"; + if ($r->method eq 'POST' and $r->url->path eq "/") { + my $resp = HTTP::Response->new( '200', 'OK' ); + + #$resp->header("Content-Type" => "application/x-www-form-urlencoded"); + #$resp->content("reply:User-Name=kittens&reply:Service-Type=Framed-User&reply:HP-Cos=0000&reply:HP-Bandwidth-Max-Ingress=999999999999999999999999999999999999&reply:User-Name=mittens"); + + $resp->header("Content-Type" => "application/json"); + $resp->content('{ + "reply|User-Name":"kittens %{User-Name}", + }'); + + $c->send_response($resp); + } else { + $c->send_error(RC_FORBIDDEN) + } + } + $c->close; + undef($c); + } diff --git a/src/modules/rlm_rest/rest.c b/src/modules/rlm_rest/rest.c new file mode 100644 index 00000000000..7d0b8d8dba3 --- /dev/null +++ b/src/modules/rlm_rest/rest.c @@ -0,0 +1,2382 @@ +/** Functions and datatypes for the REST (HTTP) transport. + * + * @file rest.c + * + * Version: $Id$ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Copyright 2011 Arran Cudbard-Bell + */ + +#include +RCSID("$Id$") + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "rest.h" + +/** Table of encoder/decoder support. + * + * Indexes in this table match the http_body_type_t enum, and should be + * updated if additional enum values are added. + * + * @see http_body_type_t + */ +const http_body_type_t http_body_type_supported[HTTP_BODY_NUM_ENTRIES] = { + HTTP_BODY_UNSUPPORTED, // HTTP_BODY_UNKOWN + HTTP_BODY_UNSUPPORTED, // HTTP_BODY_UNSUPPORTED + HTTP_BODY_UNSUPPORTED, // HTTP_BODY_INVALID + HTTP_BODY_POST, // HTTP_BODY_POST + HTTP_BODY_JSON, // HTTP_BODY_JSON + HTTP_BODY_UNSUPPORTED, // HTTP_BODY_XML + HTTP_BODY_UNSUPPORTED, // HTTP_BODY_YAML + HTTP_BODY_INVALID, // HTTP_BODY_HTML + HTTP_BODY_INVALID // HTTP_BODY_PLAIN +}; + +/* + * Lib CURL doesn't define symbols for unsupported auth methods + */ +#ifndef CURLOPT_TLSAUTH_SRP +#define CURLOPT_TLSAUTH_SRP 0 +#endif +#ifndef CURLAUTH_BASIC +#define CURLAUTH_BASIC 0 +#endif +#ifndef CURLAUTH_DIGEST +#define CURLAUTH_DIGEST 0 +#endif +#ifndef CURLAUTH_DIGEST_IE +#define CURLAUTH_DIGEST_IE 0 +#endif +#ifndef CURLAUTH_GSSNEGOTIATE +#define CURLAUTH_GSSNEGOTIATE 0 +#endif +#ifndef CURLAUTH_NTLM +#define CURLAUTH_NTLM 0 +#endif +#ifndef CURLAUTH_NTLM_WB +#define CURLAUTH_NTLM_WB 0 +#endif + +const http_body_type_t http_curl_auth[HTTP_AUTH_NUM_ENTRIES] = { + 0, // HTTP_AUTH_UNKNOWN + 0, // HTTP_AUTH_NONE + CURLOPT_TLSAUTH_SRP, // HTTP_AUTH_TLS_SRP + CURLAUTH_BASIC, // HTTP_AUTH_BASIC + CURLAUTH_DIGEST, // HTTP_AUTH_DIGEST + CURLAUTH_DIGEST_IE, // HTTP_AUTH_DIGEST_IE + CURLAUTH_GSSNEGOTIATE, // HTTP_AUTH_GSSNEGOTIATE + CURLAUTH_NTLM, // HTTP_AUTH_NTLM + CURLAUTH_NTLM_WB, // HTTP_AUTH_NTLM_WB + CURLAUTH_ANY, // HTTP_AUTH_ANY + CURLAUTH_ANYSAFE // HTTP_AUTH_ANY_SAFE +}; + + +/** Conversion table for method config values. + * + * HTTP verb strings for http_method_t enum values. Used by libcurl in the + * status line of the outgoing HTTP header, by rest_write_header for decoding + * incoming HTTP responses, and by the configuration parser. + * + * @see http_method_t + * @see fr_str2int + * @see fr_int2str + */ +const FR_NAME_NUMBER http_method_table[] = { + { "GET", HTTP_METHOD_GET }, + { "POST", HTTP_METHOD_POST }, + { "PUT", HTTP_METHOD_PUT }, + { "DELETE", HTTP_METHOD_DELETE }, + + { NULL , -1 } +}; + +/** Conversion table for type config values. + * + * Textual names for http_body_type_t enum values, used by the + * configuration parser. + * + * @see http_body_Type_t + * @see fr_str2int + * @see fr_int2str + */ +const FR_NAME_NUMBER http_body_type_table[] = { + { "unknown", HTTP_BODY_UNKNOWN }, + { "unsupported", HTTP_BODY_UNSUPPORTED }, + { "invalid", HTTP_BODY_INVALID }, + { "post", HTTP_BODY_POST }, + { "json", HTTP_BODY_JSON }, + { "xml", HTTP_BODY_XML }, + { "yaml", HTTP_BODY_YAML }, + { "html", HTTP_BODY_HTML }, + { "plain", HTTP_BODY_PLAIN }, + + { NULL , -1 } +}; + +const FR_NAME_NUMBER http_auth_table[] = { + { "none", HTTP_AUTH_NONE }, + { "srp", HTTP_AUTH_TLS_SRP }, + { "basic", HTTP_AUTH_BASIC }, + { "digest", HTTP_AUTH_DIGEST }, + { "digest-ie", HTTP_AUTH_DIGEST_IE }, + { "gss-negotiate", HTTP_AUTH_GSSNEGOTIATE }, + { "ntlm", HTTP_AUTH_NTLM }, + { "ntlm-winbind", HTTP_AUTH_NTLM_WB }, + { "any", HTTP_AUTH_ANY }, + { "safe", HTTP_AUTH_ANY_SAFE }, + + { NULL , -1 } +}; + +/** Conversion table for "Content-Type" header values. + * + * Used by rest_write_header for parsing incoming headers. + * + * Values we expect to see in the 'Content-Type:' header of the incoming + * response. + * + * Some data types (like YAML) do no have standard MIME types defined, + * so multiple types, are listed here. + * + * @see http_body_Type_t + * @see fr_str2int + * @see fr_int2str + */ +const FR_NAME_NUMBER http_content_type_table[] = { + { "application/x-www-form-urlencoded", HTTP_BODY_POST }, + { "application/json", HTTP_BODY_JSON }, + { "text/html", HTTP_BODY_HTML }, + { "text/plain", HTTP_BODY_PLAIN }, + { "text/xml", HTTP_BODY_XML }, + { "text/yaml", HTTP_BODY_YAML }, + { "text/x-yaml", HTTP_BODY_YAML }, + { "application/yaml", HTTP_BODY_YAML }, + { "application/x-yaml", HTTP_BODY_YAML }, + { NULL , -1 } +}; + +/** Flags to control the conversion of JSON values to VALUE_PAIRs. + * + * These fields are set when parsing the expanded format for value pairs in + * JSON, and control how json_pairmake_leaf and json_pairmake convert the JSON + * value, and move the new VALUE_PAIR into an attribute list. + * + * @see json_pairmake + * @see json_pairmake_leaf + */ +typedef struct json_flags { + boolean do_xlat; //!< If TRUE value will be expanded with xlat. + boolean is_json; //!< If TRUE value will be inserted as raw JSON + // (multiple values not supported). + FR_TOKEN operator; //!< The operator that determines how the new VP + // is processed. @see fr_tokens +} json_flags_t; + +/** Initialises libcurl. + * + * Allocates global variables and memory required for libcurl to fundtion. + * MUST only be called once per module instance. + * + * rest_cleanup must not be called if rest_init fails. + * + * @see rest_cleanup + * + * @param[in] instance configuration data. + * @return TRUE if init succeeded FALSE if it failed. + */ +int rest_init(rlm_rest_t *instance) +{ + CURLcode ret; + + ret = curl_global_init(CURL_GLOBAL_ALL); + if (ret != CURLE_OK) { + radlog(L_ERR, + "rlm_rest (%s): CURL init returned error: %i - %s", + instance->xlat_name, + ret, curl_easy_strerror(ret)); + + curl_global_cleanup(); + return FALSE; + } + + radlog(L_DBG, "rlm_rest (%s): CURL library version: %s", + instance->xlat_name, + curl_version()); + + return TRUE; +} + +/** Cleans up after libcurl. + * + * Wrapper around curl_global_cleanup, frees any memory allocated by rest_init. + * Must only be called once per call of rest_init. + * + * @see rest_init + */ +void rest_cleanup(void) +{ + curl_global_cleanup(); +} + +/** Creates a new connection handle for use by the FR connection API. + * + * Matches the fr_connection_create_t function prototype, is passed to + * fr_connection_pool_init, and called when a new connection is required by the + * connection pool API. + * + * Creates an instances of rlm_rest_handle_t, and rlm_rest_curl_context_t + * which hold the context data required for generating requests and parsing + * responses. Calling rest_socket_delete will free this memory. + * + * If instance->connect_uri is not NULL libcurl will attempt to open a + * TCP socket to the server specified in the URI. This is done so that when the + * socket is first used, there will already be a cached TCP connection to the + * REST server associated with the curl handle. + * + * @see rest_socket_delete + * @see fr_connection_pool_init + * @see fr_connection_create_t + * @see connection.c + * + * @param[in] instance configuration data. + * @return connection handle or NULL if the connection failed or couldn't + * be initialised. + */ +void *rest_socket_create(void *instance) +{ + rlm_rest_t *inst = instance; + + rlm_rest_handle_t *randle; + rlm_rest_curl_context_t *ctx; + + CURL *candle = curl_easy_init(); + CURLcode ret; + + if (!candle) { + radlog(L_ERR, "rlm_rest (%s): Failed to create CURL handle", + inst->xlat_name); + return NULL; + } + + if (!*inst->connect_uri) { + radlog(L_ERR, "rlm_rest (%s): Skipping pre-connect," + " connect_uri not specified", inst->xlat_name); + return candle; + } + + /* + * Pre-establish TCP connection to webserver. This would usually be + * done on the first request, but we do it here to minimise + * latency. + */ + ret = curl_easy_setopt(candle, CURLOPT_CONNECT_ONLY, 1); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_URL, + inst->connect_uri); + if (ret != CURLE_OK) goto error; + + radlog(L_DBG, "rlm_rest (%s): Connecting to \"%s\"", + inst->xlat_name, + inst->connect_uri); + + ret = curl_easy_perform(candle); + if (ret != CURLE_OK) { + radlog(L_ERR, "rlm_rest (%s): Connection failed: %i - %s", + inst->xlat_name, + ret, curl_easy_strerror(ret)); + + goto connection_error; + } + + /* + * Malloc memory for the connection handle abstraction. + */ + randle = malloc(sizeof(*randle)); + memset(randle, 0, sizeof(*randle)); + + ctx = malloc(sizeof(*ctx)); + memset(ctx, 0, sizeof(*ctx)); + + ctx->headers = NULL; /* CURL needs this to be NULL */ + ctx->read.instance = inst; + + randle->ctx = ctx; + randle->handle = candle; + + /* + * Clear any previously configured options for the first request. + */ + curl_easy_reset(candle); + + return randle; + + /* + * Cleanup for error conditions. + */ + error: + + radlog(L_ERR, "rlm_rest (%s): Failed setting curl option: %i - %s", + inst->xlat_name, + ret, curl_easy_strerror(ret)); + + /* + * So we don't leak CURL handles. + */ + connection_error: + + curl_easy_cleanup(candle); + + return NULL; +} + +/** Verifies that the last TCP socket associated with a handle is still active. + * + * Quieries libcurl to try and determine if the TCP socket associated with a + * connection handle is still viable. + * + * @param[in] instance configuration data. + * @param[in] handle to check. + * @returns FALSE if the last socket is dead, or if the socket state couldn't be + * determined, else TRUE. + */ +int rest_socket_alive(void *instance, void *handle) +{ + rlm_rest_t *inst = instance; + rlm_rest_handle_t *randle = handle; + CURL *candle = randle->handle; + + long last_socket; + CURLcode ret; + + curl_easy_getinfo(candle, CURLINFO_LASTSOCKET, &last_socket); + if (ret != CURLE_OK) { + radlog(L_ERR, + "rlm_rest (%s): Couldn't determine socket" + " state: %i - %s", inst->xlat_name, ret, + curl_easy_strerror(ret)); + + return FALSE; + } + + if (last_socket == -1) { + return FALSE; + } + + return TRUE; +} + +/** Frees a libcurl handle, and any additional memory used by context data. + * + * @param[in] instance configuration data. + * @param[in] handle rlm_rest_handle_t to close and free. + * @return returns TRUE. + */ +int rest_socket_delete(UNUSED void *instance, void *handle) +{ + rlm_rest_handle_t *randle = handle; + CURL *candle = randle->handle; + + curl_easy_cleanup(candle); + + free(randle->ctx); + free(randle); + + return TRUE; +} + +/** Encodes VALUE_PAIR linked list in POST format + * + * This is a stream function matching the rest_read_t prototype. Multiple + * successive calls will return additional encoded VALUE_PAIRs. + * Only complete attribute headers @verbatim '=' @endverbatim and values + * will be written to the ptr buffer. + * + * POST request format is: + * @verbatim =&=&=@endverbatim + * + * All attributes and values are url encoded. There is currently no support for + * nested attributes, or attribute qualifiers. + * + * Nested attributes may be added in the future using + * @verbatim :@endverbatim + * to denotate nesting. + * + * Requires libcurl for url encoding. + * + * @see rest_decode_post + * + * @param[out] ptr 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_read_t to keep encoding state between calls. + * @return length of data (including NULL) written to ptr, or 0 if no more + * data to write. + */ +static size_t rest_encode_post(void *ptr, size_t size, size_t nmemb, + void *userdata) +{ + rlm_rest_read_t *ctx = userdata; + REQUEST *request = ctx->request; /* Used by RDEBUG */ + VALUE_PAIR **current = ctx->next; + + char *p = ptr; /* Position in buffer */ + char *f = ptr; /* Position in buffer of last fully encoded attribute or value */ + char *escaped; /* Pointer to current URL escaped data */ + + ssize_t len = 0; + ssize_t s = (size * nmemb) - 1; + + /* Allow manual chunking */ + if ((ctx->chunk) && (ctx->chunk <= s)) { + s = (ctx->chunk - 1); + } + + if (ctx->state == READ_STATE_END) return FALSE; + + /* Post data requires no headers */ + if (ctx->state == READ_STATE_INIT) { + ctx->state = READ_STATE_ATTR_BEGIN; + } + + while (s > 0) { + if (!*current) { + ctx->state = READ_STATE_END; + + goto end_chunk; + } + + RDEBUG2("Encoding attribute \"%s\"", current[0]->name); + + if (ctx->state == READ_STATE_ATTR_BEGIN) { + escaped = curl_escape(current[0]->name, + strlen(current[0]->name)); + len = strlen(escaped); + + if (s < (1 + len)) { + curl_free(escaped); + goto no_space; + } + + len = sprintf(p, "%s=", escaped); + + curl_free(escaped); + + p += len; + s -= len; + + /* + * We wrote the attribute header, record progress. + */ + f = p; + ctx->state = READ_STATE_ATTR_CONT; + } + + /* + * Write out single attribute string. + */ + len = vp_prints_value(p , s, current[0], 0); + escaped = curl_escape(p, len); + len = strlen(escaped); + + if (s < len) { + curl_free(escaped); + goto no_space; + } + + len = strlcpy(p, escaped, len + 1); + + curl_free(escaped); + + RDEBUG("\tLength : %i", len); + RDEBUG("\tValue : %s", p); + + p += len; + s -= len; + + if (*++current) { + if (!--s) goto no_space; + *p++ = '&'; + } + + /* + * We wrote one full attribute value pair, record progress. + */ + f = p; + ctx->next = current; + ctx->state = READ_STATE_ATTR_BEGIN; + } + + end_chunk: + + *p = '\0'; + + len = p - (char*)ptr; + + RDEBUG2("POST Data: %s", (char*) ptr); + RDEBUG2("Returning %i bytes of POST data", len); + + return len; + + /* + * Cleanup for error conditions + */ + no_space: + + *f = '\0'; + + len = f - (char*)ptr; + + RDEBUG2("POST Data: %s", (char*) ptr); + + /* + * The buffer wasn't big enough to encode a single attribute chunk. + */ + if (!len) { + radlog(L_ERR, "rlm_rest (%s): AVP exceeds buffer length" + " or chunk", ctx->instance->xlat_name); + } else { + RDEBUG2("Returning %i bytes of POST data" + " (buffer full or chunk exceeded)", len); + } + + return len; +} + +/** Encodes VALUE_PAIR linked list in JSON format + * + * This is a stream function matching the rest_read_t prototype. Multiple + * successive calls will return additional encoded VALUE_PAIRs. + * + * Only complete attribute headers + * @verbatim "":{"type":"","value":[' @endverbatim + * and complete attribute values will be written to ptr. + * + * If an attribute occurs multiple times in the request the attribute values + * will be concatenated into a single value array. + * + * JSON request format is: +@verbatim +{ + "":{ + "type":"", + "value":[,,] + }, + "":{ + "type":"", + "value":[...] + }, + "":{ + "type":"", + "value":[...] + }, +} +@endverbatim + * + * @param[out] ptr 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_read_t to keep encoding state between calls. + * @return length of data (including NULL) written to ptr, or 0 if no more + * data to write. + */ +static size_t rest_encode_json(void *ptr, size_t size, size_t nmemb, + void *userdata) +{ + rlm_rest_read_t *ctx = userdata; + REQUEST *request = ctx->request; /* Used by RDEBUG */ + VALUE_PAIR **current = ctx->next; + + char *p = ptr; /* Position in buffer */ + char *f = ptr; /* Position in buffer of last fully encoded attribute or value */ + + const char *type; + + ssize_t len = 0; + ssize_t s = (size * nmemb) - 1; + + assert(s > 0); + + /* Allow manual chunking */ + if ((ctx->chunk) && (ctx->chunk <= s)) { + s = (ctx->chunk - 1); + } + + if (ctx->state == READ_STATE_END) return FALSE; + + if (ctx->state == READ_STATE_INIT) { + ctx->state = READ_STATE_ATTR_BEGIN; + + if (!--s) goto no_space; + *p++ = '{'; + } + + while (s > 0) { + if (!*current) { + ctx->state = READ_STATE_END; + + if (!--s) goto no_space; + *p++ = '}'; + + goto end_chunk; + } + + /* + * New attribute, write name, type, and beginning of + * value array. + */ + RDEBUG2("Encoding attribute \"%s\"", current[0]->name); + if (ctx->state == READ_STATE_ATTR_BEGIN) { + type = fr_int2str(dict_attr_types, current[0]->type, + "¿Unknown?"); + + len = strlen(type); + len += strlen(current[0]->name); + + if (s < (23 + len)) goto no_space; + + len = sprintf(p, "\"%s\":{\"type\":\"%s\",\"value\":[" , + current[0]->name, type); + p += len; + s -= len; + + RDEBUG2("\tType : %s", type); + + /* + * We wrote the attribute header, record progress + */ + f = p; + ctx->state = READ_STATE_ATTR_CONT; + } + + /* + * Put all attribute values in an array for easier remote + * parsing whether they're multivalued or not. + */ + while (TRUE) { + len = vp_prints_value_json(p , s, current[0]); + assert((s - len) >= 0); + + if (len < 0) goto no_space; + + /* + * Show actual value length minus quotes + */ + RDEBUG2("\tLength : %i", (*p == '"') ? (len - 2) : len); + RDEBUG2("\tValue : %s", p); + + p += len; + s -= len; + + /* + * Multivalued attribute + */ + if (current[1] && + ((current[0]->attribute == current[1]->attribute) && + (current[0]->vendor == current[1]->vendor))) { + *p++ = ','; + current++; + + /* + * We wrote one attribute value, record + * progress. + */ + f = p; + ctx->next = current; + } else { + break; + } + } + + if (!(s -= 2)) goto no_space; + *p++ = ']'; + *p++ = '}'; + + if (*++current) { + if (!--s) goto no_space; + *p++ = ','; + } + + /* + * We wrote one full attribute value pair, record progress. + */ + f = p; + ctx->next = current; + ctx->state = READ_STATE_ATTR_BEGIN; + } + + end_chunk: + + *p = '\0'; + + len = p - (char*)ptr; + + RDEBUG2("JSON Data: %s", (char*) ptr); + RDEBUG2("Returning %i bytes of JSON data", len); + + return len; + + /* + * Were out of buffer space + */ + no_space: + + *f = '\0'; + + len = f - (char*)ptr; + + RDEBUG2("JSON Data: %s", (char*) ptr); + + /* + * The buffer wasn't big enough to encode a single attribute chunk. + */ + if (!len) { + radlog(L_ERR, "rlm_rest (%s): AVP exceeds buffer length" + " or chunk", ctx->instance->xlat_name); + } else { + RDEBUG2("Returning %i bytes of JSON data" + " (buffer full or chunk exceeded)", len); + } + + return len; +} + +/** Emulates successive libcurl calls to an encoding function + * + * This function is used when the request will be sent to the HTTP server as one + * contiguous entity. A buffer of REST_BODY_INCR bytes is allocated and passed + * to the stream encoding function. + * + * If the stream function does not return 0, a new buffer is allocated which is + * the size of the previous buffer + REST_BODY_INCR bytes, the data from the + * previous buffer is copied, and freed, and another call is made to the stream + * function, passing a pointer into the new buffer at the end of the previously + * written data. + * + * This process continues until the stream function signals (by returning 0) + * that it has no more data to write. + * + * @param[out] buffer where the pointer to the malloced buffer should + * be written. + * @param[in] func Stream function. + * @param[in] limit Maximum buffer size to alloc. + * @param[in] userdata rlm_rest_read_t to keep encoding state between calls to + * stream function. + * @return the length of the data written to the buffer (excluding NULL) or -1 + * if alloc >= limit. + */ +static ssize_t rest_read_wrapper(char **buffer, rest_read_t func, + size_t limit, void *userdata) +{ + char *previous = NULL; + char *current; + + size_t alloc = REST_BODY_INCR; /* Size of buffer to malloc */ + size_t used = 0; /* Size of data written */ + size_t len = 0; + + while (alloc < limit) { + current = rad_malloc(alloc); + + if (previous) { + strlcpy(current, previous, used + 1); + free(previous); + } + + len = func(current + used, REST_BODY_INCR, 1, userdata); + used += len; + if (!len) { + *buffer = current; + return used; + } + + alloc += REST_BODY_INCR; + previous = current; + }; + + free(current); + + return -1; +} + +/** (Re-)Initialises the data in a rlm_rest_read_t. + * + * Resets the values of a rlm_rest_read_t to their defaults. + * + * Must be called between encoding sessions. + * + * As part of initialisation all VALUE_PAIR pointers in the REQUEST packet are + * written to an array. + * + * If sort is TRUE, this array of VALUE_PAIR pointers will be sorted by vendor + * and then by attribute. This is for stream encoders which may concatenate + * multiple attribute values together into an array. + * + * After the encoding session has completed this array must be freed by calling + * rest_read_ctx_free . + * + * @see rest_read_ctx_free + * + * @param[in] request Current request. + * @param[in] read to initialise. + * @param[in] sort If TRUE VALUE_PAIRs will be sorted within the VALUE_PAIR + * pointer array. + */ +static void rest_read_ctx_init(REQUEST *request, + rlm_rest_read_t *ctx, + boolean sort) +{ + unsigned short count = 0, i; + unsigned short swap; + + VALUE_PAIR **current, *tmp; + + /* + * Setup stream read data + */ + ctx->request = request; + ctx->state = READ_STATE_INIT; + + /* + * Create sorted array of VP pointers + */ + tmp = request->packet->vps; + while (tmp != NULL) { + tmp = tmp->next; + count++; + } + + ctx->first = current = rad_malloc((sizeof(tmp) * (count + 1))); + ctx->next = ctx->first; + + tmp = request->packet->vps; + while (tmp != NULL) { + *current++ = tmp; + tmp = tmp->next; + } + current[0] = NULL; + current = ctx->first; + + if (!sort || (count < 2)) return; + + /* TODO: Quicksort would be faster... */ + do { + for(i = 1; i < count; i++) { + assert(current[i-1]->attribute && + current[i]->attribute); + + swap = 0; + if ((current[i-1]->vendor > current[i]->vendor) || + ((current[i-1]->vendor == current[i]->vendor) && + (current[i-1]->attribute > current[i]->attribute) + )) { + tmp = current[i]; + current[i] = current[i-1]; + current[i-1] = tmp; + swap = 1; + } + } + } while (swap); +} + +/** Frees the VALUE_PAIR array created by rest_read_ctx_init. + * + * Must be called between encoding sessions else module will leak VALUE_PAIR + * pointers. + * + * @see rest_read_ctx_init + * + * @param[in] read to free. + */ +static void rest_read_ctx_free(rlm_rest_read_t *ctx) +{ + if (ctx->first != NULL) { + free(ctx->first); + } +} + +/** Verify that value wasn't truncated when it was converted to a VALUE_PAIR + * + * Certain values may be truncated when they're converted into VALUE_PAIRs + * for example 64bit integers converted to 32bit integers. Warn the user + * when this happens. + * + * @param[in] raw string from decoder. + * @param[in] vp containing parsed value. + */ +static void rest_check_truncation(REQUEST *request, const char *raw, + VALUE_PAIR *vp) +{ + char cooked[1024]; + + vp_prints_value(cooked, sizeof(cooked), vp, 0); + if (strcmp(raw, cooked) != 0) { + RDEBUG("WARNING: Value-Pair does not match POST value, " + "truncation may have occurred"); + RDEBUG("\tValue (pair) : \"%s\"", cooked); + RDEBUG("\tValue (post) : \"%s\"", raw); + } +} + +/** Converts POST response into VALUE_PAIRs and adds them to the request + * + * Accepts VALUE_PAIRS in the same format as rest_encode_post, but with the + * addition of optional attribute list qualifiers as part of the attribute name + * string. + * + * If no qualifiers are specified, will default to the request list. + * + * POST response format is: + * @verbatim [outer.][:]=&[outer.][:]=&[outer.][:]= @endverbatim + * + * @see rest_encode_post + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] handle rlm_rest_handle_t to use. + * @param[in] request Current request. + * @param[in] raw buffer containing POST data. + * @param[in] rawlen Length of data in raw buffer. + * @return the number of VALUE_PAIRs processed or -1 on unrecoverable error. + */ +static int rest_decode_post(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, + REQUEST *request, void *handle, char *raw, + UNUSED size_t rawlen) +{ + rlm_rest_handle_t *randle = handle; + CURL *candle = randle->handle; + + const char *p = raw, *q; + + const char *attribute; + char *name = NULL; + char *value = NULL; + + const DICT_ATTR *da; + VALUE_PAIR *vp; + + const DICT_ATTR **current, *processed[REST_BODY_MAX_ATTRS + 1]; + VALUE_PAIR *tmp; + + pair_lists_t list; + REQUEST *reference = request; + VALUE_PAIR **vps; + + size_t len; + int curl_len; /* Length from last curl_easy_unescape call */ + + int count = 0; + + processed[0] = NULL; + + /* + * Empty response? + */ + while (isspace(*p)) p++; + + if (p == NULL) return FALSE; + + while (((q = strchr(p, '=')) != NULL) && + (count < REST_BODY_MAX_ATTRS)) { + attribute = name; + reference = request; + + name = curl_easy_unescape(candle, p, (q - p), &curl_len); + p = (q + 1); + + RDEBUG("Decoding attribute \"%s\"", name); + + if (!radius_ref_request(&reference, &attribute)) { + RDEBUG("WARNING: Attribute name refers to outer request" + " but not in a tunnel, skipping"); + + curl_free(name); + + continue; + } + + list = radius_list_name(&attribute, PAIR_LIST_REPLY); + if (list == PAIR_LIST_UNKNOWN) { + RDEBUG("WARNING: Invalid list qualifier, skipping"); + + curl_free(name); + + continue; + } + + da = dict_attrbyname(attribute); + if (!da) { + RDEBUG("WARNING: Attribute \"%s\" unknown, skipping", + attribute); + + curl_free(name); + + continue; + } + + vps = radius_list(reference, list); + + assert(vps); + + RDEBUG2("\tType : %s", fr_int2str(dict_attr_types, da->type, + "¿Unknown?")); + + q = strchr(p, '&'); + len = (q == NULL) ? (rawlen - (p - raw)) : (unsigned)(q - p); + + value = curl_easy_unescape(candle, p, len, &curl_len); + + /* + * If we found a delimiter we want to skip over it, + * if we didn't we do *NOT* want to skip over the end + * of the buffer... + */ + p += (q == NULL) ? len : (len + 1); + + RDEBUG2("\tLength : %i", curl_len); + RDEBUG2("\tValue : \"%s\"", value); + + vp = paircreate(da->attr, da->vendor, da->type); + if (!vp) { + radlog(L_ERR, "rlm_rest (%s): Failed creating" + " value-pair", instance->xlat_name); + + goto error; + } + + vp->operator = T_OP_SET; + + /* + * Check to see if we've already processed an + * attribute of the same type if we have, change the op + * from T_OP_ADD to T_OP_SET. + */ + current = processed; + while (*current++) { + if ((current[0]->attr == da->attr) && + (current[0]->vendor == da->vendor)) { + vp->operator = T_OP_ADD; + break; + } + } + + if (vp->operator != T_OP_ADD) { + current[0] = da; + current[1] = NULL; + } + + tmp = pairparsevalue(vp, value); + if (tmp == NULL) { + RDEBUG("Incompatible value assignment, skipping"); + pairbasicfree(vp); + goto skip; + } + vp = tmp; + + rest_check_truncation(request, value, vp); + + vp->flags.do_xlat = 1; + + RDEBUG("Performing xlat expansion of response value", value); + pairxlatmove(request, vps, &vp); + + if (++count == REST_BODY_MAX_ATTRS) { + radlog(L_ERR, "rlm_rest (%s): At maximum" + " attribute limit", instance->xlat_name); + return count; + } + + skip: + + curl_free(name); + curl_free(value); + + continue; + + error: + + curl_free(name); + curl_free(value); + + return count; + } + + if (!count) { + radlog(L_ERR, "rlm_rest (%s): Malformed POST data \"%s\"", + instance->xlat_name, raw); + } + + return count; + +} + +/** Converts JSON "value" key into VALUE_PAIR. + * + * If leaf is not in fact a leaf node, but contains JSON data, the data will + * written to the attribute in JSON string format. + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] request Current request. + * @param[in] attribute name without qualifiers. + * @param[in] flags containing the operator other flags controlling value + * expansion. + * @param[in] leaf object containing the VALUE_PAIR value. + * @return The VALUE_PAIR just created, or NULL on error. + */ +static VALUE_PAIR *json_pairmake_leaf(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, + REQUEST *request, const DICT_ATTR *da, + json_flags_t *flags, json_object *leaf) +{ + const char *value; + VALUE_PAIR *vp, *tmp; + + /* + * Should encode any nested JSON structures into JSON strings. + * + * "I knew you liked JSON so I put JSON in your JSON!" + */ + value = json_object_get_string(leaf); + + RDEBUG2("\tType : %s", fr_int2str(dict_attr_types, da->type, + "¿Unknown?")); + RDEBUG2("\tLength : %i", strlen(value)); + RDEBUG2("\tValue : \"%s\"", value); + + vp = paircreate(da->attr, da->vendor, da->type); + if (!vp) { + radlog(L_ERR, "rlm_rest (%s): Failed creating value-pair", + instance->xlat_name); + return NULL; + } + + vp->operator = flags->operator; + + tmp = pairparsevalue(vp, value); + if (tmp == NULL) { + RDEBUG("Incompatible value assignment, skipping"); + pairbasicfree(vp); + return NULL; + } + vp = tmp; + + rest_check_truncation(request, value, vp); + + if (flags->do_xlat) vp->flags.do_xlat = 1; + + return vp; +} + +/** Processes JSON response and converts it into multiple VALUE_PAIRs + * + * Processes JSON attribute declarations in the format below. Will recurse when + * processing nested attributes. When processing nested attributes flags and + * operators from previous attributes are not inherited. + * + * JSON response format is: +@verbatim +{ + "":{ + do_xlat:, + is_json:, + "op":"", + "value":[,,] + }, + "":{ + "value":{ + "":{ + "op":"", + "value": + } + } + }, + "":"", + "":"[,,]" +} +@endverbatim + * + * JSON valuepair flags (bools): + * - do_xlat (optional) Controls xlat expansion of values. Defaults to TRUE. + * - is_json (optional) If TRUE, any nested JSON data will be copied to the + * VALUE_PAIR in string form. Defaults to TRUE. + * - op (optional) Controls how the attribute is inserted into + * the target list. Defaults to ':=' (T_OP_SET). + * + * If "op" is ':=' or '=', it will be automagically changed to '+=' for the + * second and subsequent values in multivalued attributes. This does not work + * between multiple attribute declarations. + * + * @see fr_tokens + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] request Current request. + * @param[in] object containing root node, or parent node. + * @param[in] level Current nesting level. + * @param[in] max_attrs counter, decremented after each VALUE_PAIR is created, + * when 0 no more attributes will be processed. + * @return VALUE_PAIR or NULL on error. + */ +static VALUE_PAIR *json_pairmake(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, + REQUEST *request, json_object *object, + int level, int *max_attrs) +{ + const char *p; + char *q; + + const char *name, *attribute; + + struct json_object *value, *idx, *tmp; + struct lh_entry *entry; + json_flags_t flags; + + const DICT_ATTR *da; + VALUE_PAIR *vp; + + pair_lists_t list; + REQUEST *reference = request; + VALUE_PAIR **vps; + + int i, len; + + if (!json_object_is_type(object, json_type_object)) { + RDEBUG("Can't process VP container, expected JSON object," + " got \"%s\", skipping", + json_object_get_type(object)); + return NULL; + } + + /* + * Process VP container + */ + entry = json_object_get_object(object)->head; + while (entry) { + flags.operator = T_OP_SET; + flags.do_xlat = 1; + flags.is_json = 0; + + name = (char*)entry->k; + + /* Fix the compiler warnings regarding const... */ + memcpy(&value, &entry->v, sizeof(value)); + + entry = entry->next; + + /* + * For people handcrafting JSON responses + */ + p = name; + while ((p = q = strchr(p, '|'))) { + *q = ':'; + p++; + } + + attribute = name; + reference = request; + + /* + * Resolve attribute name to a dictionary entry and + * pairlist. + */ + RDEBUG2("Decoding attribute \"%s\"", name); + + if (!radius_ref_request(&reference, &attribute)) { + RDEBUG("WARNING: Attribute name refers to outer request" + " but not in a tunnel, skipping"); + + continue; + } + + list = radius_list_name(&attribute, PAIR_LIST_REPLY); + if (list == PAIR_LIST_UNKNOWN) { + RDEBUG("WARNING: Invalid list qualifier, skipping"); + + continue; + } + + da = dict_attrbyname(attribute); + if (!da) { + RDEBUG("WARNING: Attribute \"%s\" unknown, skipping", + attribute); + + continue; + } + + vps = radius_list(reference, list); + + assert(vps); + + /* + * Alternate JSON structure that allows operator, + * and other flags to be specified. + * + * "":{ + * "do_xlat":, + * "is_json":, + * "op":"", + * "value": + * } + * + * Where value is a: + * - [] Multivalued array + * - {} Nested Valuepair + * - * Integer or string value + */ + if (json_object_is_type(value, json_type_object)) { + /* + * Process operator if present. + */ + tmp = json_object_object_get(value, "op"); + if (tmp) { + flags.operator = fr_str2int(fr_tokens, + json_object_get_string(tmp), 0); + + if (!flags.operator) { + RDEBUG("Invalid operator value \"%s\"," + " skipping", tmp); + continue; + } + } + + /* + * Process optional do_xlat bool. + */ + tmp = json_object_object_get(value, "do_xlat"); + if (tmp) { + flags.do_xlat = json_object_get_boolean(tmp); + } + + /* + * Process optional is_json bool. + */ + tmp = json_object_object_get(value, "is_json"); + if (tmp) { + flags.is_json = json_object_get_boolean(tmp); + } + + /* + * Value key must be present if were using + * the expanded syntax. + */ + value = json_object_object_get(value, "value"); + if (!value) { + RDEBUG("Value key missing, skipping", value); + continue; + } + } + + /* + * Setup pairmake / recursion loop. + */ + if (!flags.is_json && + json_object_is_type(value, json_type_array)) { + len = json_object_array_length(value); + if (!len) { + RDEBUG("Zero length value array, skipping", value); + continue; + } + idx = json_object_array_get_idx(value, 0); + } else { + len = 1; + idx = value; + } + + i = 0; + do { + if (!(*max_attrs)--) { + radlog(L_ERR, "rlm_rest (%s): At maximum" + " attribute limit", instance->xlat_name); + return NULL; + } + + /* + * Automagically switch the op for multivalued + * attributes. + */ + if (((flags.operator == T_OP_SET) || + (flags.operator == T_OP_EQ)) && (len > 1)) { + flags.operator = T_OP_ADD; + } + + if (!flags.is_json && + json_object_is_type(value, json_type_object)) { + /* TODO: Insert nested VP into VP structure...*/ + RDEBUG("Found nested VP", value); + vp = json_pairmake(instance, section, + request, value, + level + 1, max_attrs); + } else { + vp = json_pairmake_leaf(instance, section, + request, da, &flags, + idx); + + if (vp != NULL) { + if (vp->flags.do_xlat) { + RDEBUG("Performing xlat" + " expansion of response" + " value", value); + } + + pairxlatmove(request, vps, &vp); + } + } + } while ((++i < len) && (idx = json_object_array_get_idx(value, i))); + } + + return vp; +} + +/** Converts JSON response into VALUE_PAIRs and adds them to the request. + * + * Converts the raw JSON string into a json-c object tree and passes it to + * json_pairmake. After the tree has been parsed json_object_put is called + * which decrements the reference, count to the root node by one, and frees + * the entire tree. + * + * @see rest_encode_json + * @see json_pairmake + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] g to use. + * @param[in] request Current request. + * @param[in] raw buffer containing JSON data. + * @param[in] rawlen Length of data in raw buffer. + * @return the number of VALUE_PAIRs processed or -1 on unrecoverable error. + */ +static int rest_decode_json(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, + UNUSED REQUEST *request, UNUSED void *handle, + char *raw, UNUSED size_t rawlen) +{ + const char *p = raw; + + struct json_object *json; + + int max = REST_BODY_MAX_ATTRS; + + /* + * Empty response? + */ + while (isspace(*p)) p++; + if (p == NULL) return FALSE; + + json = json_tokener_parse(p); + if (!json) { + radlog(L_ERR, "rlm_rest (%s): Malformed JSON data \"%s\"", + instance->xlat_name, raw); + return -1; + } + + json_pairmake(instance, section, request, json, 0, &max); + + /* + * Decrement reference count for root object, should free entire + * JSON tree. + */ + json_object_put(json); + + return (REST_BODY_MAX_ATTRS - max); +} + +/** Processes incoming HTTP header data from libcurl. + * + * Processes the status line, and Content-Type headers from the incoming HTTP + * response. + * + * Matches prototype for CURLOPT_HEADERFUNCTION, and will be called directly + * by libcurl. + * + * @param[in] ptr Char buffer where inbound header data is written. + * @param[in] size Multiply by nmemb to get the length of ptr. + * @param[in] nmemb Multiply by size to get the length of ptr. + * @param[in] userdata rlm_rest_write_t to keep parsing state between calls. + * @return Length of data processed, or 0 on error. + */ +static size_t rest_write_header(void *ptr, size_t size, size_t nmemb, + void *userdata) +{ + rlm_rest_write_t *ctx = userdata; + REQUEST *request = ctx->request; /* Used by RDEBUG */ + + const char *p = ptr, *q; + char *tmp; + + const size_t t = (size * nmemb); + size_t s = t; + size_t len; + + http_body_type_t type; + http_body_type_t supp; + + switch (ctx->state) + { + case WRITE_STATE_INIT: + RDEBUG("Processing header"); + + /* + * HTTP/ [ ]\r\n + * + * "HTTP/1.1 " (8) + "100 " (4) + "\r\n" (2) = 14 + */ + if (s < 14) goto malformed; + + /* + * Check start of header matches... + */ + if (strncasecmp("HTTP/", p, 5) != 0) goto malformed; + + p += 5; + s -= 5; + + /* + * Skip the version field, next space should mark start + * of reason_code. + */ + q = memchr(p, ' ', s); + if (q == NULL) goto malformed; + + s -= (q - p); + p = q; + + /* + * Process reason_code. + * + * " 100" (4) + "\r\n" (2) = 6 + */ + if (s < 6) goto malformed; + p++; + s--; + + /* Char after reason code must be a space, or \r */ + if (!((p[3] == ' ') || (p[3] == '\r'))) goto malformed; + + ctx->code = atoi(p); + + /* + * Process reason_phrase (if present). + */ + if (p[3] == ' ') { + p += 4; + s -= 4; + + q = memchr(p, '\r', s); + if (q == NULL) goto malformed; + + len = (q - p); + + tmp = rad_malloc(len + 1); + strlcpy(tmp, p, len + 1); + + RDEBUG("\tStatus : %i (%s)", ctx->code, tmp); + + free(tmp); + } else { + RDEBUG("\tStatus : %i", ctx->code); + } + + ctx->state = WRITE_STATE_PARSE_HEADERS; + + break; + + case WRITE_STATE_PARSE_HEADERS: + if ((s >= 14) && + (strncasecmp("Content-Type: ", p, 14) == 0)) { + p += 14; + s -= 14; + + /* + * Check to see if there's a parameter + * separator. + */ + q = memchr(p, ';', s); + + /* + * If there's not, find the end of this + * header. + */ + if (q == NULL) q = memchr(p, '\r', s); + + len = (q == NULL) ? s : (unsigned)(q - p); + + type = fr_substr2int(http_content_type_table, + p, HTTP_BODY_UNKNOWN, + len); + + supp = http_body_type_supported[type]; + + tmp = rad_malloc(len + 1); + strlcpy(tmp, p, len + 1); + + RDEBUG("\tType : %s (%s)", + fr_int2str(http_body_type_table, type, + "¿Unknown?"), tmp); + + free(tmp); + + if (type == HTTP_BODY_UNKNOWN) { + RDEBUG("Couldn't determine type, using" + " request type \"%s\".", + fr_int2str(http_body_type_table, + ctx->type, + "¿Unknown?")); + + } else if (supp == HTTP_BODY_UNSUPPORTED) { + RDEBUG("Type \"%s\" is currently" + " unsupported", + fr_int2str(http_body_type_table, + type, "¿Unknown?")); + ctx->type = HTTP_BODY_UNSUPPORTED; + + } else if (supp == HTTP_BODY_INVALID) { + RDEBUG("Type \"%s\" is not a valid web" + " API data markup format", + fr_int2str(http_body_type_table, + type, "¿Unknown?")); + + ctx->type = HTTP_BODY_INVALID; + + } else if (type != ctx->type) { + ctx->type = type; + } + } + break; + + default: + break; + } + return t; + + malformed: + + RDEBUG("Incoming header was malformed"); + ctx->code = -1; + + return (t - s); +} + +/** Processes incoming HTTP body data from libcurl. + * + * Writes incoming body data to an intermediary buffer for later parsing by + * one of the decode functions. + * + * @param[in] ptr Char buffer where inbound header data is written + * @param[in] size Multiply by nmemb to get the length of ptr. + * @param[in] nmemb Multiply by size to get the length of ptr. + * @param[in] userdata rlm_rest_write_t to keep parsing state between calls. + * @return length of data processed, or 0 on error. + */ +static size_t rest_write_body(void *ptr, size_t size, size_t nmemb, + void *userdata) +{ + rlm_rest_write_t *ctx = userdata; + REQUEST *request = ctx->request; /* Used by RDEBUG */ + + const char *p = ptr; + char *tmp; + + const size_t t = (size * nmemb); + + /* + * Any post processing of headers should go here... + */ + if (ctx->state == WRITE_STATE_PARSE_HEADERS) { + ctx->state = WRITE_STATE_PARSE_CONTENT; + } + + switch (ctx->type) + { + case HTTP_BODY_UNSUPPORTED: + return t; + + case HTTP_BODY_INVALID: + tmp = rad_malloc(t + 1); + strlcpy(tmp, p, t + 1); + + RDEBUG2("%s", tmp); + + free(tmp); + + return t; + + default: + if (t > (ctx->alloc - ctx->used)) { + ctx->alloc += ((t + 1) > REST_BODY_INCR) ? + t + 1 : REST_BODY_INCR; + + tmp = ctx->buffer; + + ctx->buffer = rad_malloc(ctx->alloc); + + /* If data has been written previously */ + if (tmp) { + strlcpy(ctx->buffer, tmp, + (ctx->used + 1)); + free(tmp); + } + } + strlcpy(ctx->buffer + ctx->used, p, t + 1); + ctx->used += t; + + break; + } + + return t; +} + +/** (Re-)Initialises the data in a rlm_rest_write_t. + * + * This resets the values of the a rlm_rest_write_t to their defaults. + * Must be called between encoding sessions. + * + * @see rest_write_body + * @see rest_write_header + * + * @param[in] request Current request. + * @param[in] data to initialise. + * @param[in] type Default http_body_type to use when decoding raw data, may be + * overwritten by rest_write_header. + */ +static void rest_write_ctx_init(REQUEST *request, rlm_rest_write_t *ctx, + http_body_type_t type) +{ + ctx->request = request; + ctx->type = type; + ctx->state = WRITE_STATE_INIT; + ctx->alloc = 0; + ctx->used = 0; + ctx->buffer = NULL; +} + +/** Frees the intermediary buffer created by rest_write. + * + * @param[in] data to be freed. + */ +static void rest_write_free(rlm_rest_write_t *ctx) +{ + if (ctx->buffer != NULL) { + free(ctx->buffer); + } +} + +/** Configures body specific curlopts. + * + * Configures libcurl handle to use either chunked mode, where the request + * data will be sent using multiple HTTP requests, or contiguous mode where + * the request data will be sent in a single HTTP request. + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] handle rlm_rest_handle_t to configure. + * @param[in] func to pass to libcurl for chunked. + * transfers (NULL if not using chunked mode). + * @return TRUE on success FALSE on error. + */ +static int rest_request_config_body(rlm_rest_t *instance, + rlm_rest_section_t *section, + rlm_rest_handle_t *handle, + rest_read_t func) +{ + rlm_rest_curl_context_t *ctx = handle->ctx; + CURL *candle = handle->handle; + + ssize_t len; + CURLcode ret; + + if (section->chunk > 0) { + ret = curl_easy_setopt(candle, CURLOPT_READDATA, + &ctx->read); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_READFUNCTION, + rest_encode_json); + if (ret != CURLE_OK) goto error; + } else { + len = rest_read_wrapper(&ctx->body, func, + REST_BODY_MAX_LEN , &ctx->read); + if (len <= 0) { + radlog(L_ERR, "rlm_rest (%s): Failed creating HTTP" + " body content", instance->xlat_name); + return FALSE; + } + + ret = curl_easy_setopt(candle, CURLOPT_POSTFIELDS, + ctx->body); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_POSTFIELDSIZE, + len); + if (ret != CURLE_OK) goto error; + } + + return TRUE; + + error: + radlog(L_ERR, "rlm_rest (%s): Failed setting curl option: %i - %s", + instance->xlat_name, ret, curl_easy_strerror(ret)); + + return FALSE; +} + +/** Configures request curlopts. + * + * Configures libcurl handle setting various curlopts for things like local + * client time, Content-Type, and other FreeRADIUS custom headers. + * + * Current FreeRADIUS custom headers are: + * - X-FreeRADIUS-Section The module section being processed. + * - X-FreeRADIUS-Server The current virtual server the REQUEST is + * passing through. + * + * Sets up callbacks for all response processing (buffers and body data). + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] handle to configure. + * @param[in] request Current request. + * @param[in] method to use (HTTP verbs PUT, POST, DELETE etc...). + * @param[in] type Content-Type for request encoding, also sets the default + * for decoding. + * @param[in] uri buffer containing the expanded URI to send the request to. + * @return TRUE on success (all opts configured) FALSE on error. + */ +int rest_request_config(rlm_rest_t *instance, rlm_rest_section_t *section, + REQUEST *request, void *handle, http_method_t method, + http_body_type_t type, char *uri) +{ + rlm_rest_handle_t *randle = handle; + rlm_rest_curl_context_t *ctx = randle->ctx; + CURL *candle = randle->handle; + + http_auth_type_t auth = section->auth; + + CURLcode ret; + long val = 1; + + char buffer[512]; + + buffer[(sizeof(buffer) - 1)] = '\0'; + + /* + * Setup any header options and generic headers. + */ + ret = curl_easy_setopt(candle, CURLOPT_URL, uri); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_USERAGENT, "FreeRADIUS"); + if (ret != CURLE_OK) goto error; + + snprintf(buffer, (sizeof(buffer) - 1), "Content-Type: %s", + fr_int2str(http_content_type_table, type, "¿Unknown?")); + ctx->headers = curl_slist_append(ctx->headers, buffer); + if (!ctx->headers) goto error_header; + + if (section->timeout) { + ret = curl_easy_setopt(candle, CURLOPT_TIMEOUT, + section->timeout); + if (ret != CURLE_OK) goto error; + } + + ret = curl_easy_setopt(candle, CURLOPT_PROTOCOLS, + (CURLPROTO_HTTP | CURLPROTO_HTTPS)); + if (ret != CURLE_OK) goto error; + + /* + * FreeRADIUS custom headers + */ + snprintf(buffer, (sizeof(buffer) - 1), "X-FreeRADIUS-Section: %s", + section->name); + ctx->headers = curl_slist_append(ctx->headers, buffer); + if (!ctx->headers) goto error_header; + + snprintf(buffer, (sizeof(buffer) - 1), "X-FreeRADIUS-Server: %s", + request->server); + ctx->headers = curl_slist_append(ctx->headers, buffer); + if (!ctx->headers) goto error_header; + + /* + * Configure HTTP verb (GET, POST, PUT, DELETE, other...) + */ + switch (method) + { + case HTTP_METHOD_GET : + ret = curl_easy_setopt(candle, CURLOPT_HTTPGET, + val); + if (ret != CURLE_OK) goto error; + + break; + + case HTTP_METHOD_POST : + ret = curl_easy_setopt(candle, CURLOPT_POST, + val); + if (ret != CURLE_OK) goto error; + + break; + + case HTTP_METHOD_PUT : + ret = curl_easy_setopt(candle, CURLOPT_PUT, + val); + if (ret != CURLE_OK) goto error; + + break; + + case HTTP_METHOD_DELETE : + ret = curl_easy_setopt(candle, CURLOPT_HTTPGET, + val); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, + CURLOPT_CUSTOMREQUEST, "DELETE"); + if (ret != CURLE_OK) goto error; + + break; + + case HTTP_METHOD_CUSTOM : + ret = curl_easy_setopt(candle, CURLOPT_HTTPGET, + val); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, + CURLOPT_CUSTOMREQUEST, + section->method); + if (ret != CURLE_OK) goto error; + + default: + assert(0); + break; + }; + + /* + * Set user based authentication parameters + */ + if (auth) { + if ((auth >= HTTP_AUTH_BASIC) && + (auth <= HTTP_AUTH_ANY_SAFE)) { + ret = curl_easy_setopt(candle, CURLOPT_HTTPAUTH, + http_curl_auth[auth]); + if (ret != CURLE_OK) goto error; + + if (section->username) { + radius_xlat(buffer, sizeof(buffer), + section->username, request, NULL); + + ret = curl_easy_setopt(candle, CURLOPT_USERNAME, + buffer); + if (ret != CURLE_OK) goto error; + } + if (section->password) { + radius_xlat(buffer, sizeof(buffer), + section->password, request, NULL); + + ret = curl_easy_setopt(candle, CURLOPT_PASSWORD, + buffer); + if (ret != CURLE_OK) goto error; + } + } else if (type == HTTP_AUTH_TLS_SRP) { + ret = curl_easy_setopt(candle, CURLOPT_TLSAUTH_TYPE, + http_curl_auth[auth]); + + if (section->username) { + radius_xlat(buffer, sizeof(buffer), + section->username, request, NULL); + + ret = curl_easy_setopt(candle, + CURLOPT_TLSAUTH_USERNAME, + buffer); + if (ret != CURLE_OK) goto error; + } + if (section->password) { + radius_xlat(buffer, sizeof(buffer), + section->password, request, NULL); + + ret = curl_easy_setopt(candle, + CURLOPT_TLSAUTH_PASSWORD, + buffer); + if (ret != CURLE_OK) goto error; + } + + } + } + + /* + * Set SSL authentication parameters + */ + if (section->certificate_file) { + ret = curl_easy_setopt(candle, + CURLOPT_SSLCERT, + section->certificate_file); + if (ret != CURLE_OK) goto error; + } + + if (section->file_type == FALSE) { + ret = curl_easy_setopt(candle, + CURLOPT_SSLCERT, + "DER"); + if (ret != CURLE_OK) goto error; + } + + if (section->private_key_file) { + ret = curl_easy_setopt(candle, + CURLOPT_SSLCERT, + section->private_key_file); + if (ret != CURLE_OK) goto error; + } + + if (section->private_key_password) { + ret = curl_easy_setopt(candle, + CURLOPT_KEYPASSWD, + section->private_key_password); + if (ret != CURLE_OK) goto error; + } + + if (section->ca_file) { + ret = curl_easy_setopt(candle, + CURLOPT_ISSUERCERT, + section->ca_file); + if (ret != CURLE_OK) goto error; + } + + if (section->ca_path) { + ret = curl_easy_setopt(candle, + CURLOPT_CAPATH, + section->ca_path); + if (ret != CURLE_OK) goto error; + } + + if (section->random_file) { + ret = curl_easy_setopt(candle, + CURLOPT_RANDOM_FILE, + section->random_file); + if (ret != CURLE_OK) goto error; + } + + ret = curl_easy_setopt(candle, + CURLOPT_SSL_VERIFYHOST, + (section->check_cert_cn == TRUE) ? + 2 : 0); + if (ret != CURLE_OK) goto error; + + /* + * Tell CURL how to get HTTP body content, and how to process + * incoming data. + */ + rest_write_ctx_init(request, &ctx->write, type); + + ret = curl_easy_setopt(candle, CURLOPT_HEADERFUNCTION, + rest_write_header); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_HEADERDATA, + &ctx->write); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_WRITEFUNCTION, + rest_write_body); + if (ret != CURLE_OK) goto error; + + ret = curl_easy_setopt(candle, CURLOPT_WRITEDATA, + &ctx->write); + if (ret != CURLE_OK) goto error; + + switch (method) + { + case HTTP_METHOD_GET : + case HTTP_METHOD_DELETE : + return FALSE; + break; + + case HTTP_METHOD_POST : + case HTTP_METHOD_PUT : + case HTTP_METHOD_CUSTOM : + if (section->chunk > 0) { + ctx->read.chunk = section->chunk; + + ctx->headers = curl_slist_append(ctx->headers, + "Expect:"); + if (!ctx->headers) goto error_header; + + ctx->headers = curl_slist_append(ctx->headers, + "Transfer-Encoding: chunked"); + if (!ctx->headers) goto error_header; + } + + switch (type) + { + case HTTP_BODY_JSON: + rest_read_ctx_init(request, + &ctx->read, 1); + + ret = rest_request_config_body(instance, + section, + handle, + rest_encode_json); + if (!ret) return -1; + + break; + + case HTTP_BODY_POST: + rest_read_ctx_init(request, + &ctx->read, 0); + + ret = rest_request_config_body(instance, + section, + handle, + rest_encode_post); + if (!ret) return -1; + + break; + + default: + assert(0); + } + + ret = curl_easy_setopt(candle, CURLOPT_HTTPHEADER, + ctx->headers); + if (ret != CURLE_OK) goto error; + + break; + + default: + assert(0); + }; + + return TRUE; + + error: + radlog(L_ERR, "rlm_rest (%s): Failed setting curl option: %i - %s", + instance->xlat_name, ret, curl_easy_strerror(ret)); + return FALSE; + + error_header: + radlog(L_ERR, "rlm_rest (%s): Failed creating header", + instance->xlat_name); + return FALSE; +} + +/** Sends a REST (HTTP) request. + * + * Send the actual REST request to the server. The response will be handled by + * the numerous callbacks configured in rest_request_config. + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] handle to use. + * @return TRUE on success or FALSE on error. + */ +int rest_request_perform(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, void *handle) +{ + rlm_rest_handle_t *randle = handle; + CURL *candle = randle->handle; + CURLcode ret; + + ret = curl_easy_perform(candle); + if (ret != CURLE_OK) { + radlog(L_ERR, "rlm_rest (%s): Request failed: %i - %s", + instance->xlat_name, ret, curl_easy_strerror(ret)); + return FALSE; + } + + return TRUE; +} + +/** Sends the response to the correct decode function. + * + * Uses the Content-Type information written in rest_write_header to + * determine the correct decode function to use. The decode function will + * then convert the raw received data into VALUE_PAIRs. + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] request Current request. + * @param[in] handle to use. + * @return TRUE on success or FALSE on error. + */ +int rest_request_decode(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, + REQUEST *request, void *handle) +{ + rlm_rest_handle_t *randle = handle; + rlm_rest_curl_context_t *ctx = randle->ctx; + + int ret; + + if (ctx->write.buffer == NULL) { + RDEBUG("Skipping attribute processing, no body data received"); + return FALSE; + } + + RDEBUG("Processing body", ret); + + switch (ctx->write.type) + { + case HTTP_BODY_POST: + ret = rest_decode_post(instance, section, request, + handle, ctx->write.buffer, + ctx->write.used); + break; + + case HTTP_BODY_JSON: + ret = rest_decode_json(instance, section, request, + handle, ctx->write.buffer, + ctx->write.used); + break; + + case HTTP_BODY_UNSUPPORTED: + case HTTP_BODY_INVALID: + return -1; + + default: + assert(0); + } + + return ret; +} + +/** Cleans up after a REST request. + * + * Resets all options associated with a CURL handle, and frees any headers + * associated with it. + * + * Calls rest_read_ctx_free and rest_write_free to free any memory used by + * context data. + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] handle to cleanup. + * @return TRUE on success or FALSE on error. + */ +void rest_request_cleanup(UNUSED rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, void *handle) +{ + rlm_rest_handle_t *randle = handle; + rlm_rest_curl_context_t *ctx = randle->ctx; + CURL *candle = randle->handle; + + /* + * Clear any previously configured options + */ + curl_easy_reset(candle); + + /* + * Free header list + */ + if (ctx->headers != NULL) { + curl_slist_free_all(ctx->headers); + ctx->headers = NULL; + } + + /* + * Free body data (only used if chunking is disabled) + */ + if (ctx->body != NULL) free(ctx->body); + + /* + * Free other context info + */ + rest_read_ctx_free(&ctx->read); + rest_write_free(&ctx->write); +} + +/** URL encodes a string. + * + * Encode special chars as per RFC 3986 section 4. + * + * @param[out] out Where to write escaped string. + * @param[in] outlen Size of out buffer. + * @param[in] raw string to be urlencoded. + * @return length of data written to out (excluding NULL). + */ +static size_t rest_uri_escape(char *out, size_t outlen, const char *raw) +{ + char *escaped; + + escaped = curl_escape(raw, strlen(raw)); + strlcpy(out, escaped, outlen); + curl_free(escaped); + + return strlen(out); +} + +/** Builds URI; performs XLAT expansions and encoding. + * + * Splits the URI into "http://example.org" and "/%{xlat}/query/?bar=foo" + * Both components are expanded, but values expanded for the second component + * are also url encoded. + * + * @param[in] instance configuration data. + * @param[in] section configuration data. + * @param[in] request Current request + * @param[out] buffer to write expanded URI to. + * @param[in] bufsize Size of buffer. + * @return length of data written to buffer (excluding NULL) or < 0 if an error + * occurred. + */ +ssize_t rest_uri_build(rlm_rest_t *instance, rlm_rest_section_t *section, + REQUEST *request, char *buffer, size_t bufsize) +{ + const char *p, *q; + + char *out, *scheme; + const char *path; + + unsigned short count = 0; + + size_t len; + + p = section->uri; + + while ((q = strchr(p, '/')) && (count++ < 3)) p = (q + 1); + + if (count != 3) { + radlog(L_ERR, "rlm_rest (%s): Error URI is malformed," + " can't find start of path", instance->xlat_name); + return -1; + } + + len = (q - p); + + scheme = rad_malloc(len + 1); + strlcpy(scheme, section->uri, len + 1); + + path = (q + 1); + + out = buffer; + out += radius_xlat(out, bufsize, scheme, request, NULL); + + free(scheme); + + out += radius_xlat(out, (bufsize - (buffer - out)), path, request, + rest_uri_escape); + + return (buffer - out); +} \ No newline at end of file diff --git a/src/modules/rlm_rest/rest.h b/src/modules/rlm_rest/rest.h new file mode 100644 index 00000000000..a95de968c66 --- /dev/null +++ b/src/modules/rlm_rest/rest.h @@ -0,0 +1,250 @@ +/** Function prototypes datatypes for the REST (HTTP) transport. + * + * @file rest.h + * + * Version: $Id$ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Copyright 2011 Arran Cudbard-Bell + */ +#include +#include + +RCSIDH(other_h, "$Id$") + + +#define REST_URI_MAX_LEN 2048 +#define REST_BODY_MAX_LEN 8192 +#define REST_BODY_INCR 512 +#define REST_BODY_MAX_ATTRS 256 + +typedef enum { + HTTP_METHOD_CUSTOM, + HTTP_METHOD_GET, + HTTP_METHOD_POST, + HTTP_METHOD_PUT, + HTTP_METHOD_DELETE +} http_method_t; + +typedef enum { + HTTP_BODY_UNKNOWN = 0, + HTTP_BODY_UNSUPPORTED, + HTTP_BODY_INVALID, + HTTP_BODY_POST, + HTTP_BODY_JSON, + HTTP_BODY_XML, + HTTP_BODY_YAML, + HTTP_BODY_HTML, + HTTP_BODY_PLAIN, + HTTP_BODY_NUM_ENTRIES +} http_body_type_t; + +typedef enum { + HTTP_AUTH_UNKNOWN = 0, + HTTP_AUTH_NONE, + HTTP_AUTH_TLS_SRP, + HTTP_AUTH_BASIC, + HTTP_AUTH_DIGEST, + HTTP_AUTH_DIGEST_IE, + HTTP_AUTH_GSSNEGOTIATE, + HTTP_AUTH_NTLM, + HTTP_AUTH_NTLM_WB, + HTTP_AUTH_ANY, + HTTP_AUTH_ANY_SAFE, + HTTP_AUTH_NUM_ENTRIES +} http_auth_type_t; + +/* + * Must be updated (in rest.c) if additional values are added to + * http_body_type_t + */ +extern const http_body_type_t http_body_type_supported[HTTP_BODY_NUM_ENTRIES]; + +extern const http_body_type_t http_curl_auth[HTTP_AUTH_NUM_ENTRIES]; + +extern const FR_NAME_NUMBER http_auth_table[]; + +extern const FR_NAME_NUMBER http_method_table[]; + +extern const FR_NAME_NUMBER http_body_table[]; + +extern const FR_NAME_NUMBER http_content_header_table[]; + +/* + * Structure for section configuration + */ +typedef struct rlm_rest_section_t { + const char *name; + char *uri; + + char *method_str; + http_method_t method; + + char *body_str; + http_body_type_t body; + + char *username; + char *password; + char *auth_str; + http_auth_type_t auth; + int require_auth; + + char *certificate_file; + int file_type; + char *private_key_file; + char *private_key_password; + char *ca_file; + char *ca_path; + char *random_file; + int check_cert_cn; + + int timeout; + unsigned int chunk; +} rlm_rest_section_t; + +/* + * Structure for module configuration + */ +typedef struct rlm_rest_t { + const char *xlat_name; + + char *connect_uri; + + fr_connection_pool_t *conn_pool; + + rlm_rest_section_t authorize; + rlm_rest_section_t authenticate; + rlm_rest_section_t accounting; + rlm_rest_section_t checksimul; + rlm_rest_section_t postauth; +} rlm_rest_t; + +/* + * States for stream based attribute encoders + */ +typedef enum { + READ_STATE_INIT = 0, + READ_STATE_ATTR_BEGIN, + READ_STATE_ATTR_CONT, + READ_STATE_END, +} read_state_t; + +/* + * States for the response parser + */ +typedef enum { + WRITE_STATE_INIT = 0, + WRITE_STATE_PARSE_HEADERS, + WRITE_STATE_PARSE_CONTENT, + WRITE_STATE_DISCARD, +} write_state_t; + +/* + * Outbound data context (passed to CURLOPT_READFUNCTION as CURLOPT_READDATA) + */ +typedef struct rlm_rest_read_t { + rlm_rest_t *instance; + REQUEST *request; + read_state_t state; + + VALUE_PAIR **first; + VALUE_PAIR **next; + + unsigned int chunk; +} rlm_rest_read_t; + +/* + * Curl inbound data context (passed to CURLOPT_WRITEFUNCTION and + * CURLOPT_HEADERFUNCTION as CURLOPT_WRITEDATA and CURLOPT_HEADERDATA) + */ +typedef struct rlm_rest_write_t { + rlm_rest_t *instance; + REQUEST *request; + write_state_t state; + + char *buffer; /* HTTP incoming raw data */ + size_t alloc; /* Space allocated for buffer */ + size_t used; /* Space used in buffer */ + + int code; /* HTTP Status Code */ + http_body_type_t type; /* HTTP Content Type */ +} rlm_rest_write_t; + +/* + * Curl context data + */ +typedef struct rlm_rest_curl_context_t { + struct curl_slist *headers; + char *body; + rlm_rest_read_t read; + rlm_rest_write_t write; +} rlm_rest_curl_context_t; + +/* + * Connection API handle + */ +typedef struct rlm_rest_handle_t { + void *handle; /* Real Handle */ + void *ctx; /* Context */ +} rlm_rest_handle_t; + +/* + * Function prototype for rest_read_wrapper. Matches CURL's + * CURLOPT_READFUNCTION prototype. + */ +typedef size_t (*rest_read_t)(void *ptr, size_t size, size_t nmemb, + void *userdata); + +/* + * Connection API callbacks + */ +int rest_init(rlm_rest_t *instance); + +void rest_cleanup(void); + +void *rest_socket_create(void *instance); + +int rest_socket_alive(void *instance, void *handle); + +int rest_socket_delete(void *instance, void *handle); + +/* + * Request processing API + */ +int rest_request_config(rlm_rest_t *instance, + rlm_rest_section_t *section, REQUEST *request, + void *handle, http_method_t method, + http_body_type_t type, char *uri); + +int rest_request_perform(rlm_rest_t *instance, rlm_rest_section_t *section, + void *handle); + +int rest_request_decode(rlm_rest_t *instance, + UNUSED rlm_rest_section_t *section, REQUEST *request, + void *handle); + +void rest_request_cleanup(rlm_rest_t *instance, rlm_rest_section_t *section, + void *handle); + +#define rest_get_handle_code(handle)(((rlm_rest_curl_context_t*)((rlm_rest_handle_t*)handle)->ctx)->write.code) + +#define rest_get_handle_type(handle)(((rlm_rest_curl_context_t*)((rlm_rest_handle_t*)handle)->ctx)->write.type) + +/* + * Helper functions + */ +ssize_t rest_uri_build(rlm_rest_t *instance, rlm_rest_section_t *section, + REQUEST *request, char *buffer, size_t bufsize); \ No newline at end of file diff --git a/src/modules/rlm_rest/rlm_rest.c b/src/modules/rlm_rest/rlm_rest.c new file mode 100644 index 00000000000..a76a4cc1540 --- /dev/null +++ b/src/modules/rlm_rest/rlm_rest.c @@ -0,0 +1,446 @@ +/* + * rlm_rest.c + * + * Version: $Id$ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Copyright 2011 Arran Cudbard-Bell > + */ + +#include +RCSID("$Id$") + +#include +#include +#include + +#include "rest.h" + +/* + * A mapping of configuration file names to internal variables. + * + * Note that the string is dynamically allocated, so it MUST + * be freed. When the configuration file parse re-reads the string, + * it free's the old one, and strdup's the new one, placing the pointer + * to the strdup'd string into 'config.string'. This gets around + * buffer over-flows. + */ +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, "post" }, + + /* User authentication */ + { "auth", PW_TYPE_STRING_PTR, + offsetof(rlm_rest_section_t, auth_str), NULL, "none" }, + { "username", PW_TYPE_STRING_PTR, + offsetof(rlm_rest_section_t, username), NULL, "" }, + { "password", PW_TYPE_STRING_PTR, + offsetof(rlm_rest_section_t, password), NULL, "" }, + { "require_auth", PW_TYPE_BOOLEAN, + offsetof(rlm_rest_section_t, require_auth), NULL, "no"}, + + /* SSL authentication */ + { "certificate_file", PW_TYPE_FILENAME, + offsetof(rlm_rest_section_t, certificate_file), NULL, NULL }, + { "pem_file_type", PW_TYPE_BOOLEAN, + offsetof(rlm_rest_section_t, file_type), NULL, "yes" }, + { "private_key_file", PW_TYPE_FILENAME, + offsetof(rlm_rest_section_t, private_key_file), NULL, NULL }, + { "private_key_password", PW_TYPE_STRING_PTR, + offsetof(rlm_rest_section_t, private_key_password), NULL, NULL }, + { "CA_file", PW_TYPE_FILENAME, + offsetof(rlm_rest_section_t, ca_file), NULL, NULL }, + { "CA_path", PW_TYPE_FILENAME, + offsetof(rlm_rest_section_t, ca_path), NULL, NULL }, + { "random_file", PW_TYPE_STRING_PTR, + offsetof(rlm_rest_section_t, random_file), NULL, NULL }, + { "check_cert_cn", PW_TYPE_BOOLEAN, + offsetof(rlm_rest_section_t, check_cert_cn), NULL, "yes"}, + + /* Transfer configuration */ + { "timeout", PW_TYPE_INTEGER, + offsetof(rlm_rest_section_t, timeout), NULL, "0" }, + { "chunk", PW_TYPE_INTEGER, + offsetof(rlm_rest_section_t, chunk), NULL, "0" }, + + { NULL, -1, 0, NULL, NULL } +}; + +static const CONF_PARSER module_config[] = { + { "connect_uri", PW_TYPE_STRING_PTR, + offsetof(rlm_rest_t, connect_uri), NULL, "http://localhost/" }, + + { NULL, -1, 0, NULL, NULL } +}; + +static int rlm_rest_perform (rlm_rest_t *instance, rlm_rest_section_t *section, + void *handle, REQUEST *request) +{ + size_t uri_len; + char uri[REST_URI_MAX_LEN]; + + int ret; + + RDEBUG("Expanding URI components"); + /* + * Build xlat'd URI, this allows REST servers to be specified by + * request attributes. + */ + uri_len = rest_uri_build(instance, section, request, uri, sizeof(uri)); + if (uri_len <= 0) return -1; + + RDEBUG("Sending HTTP %s to \"%s\"", + fr_int2str(http_method_table, section->method, NULL), + uri); + + /* + * Configure various CURL options, and initialise the read/write + * context data. + */ + ret = rest_request_config(instance, section, request, + handle, + section->method, + section->body, + uri); + if (ret <= 0) return -1; + + /* + * Send the CURL request, pre-parse headers, aggregate incoming + * HTTP body data into a single contiguous buffer. + */ + ret = rest_request_perform(instance, section, handle); + if (ret <= 0) return -1; + + return 1; +} + +static void rlm_rest_cleanup (rlm_rest_t *instance, rlm_rest_section_t *section, + void *handle) +{ + rest_request_cleanup(instance, section, handle); +}; + +static int parse_sub_section(CONF_SECTION *parent, + rlm_rest_t *instance, rlm_rest_section_t *config, + rlm_components_t comp) +{ + CONF_SECTION *cs; + + const char *name = section_type_value[comp].section; + + cs = cf_section_sub_find(parent, name); + if (!cs) { + /* TODO: Should really setup section with default values */ + return 0; + } + cf_section_parse(cs, config, section_config); + + /* + * Add section name (Maybe add to headers later?). + */ + config->name = name; + + /* + * Convert HTTP method auth and body type strings into their + * integer equivalents. + */ + config->auth = fr_str2int(http_auth_table, config->auth_str, + HTTP_AUTH_UNKNOWN); + + if (config->auth == HTTP_AUTH_UNKNOWN) { + radlog(L_ERR, "rlm_rest (%s): Unknown HTTP auth type \"%s\"", + instance->xlat_name, config->auth_str); + return -1; + } + + if (!http_curl_auth[config->auth]) { + radlog(L_ERR, "rlm_rest (%s): Unsupported HTTP auth type \"%s\"" + ", check libcurl version, OpenSSL build configuration," + " then recompile this module", + instance->xlat_name, config->body_str); + return -1; + } + + config->method = fr_str2int(http_method_table, config->method_str, + HTTP_METHOD_CUSTOM); + + config->body = fr_str2int(http_body_table, config->body_str, + HTTP_BODY_UNKNOWN); + + if (config->body == HTTP_BODY_UNKNOWN) { + radlog(L_ERR, "rlm_rest (%s): Unknown HTTP body type \"%s\"", + instance->xlat_name, config->body_str); + return -1; + } + + if (http_body_type_supported[config->body] == HTTP_BODY_UNSUPPORTED) { + radlog(L_ERR, "rlm_rest (%s): Unsupported HTTP body type \"%s\"" + ", please submit patches", instance->xlat_name, + config->body_str); + return -1; + } + + return 1; +} + +/* + * Do any per-module initialization that is separate to each + * configured instance of the module. e.g. set up connections + * to external databases, read configuration files, set up + * dictionary entries, etc. + * + * If configuration information is given in the config section + * that must be referenced in later calls, store a handle to it + * in *instance otherwise put a null pointer there. + */ +static int rlm_rest_instantiate(CONF_SECTION *conf, void **instance) +{ + rlm_rest_t *data; + const char *xlat_name; + + /* + * Allocate memory for instance data. + */ + data = rad_malloc(sizeof(*data)); + if (!data) { + return -1; + } + memset(data, 0, sizeof(*data)); + + /* + * If the configuration parameters can't be parsed, then + * fail. + */ + if (cf_section_parse(conf, data, module_config) < 0) { + free(data); + return -1; + } + + xlat_name = cf_section_name2(conf); + if (xlat_name == NULL) { + xlat_name = cf_section_name1(conf); + } + + data->xlat_name = xlat_name; + + /* + * Parse sub-section configs. + */ + if ( + (parse_sub_section(conf, data, &data->authorize, + RLM_COMPONENT_AUTZ) < 0) || + (parse_sub_section(conf, data, &data->authenticate, + RLM_COMPONENT_AUTH) < 0) || + (parse_sub_section(conf, data, &data->accounting, + RLM_COMPONENT_ACCT) < 0) || + (parse_sub_section(conf, data, &data->checksimul, + RLM_COMPONENT_SESS) < 0) || + (parse_sub_section(conf, data, &data->postauth, + RLM_COMPONENT_POST_AUTH) < 0)) + { + return -1; + } + + /* + * Initialise REST libraries. + */ + if (!rest_init(data)) { + return -1; + } + + data->conn_pool = fr_connection_pool_init(conf, data, + rest_socket_create, + rest_socket_alive, + rest_socket_delete); + + if (!data->conn_pool) { + return -1; + } + + *instance = data; + + return 0; +} + +/* + * Find the named user in this modules database. Create the set + * of attribute-value pairs to check and reply with for this user + * from the database. The authentication code only needs to check + * the password, the rest is done here. + */ +static int rlm_rest_authorize(void *instance, REQUEST *request) +{ + rlm_rest_t *my_instance = instance; + rlm_rest_section_t *section = &my_instance->authorize; + + void *handle; + int hcode; + int rcode = RLM_MODULE_OK; + int ret; + + handle = fr_connection_get(my_instance->conn_pool); + if (!handle) return RLM_MODULE_FAIL; + + ret = rlm_rest_perform(instance, section, handle, request); + if (ret < 0) { + rcode = RLM_MODULE_FAIL; + goto end; + } + + hcode = rest_get_handle_code(handle); + + switch (hcode) { + case 404: + case 410: + rcode = RLM_MODULE_NOTFOUND; + break; + case 403: + rcode = RLM_MODULE_USERLOCK; + break; + case 401: + /* + * Attempt to parse content if there was any. + */ + ret = rest_request_decode(my_instance, section, + request, handle); + if (ret < 0) { + rcode = RLM_MODULE_FAIL; + break; + } + + rcode = RLM_MODULE_REJECT; + break; + case 204: + rcode = RLM_MODULE_OK; + break; + default: + /* + * Attempt to parse content if there was any. + */ + if ((hcode >= 200) && (hcode < 300)) { + ret = rest_request_decode(my_instance, section, + request, handle); + if (ret < 0) rcode = RLM_MODULE_FAIL; + else if (ret == 0) rcode = RLM_MODULE_OK; + else rcode = RLM_MODULE_UPDATED; + break; + } else if (hcode < 500) { + rcode = RLM_MODULE_INVALID; + } else { + rcode = RLM_MODULE_FAIL; + } + } + + end: + + rlm_rest_cleanup(instance, section, handle); + + fr_connection_release(my_instance->conn_pool, handle); + + return rcode; +} + +/* + * Authenticate the user with the given password. + */ +static int rlm_rest_authenticate(void *instance, REQUEST *request) +{ + /* quiet the compiler */ + instance = instance; + request = request; + + return RLM_MODULE_OK; +} + +/* + * Write accounting information to this modules database. + */ +static int rlm_rest_accounting(void *instance, REQUEST *request) +{ + /* quiet the compiler */ + instance = instance; + request = request; + + return RLM_MODULE_OK; +} + +/* + * See if a user is already logged in. Sets request->simul_count to the + * current session count for this user and sets request->simul_mpp to 2 + * if it looks like a multilink attempt based on the requested IP + * address, otherwise leaves request->simul_mpp alone. + * + * Check twice. If on the first pass the user exceeds his + * max. number of logins, do a second pass and validate all + * logins by querying the terminal server (using eg. SNMP). + */ +static int rlm_rest_checksimul(void *instance, REQUEST *request) +{ + instance = instance; + + request->simul_count=0; + + return RLM_MODULE_OK; +} + +/* + * Only free memory we allocated. The strings allocated via + * cf_section_parse() do not need to be freed. + */ +static int rlm_rest_detach(void *instance) +{ + rlm_rest_t *my_instance = instance; + + fr_connection_pool_delete(my_instance->conn_pool); + + free(my_instance); + + /* Free any memory used by libcurl */ + rest_cleanup(); + + return 0; +} + +/* + * The module name should be the only globally exported symbol. + * That is, everything else should be 'static'. + * + * If the module needs to temporarily modify it's instantiation + * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE. + * The server will then take care of ensuring that the module + * is single-threaded. + */ +module_t rlm_rest = { + RLM_MODULE_INIT, + "rlm_rest", + RLM_TYPE_THREAD_SAFE, /* type */ + rlm_rest_instantiate, /* instantiation */ + rlm_rest_detach, /* detach */ + { + rlm_rest_authenticate, /* authentication */ + rlm_rest_authorize, /* authorization */ + NULL, /* preaccounting */ + rlm_rest_accounting, /* accounting */ + rlm_rest_checksimul, /* checksimul */ + NULL, /* pre-proxy */ + NULL, /* post-proxy */ + NULL /* post-auth */ + }, +};