2 * This program is is free software; you can redistribute it and/or modify
3 * it under the terms of the GNU General Public License as published by
4 * the Free Software Foundation; either version 2 of the License, or (at
5 * your option) any later version.
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20 * @brief Integrate FreeRADIUS with RESTfull APIs
22 * @copyright 2012-2014 Arran Cudbard-Bell <arran.cudbardb@freeradius.org>
26 #include <freeradius-devel/radiusd.h>
27 #include <freeradius-devel/modules.h>
28 #include <freeradius-devel/token.h>
29 #include <freeradius-devel/rad_assert.h>
37 static CONF_PARSER tls_config[] = {
38 { "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_ca_file), NULL },
39 { "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_ca_path), NULL },
40 { "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_certificate_file), NULL },
41 { "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_private_key_file), NULL },
42 { "private_key_password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, rlm_rest_section_t, tls_private_key_password), NULL },
43 { "random_file", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, tls_random_file), NULL },
44 { "check_cert", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, tls_check_cert), "yes" },
45 { "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, tls_check_cert_cn), "yes" },
46 CONF_PARSER_TERMINATOR
50 * A mapping of configuration file names to internal variables.
52 * Note that the string is dynamically allocated, so it MUST
53 * be freed. When the configuration file parse re-reads the string,
54 * it free's the old one, and strdup's the new one, placing the pointer
55 * to the strdup'd string into 'config.string'. This gets around
58 static const CONF_PARSER section_config[] = {
59 { "uri", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_rest_section_t, uri), "" },
60 { "method", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, method_str), "GET" },
61 { "body", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, body_str), "none" },
62 { "data", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_rest_section_t, data), NULL },
63 { "force_to", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, force_to_str), NULL },
65 /* User authentication */
66 { "auth", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, auth_str), "none" },
67 { "username", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_rest_section_t, username), NULL },
68 { "password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_rest_section_t, password), NULL },
69 { "require_auth", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, require_auth), "no" },
71 /* Transfer configuration */
72 { "timeout", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, rlm_rest_section_t, timeout_tv), "4.0" },
73 { "chunk", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_rest_section_t, chunk), "0" },
76 { "tls", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) tls_config },
77 CONF_PARSER_TERMINATOR
80 static const CONF_PARSER module_config[] = {
81 { "connect_uri", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_t, connect_uri), NULL },
82 { "connect_timeout", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, rlm_rest_t, connect_timeout_tv), "4.0" },
83 CONF_PARSER_TERMINATOR
86 static int rlm_rest_perform(rlm_rest_t *instance, rlm_rest_section_t *section, void *handle, REQUEST *request,
87 char const *username, char const *password)
94 RDEBUG("Expanding URI components");
97 * Build xlat'd URI, this allows REST servers to be specified by
100 uri_len = rest_uri_build(&uri, instance, request, section->uri);
101 if (uri_len <= 0) return -1;
103 RDEBUG("Sending HTTP %s to \"%s\"", fr_int2str(http_method_table, section->method, NULL), uri);
106 * Configure various CURL options, and initialise the read/write
109 ret = rest_request_config(instance, section, request, handle, section->method, section->body,
110 uri, username, password);
112 if (ret < 0) return -1;
115 * Send the CURL request, pre-parse headers, aggregate incoming
116 * HTTP body data into a single contiguous buffer.
118 ret = rest_request_perform(instance, section, request, handle);
119 if (ret < 0) return -1;
124 static void rlm_rest_cleanup(rlm_rest_t *instance, rlm_rest_section_t *section, void *handle)
126 rest_request_cleanup(instance, section, handle);
129 static ssize_t jsonquote_xlat(UNUSED void *instance, UNUSED REQUEST *request,
130 char const *fmt, char *out, size_t outlen)
133 size_t freespace = outlen;
136 for (p = fmt; *p != '\0'; p++) {
137 /* Indicate truncation */
147 } else if (*p == '\\') {
151 } else if (*p == '/') {
155 } else if (*p >= ' ') {
192 len = snprintf(out, freespace, "u%04X", *p);
193 if (is_truncated(len, freespace)) return (outlen - freespace) + len;
202 return outlen - freespace;
205 * Simple xlat to read text data from a URL
207 static ssize_t rest_xlat(void *instance, REQUEST *request,
208 char const *fmt, char *out, size_t freespace)
210 rlm_rest_t *inst = instance;
214 ssize_t len, outlen = 0;
216 char const *p = fmt, *q;
218 http_method_t method;
220 /* There are no configurable parameters other than the URI */
221 rlm_rest_section_t section = {
223 .method = HTTP_METHOD_GET,
224 .body = HTTP_BODY_NONE,
225 .body_str = "application/x-www-form-urlencoded",
226 .require_auth = false,
227 .force_to = HTTP_BODY_PLAIN
233 RDEBUG("Expanding URI components");
235 handle = fr_connection_get(inst->pool);
236 if (!handle) return -1;
239 * Extract the method from the start of the format string (if there is one)
241 method = fr_substr2int(http_method_table, p, HTTP_METHOD_UNKNOWN, -1);
242 if (method != HTTP_METHOD_UNKNOWN) {
243 section.method = method;
244 p += strlen(http_method_table[method].name);
250 while (isspace(*p) && p++);
253 * Unescape parts of xlat'd URI, this allows REST servers to be specified by
254 * request attributes.
256 len = rest_uri_host_unescape(&uri, instance, request, handle, p);
263 * Extract freeform body data (url can't contain spaces)
266 if (q && (*++q != '\0')) {
267 section.body = HTTP_BODY_CUSTOM_LITERAL;
271 RDEBUG("Sending HTTP %s to \"%s\"", fr_int2str(http_method_table, section.method, NULL), uri);
274 * Configure various CURL options, and initialise the read/write
277 * @todo We could extract the User-Name and password from the URL string.
279 ret = rest_request_config(instance, §ion, request, handle, section.method, section.body,
288 * Send the CURL request, pre-parse headers, aggregate incoming
289 * HTTP body data into a single contiguous buffer.
291 ret = rest_request_perform(instance, §ion, request, handle);
297 hcode = rest_get_handle_code(handle);
306 rest_response_error(request, handle);
314 * Attempt to parse content if there was any.
316 if ((hcode >= 200) && (hcode < 300)) {
318 } else if (hcode < 500) {
327 len = rest_get_handle_data(&body, handle);
328 if ((size_t) len >= freespace) {
329 REDEBUG("Insufficient space to write HTTP response, needed %zu bytes, have %zu bytes", len + 1,
336 strlcpy(out, body, len + 1); /* strlcpy takes the size of the buffer */
340 rlm_rest_cleanup(instance, §ion, handle);
342 fr_connection_release(inst->pool, handle);
348 * Find the named user in this modules database. Create the set
349 * of attribute-value pairs to check and reply with for this user
350 * from the database. The authentication code only needs to check
351 * the password, the rest is done here.
353 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request)
355 rlm_rest_t *inst = instance;
356 rlm_rest_section_t *section = &inst->authorize;
360 int rcode = RLM_MODULE_OK;
363 if (!section->name) return RLM_MODULE_NOOP;
365 handle = fr_connection_get(inst->pool);
366 if (!handle) return RLM_MODULE_FAIL;
368 ret = rlm_rest_perform(instance, section, handle, request, NULL, NULL);
370 rcode = RLM_MODULE_FAIL;
374 hcode = rest_get_handle_code(handle);
378 rcode = RLM_MODULE_NOTFOUND;
382 rcode = RLM_MODULE_USERLOCK;
387 * Attempt to parse content if there was any.
389 ret = rest_response_decode(inst, section, request, handle);
391 rcode = RLM_MODULE_FAIL;
395 rcode = RLM_MODULE_REJECT;
399 rcode = RLM_MODULE_OK;
404 * Attempt to parse content if there was any.
406 if ((hcode >= 200) && (hcode < 300)) {
407 ret = rest_response_decode(inst, section, request, handle);
408 if (ret < 0) rcode = RLM_MODULE_FAIL;
409 else if (ret == 0) rcode = RLM_MODULE_OK;
410 else rcode = RLM_MODULE_UPDATED;
412 } else if (hcode < 500) {
413 rcode = RLM_MODULE_INVALID;
415 rcode = RLM_MODULE_FAIL;
421 case RLM_MODULE_INVALID:
422 case RLM_MODULE_FAIL:
423 case RLM_MODULE_USERLOCK:
424 rest_response_error(request, handle);
431 rlm_rest_cleanup(instance, section, handle);
433 fr_connection_release(inst->pool, handle);
439 * Authenticate the user with the given password.
441 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request)
443 rlm_rest_t *inst = instance;
444 rlm_rest_section_t *section = &inst->authenticate;
448 int rcode = RLM_MODULE_OK;
451 VALUE_PAIR const *username;
452 VALUE_PAIR const *password;
454 if (!section->name) return RLM_MODULE_NOOP;
456 username = request->username;
457 if (!request->username) {
458 REDEBUG("Can't perform authentication, 'User-Name' attribute not found in the request");
460 return RLM_MODULE_INVALID;
463 password = request->password;
465 (password->da->attr != PW_USER_PASSWORD)) {
466 REDEBUG("You set 'Auth-Type = REST' for a request that does not contain a User-Password attribute!");
467 return RLM_MODULE_INVALID;
470 handle = fr_connection_get(inst->pool);
471 if (!handle) return RLM_MODULE_FAIL;
473 ret = rlm_rest_perform(instance, section, handle, request, username->vp_strvalue, password->vp_strvalue);
475 rcode = RLM_MODULE_FAIL;
479 hcode = rest_get_handle_code(handle);
483 rcode = RLM_MODULE_NOTFOUND;
487 rcode = RLM_MODULE_USERLOCK;
492 * Attempt to parse content if there was any.
494 ret = rest_response_decode(inst, section, request, handle);
496 rcode = RLM_MODULE_FAIL;
500 rcode = RLM_MODULE_REJECT;
504 rcode = RLM_MODULE_OK;
509 * Attempt to parse content if there was any.
511 if ((hcode >= 200) && (hcode < 300)) {
512 ret = rest_response_decode(inst, section, request, handle);
513 if (ret < 0) rcode = RLM_MODULE_FAIL;
514 else if (ret == 0) rcode = RLM_MODULE_OK;
515 else rcode = RLM_MODULE_UPDATED;
517 } else if (hcode < 500) {
518 rcode = RLM_MODULE_INVALID;
520 rcode = RLM_MODULE_FAIL;
526 case RLM_MODULE_INVALID:
527 case RLM_MODULE_FAIL:
528 case RLM_MODULE_USERLOCK:
529 rest_response_error(request, handle);
536 rlm_rest_cleanup(instance, section, handle);
538 fr_connection_release(inst->pool, handle);
544 * Send accounting info to a REST API endpoint
546 static rlm_rcode_t CC_HINT(nonnull) mod_accounting(void *instance, REQUEST *request)
548 rlm_rest_t *inst = instance;
549 rlm_rest_section_t *section = &inst->accounting;
553 int rcode = RLM_MODULE_OK;
556 if (!section->name) return RLM_MODULE_NOOP;
558 handle = fr_connection_get(inst->pool);
559 if (!handle) return RLM_MODULE_FAIL;
561 ret = rlm_rest_perform(inst, section, handle, request, NULL, NULL);
563 rcode = RLM_MODULE_FAIL;
567 hcode = rest_get_handle_code(handle);
569 rcode = RLM_MODULE_FAIL;
570 } else if (hcode == 204) {
571 rcode = RLM_MODULE_OK;
572 } else if ((hcode >= 200) && (hcode < 300)) {
573 ret = rest_response_decode(inst, section, request, handle);
574 if (ret < 0) rcode = RLM_MODULE_FAIL;
575 else if (ret == 0) rcode = RLM_MODULE_OK;
576 else rcode = RLM_MODULE_UPDATED;
578 rcode = RLM_MODULE_INVALID;
583 case RLM_MODULE_INVALID:
584 case RLM_MODULE_FAIL:
585 rest_response_error(request, handle);
592 rlm_rest_cleanup(inst, section, handle);
594 fr_connection_release(inst->pool, handle);
600 * Send post-auth info to a REST API endpoint
602 static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *request)
604 rlm_rest_t *inst = instance;
605 rlm_rest_section_t *section = &inst->post_auth;
609 int rcode = RLM_MODULE_OK;
612 if (!section->name) return RLM_MODULE_NOOP;
614 handle = fr_connection_get(inst->pool);
615 if (!handle) return RLM_MODULE_FAIL;
617 ret = rlm_rest_perform(inst, section, handle, request, NULL, NULL);
619 rcode = RLM_MODULE_FAIL;
623 hcode = rest_get_handle_code(handle);
625 rcode = RLM_MODULE_FAIL;
626 } else if (hcode == 204) {
627 rcode = RLM_MODULE_OK;
628 } else if ((hcode >= 200) && (hcode < 300)) {
629 ret = rest_response_decode(inst, section, request, handle);
630 if (ret < 0) rcode = RLM_MODULE_FAIL;
631 else if (ret == 0) rcode = RLM_MODULE_OK;
632 else rcode = RLM_MODULE_UPDATED;
634 rcode = RLM_MODULE_INVALID;
639 case RLM_MODULE_INVALID:
640 case RLM_MODULE_FAIL:
641 rest_response_error(request, handle);
648 rlm_rest_cleanup(inst, section, handle);
650 fr_connection_release(inst->pool, handle);
655 static int parse_sub_section(CONF_SECTION *parent, rlm_rest_section_t *config, rlm_components_t comp)
659 char const *name = section_type_value[comp].section;
661 cs = cf_section_sub_find(parent, name);
667 if (cf_section_parse(cs, config, section_config) < 0) {
673 * Add section name (Maybe add to headers later?).
680 if ((config->username && !config->password) || (!config->username && config->password)) {
681 cf_log_err_cs(cs, "'username' and 'password' must both be set or both be absent");
687 * Convert HTTP method auth and body type strings into their integer equivalents.
689 config->auth = fr_str2int(http_auth_table, config->auth_str, HTTP_AUTH_UNKNOWN);
690 if (config->auth == HTTP_AUTH_UNKNOWN) {
691 cf_log_err_cs(cs, "Unknown HTTP auth type '%s'", config->auth_str);
694 } else if ((config->auth != HTTP_AUTH_NONE) && !http_curl_auth[config->auth]) {
695 cf_log_err_cs(cs, "Unsupported HTTP auth type \"%s\", check libcurl version, OpenSSL build "
696 "configuration, then recompile this module", config->auth_str);
701 config->method = fr_str2int(http_method_table, config->method_str, HTTP_METHOD_CUSTOM);
702 config->timeout = ((config->timeout_tv.tv_usec * 1000) + (config->timeout_tv.tv_sec / 1000));
705 * We don't have any custom user data, so we need to select the right encoder based
708 * To make this slightly more/less confusing, we accept both canonical body_types,
712 config->body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
713 if (config->body == HTTP_BODY_UNKNOWN) {
714 config->body = fr_str2int(http_content_type_table, config->body_str, HTTP_BODY_UNKNOWN);
717 if (config->body == HTTP_BODY_UNKNOWN) {
718 cf_log_err_cs(cs, "Unknown HTTP body type '%s'", config->body_str);
722 switch (http_body_type_supported[config->body]) {
723 case HTTP_BODY_UNSUPPORTED:
724 cf_log_err_cs(cs, "Unsupported HTTP body type \"%s\", please submit patches",
728 case HTTP_BODY_INVALID:
729 cf_log_err_cs(cs, "Invalid HTTP body type. \"%s\" is not a valid web API data "
730 "markup format", config->body_str);
733 case HTTP_BODY_UNAVAILABLE:
734 cf_log_err_cs(cs, "Unavailable HTTP body type. \"%s\" is not available in this "
735 "build", config->body_str);
742 * We have custom body data so we set HTTP_BODY_CUSTOM_XLAT, but also need to try and
743 * figure out what content-type to use. So if they've used the canonical form we
744 * need to convert it back into a proper HTTP content_type value.
747 http_body_type_t body;
749 config->body = HTTP_BODY_CUSTOM_XLAT;
751 body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
752 if (body != HTTP_BODY_UNKNOWN) {
753 config->body_str = fr_int2str(http_content_type_table, body, config->body_str);
757 if (config->force_to_str) {
758 config->force_to = fr_str2int(http_body_type_table, config->force_to_str, HTTP_BODY_UNKNOWN);
759 if (config->force_to == HTTP_BODY_UNKNOWN) {
760 config->force_to = fr_str2int(http_content_type_table, config->force_to_str, HTTP_BODY_UNKNOWN);
763 if (config->force_to == HTTP_BODY_UNKNOWN) {
764 cf_log_err_cs(cs, "Unknown forced response body type '%s'", config->force_to_str);
768 switch (http_body_type_supported[config->force_to]) {
769 case HTTP_BODY_UNSUPPORTED:
770 cf_log_err_cs(cs, "Unsupported forced response body type \"%s\", please submit patches",
771 config->force_to_str);
774 case HTTP_BODY_INVALID:
775 cf_log_err_cs(cs, "Invalid HTTP forced response body type. \"%s\" is not a valid web API data "
776 "markup format", config->force_to_str);
788 static int mod_bootstrap(CONF_SECTION *conf, void *instance)
790 rlm_rest_t *inst = instance;
792 inst->xlat_name = cf_section_name2(conf);
793 if (!inst->xlat_name) inst->xlat_name = cf_section_name1(conf);
796 * Register the rest xlat function
798 xlat_register(inst->xlat_name, rest_xlat, rest_uri_escape, inst);
799 xlat_register("jsonquote", jsonquote_xlat, NULL, inst);
806 * Do any per-module initialization that is separate to each
807 * configured instance of the module. e.g. set up connections
808 * to external databases, read configuration files, set up
809 * dictionary entries, etc.
811 * If configuration information is given in the config section
812 * that must be referenced in later calls, store a handle to it
813 * in *instance otherwise put a null pointer there.
815 static int mod_instantiate(CONF_SECTION *conf, void *instance)
817 rlm_rest_t *inst = instance;
820 * Parse sub-section configs.
823 (parse_sub_section(conf, &inst->authorize, MOD_AUTHORIZE) < 0) ||
824 (parse_sub_section(conf, &inst->authenticate, MOD_AUTHENTICATE) < 0) ||
825 (parse_sub_section(conf, &inst->accounting, MOD_ACCOUNTING) < 0) ||
827 /* @todo add behaviour for checksimul */
828 /* (parse_sub_section(conf, &inst->checksimul, MOD_SESSION) < 0) || */
829 (parse_sub_section(conf, &inst->post_auth, MOD_POST_AUTH) < 0))
835 * Initialise REST libraries.
837 if (rest_init(inst) < 0) {
841 inst->connect_timeout = ((inst->connect_timeout_tv.tv_usec * 1000) +
842 (inst->connect_timeout_tv.tv_sec / 1000));
843 inst->pool = fr_connection_pool_module_init(conf, inst, mod_conn_create, mod_conn_alive, NULL);
844 if (!inst->pool) return -1;
850 * Only free memory we allocated. The strings allocated via
851 * cf_section_parse() do not need to be freed.
853 static int mod_detach(void *instance)
855 rlm_rest_t *inst = instance;
857 fr_connection_pool_free(inst->pool);
859 /* Free any memory used by libcurl */
866 * The module name should be the only globally exported symbol.
867 * That is, everything else should be 'static'.
869 * If the module needs to temporarily modify it's instantiation
870 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
871 * The server will then take care of ensuring that the module
872 * is single-threaded.
874 extern module_t rlm_rest;
875 module_t rlm_rest = {
876 .magic = RLM_MODULE_INIT,
878 .type = RLM_TYPE_THREAD_SAFE,
879 .inst_size = sizeof(rlm_rest_t),
880 .config = module_config,
881 .bootstrap = mod_bootstrap,
882 .instantiate = mod_instantiate,
883 .detach = mod_detach,
885 [MOD_AUTHENTICATE] = mod_authenticate,
886 [MOD_AUTHORIZE] = mod_authorize,
887 [MOD_ACCOUNTING] = mod_accounting,
888 [MOD_POST_AUTH] = mod_post_auth