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, version 2 if the
4 * License as published by the Free Software Foundation.
6 * This program is distributed in the hope that it will be useful,
7 * but WITHOUT ANY WARRANTY; without even the implied warranty of
8 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 * GNU General Public License for more details.
11 * You should have received a copy of the GNU General Public License
12 * along with this program; if not, write to the Free Software
13 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19 * @brief Integrate FreeRADIUS with RESTfull APIs
21 * @copyright 2012-2014 Arran Cudbard-Bell <arran.cudbardb@freeradius.org>
25 #include <freeradius-devel/radiusd.h>
26 #include <freeradius-devel/modules.h>
27 #include <freeradius-devel/token.h>
28 #include <freeradius-devel/rad_assert.h>
36 static CONF_PARSER tls_config[] = {
37 { "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_ca_file), NULL },
38 { "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_ca_path), NULL },
39 { "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_certificate_file), NULL },
40 { "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_rest_section_t, tls_private_key_file), NULL },
41 { "private_key_password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, rlm_rest_section_t, tls_private_key_password), NULL },
42 { "random_file", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_section_t, tls_random_file), NULL },
43 { "check_cert", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, tls_check_cert), "yes" },
44 { "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_rest_section_t, tls_check_cert_cn), "yes" },
46 { NULL, -1, 0, NULL, NULL }
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, 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, 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, rlm_rest_section_t, username), NULL },
68 { "password", FR_CONF_OFFSET(PW_TYPE_STRING, 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_INTEGER, rlm_rest_section_t, timeout), "4" },
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 },
78 { NULL, -1, 0, NULL, NULL }
81 static const CONF_PARSER module_config[] = {
82 { "connect_uri", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_rest_t, connect_uri), NULL },
84 { NULL, -1, 0, NULL, NULL }
87 static int rlm_rest_perform(rlm_rest_t *instance, rlm_rest_section_t *section, void *handle, REQUEST *request,
88 char const *username, char const *password)
95 RDEBUG("Expanding URI components");
98 * Build xlat'd URI, this allows REST servers to be specified by
101 uri_len = rest_uri_build(&uri, instance, request, section->uri);
102 if (uri_len <= 0) return -1;
104 RDEBUG("Sending HTTP %s to \"%s\"", fr_int2str(http_method_table, section->method, NULL), uri);
107 * Configure various CURL options, and initialise the read/write
110 ret = rest_request_config(instance, section, request, handle, section->method, section->body,
111 uri, username, password);
113 if (ret < 0) return -1;
116 * Send the CURL request, pre-parse headers, aggregate incoming
117 * HTTP body data into a single contiguous buffer.
119 ret = rest_request_perform(instance, section, request, handle);
120 if (ret < 0) return -1;
125 static void rlm_rest_cleanup(rlm_rest_t *instance, rlm_rest_section_t *section, void *handle)
127 rest_request_cleanup(instance, section, handle);
131 * Simple xlat to read text data from a URL
133 static ssize_t rest_xlat(void *instance, REQUEST *request,
134 char const *fmt, char *out, size_t freespace)
136 rlm_rest_t *inst = instance;
140 ssize_t len, outlen = 0;
142 char const *p = fmt, *q;
144 http_method_t method;
146 /* There are no configurable parameters other than the URI */
147 rlm_rest_section_t section = {
149 .method = HTTP_METHOD_GET,
150 .body = HTTP_BODY_NONE,
151 .body_str = "application/x-www-form-urlencoded",
152 .require_auth = false,
154 .force_to = HTTP_BODY_PLAIN
160 RDEBUG("Expanding URI components");
162 handle = fr_connection_get(inst->conn_pool);
163 if (!handle) return -1;
166 * Extract the method from the start of the format string (if there is one)
168 method = fr_substr2int(http_method_table, p, HTTP_METHOD_UNKNOWN, -1);
169 if (method != HTTP_METHOD_UNKNOWN) {
170 section.method = method;
171 p += strlen(http_method_table[method].name);
177 while (isspace(*p) && p++);
180 * Unescape parts of xlat'd URI, this allows REST servers to be specified by
181 * request attributes.
183 len = rest_uri_host_unescape(&uri, instance, request, handle, p);
190 * Extract freeform body data (url can't contain spaces)
193 if (q && (*++q != '\0')) {
194 section.body = HTTP_BODY_CUSTOM_LITERAL;
198 RDEBUG("Sending HTTP %s to \"%s\"", fr_int2str(http_method_table, section.method, NULL), uri);
201 * Configure various CURL options, and initialise the read/write
204 * @todo We could extract the User-Name and password from the URL string.
206 ret = rest_request_config(instance, §ion, request, handle, section.method, section.body,
209 if (ret < 0) return -1;
212 * Send the CURL request, pre-parse headers, aggregate incoming
213 * HTTP body data into a single contiguous buffer.
215 ret = rest_request_perform(instance, §ion, request, handle);
216 if (ret < 0) return -1;
218 hcode = rest_get_handle_code(handle);
227 rest_response_error(request, handle);
235 * Attempt to parse content if there was any.
237 if ((hcode >= 200) && (hcode < 300)) {
239 } else if (hcode < 500) {
248 len = rest_get_handle_data(&body, handle);
249 if ((size_t) len >= freespace) {
250 REDEBUG("Insufficient space to write HTTP response, needed %zu bytes, have %zu bytes", len + 1,
257 strlcpy(out, body, len + 1); /* strlcpy takes the size of the buffer */
261 rlm_rest_cleanup(instance, §ion, handle);
263 fr_connection_release(inst->conn_pool, handle);
269 * Find the named user in this modules database. Create the set
270 * of attribute-value pairs to check and reply with for this user
271 * from the database. The authentication code only needs to check
272 * the password, the rest is done here.
274 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request)
276 rlm_rest_t *inst = instance;
277 rlm_rest_section_t *section = &inst->authorize;
281 int rcode = RLM_MODULE_OK;
284 if (!section->name) return RLM_MODULE_NOOP;
286 handle = fr_connection_get(inst->conn_pool);
287 if (!handle) return RLM_MODULE_FAIL;
289 ret = rlm_rest_perform(instance, section, handle, request, NULL, NULL);
291 rcode = RLM_MODULE_FAIL;
295 hcode = rest_get_handle_code(handle);
299 rcode = RLM_MODULE_NOTFOUND;
303 rcode = RLM_MODULE_USERLOCK;
308 * Attempt to parse content if there was any.
310 ret = rest_response_decode(inst, section, request, handle);
312 rcode = RLM_MODULE_FAIL;
316 rcode = RLM_MODULE_REJECT;
320 rcode = RLM_MODULE_OK;
325 * Attempt to parse content if there was any.
327 if ((hcode >= 200) && (hcode < 300)) {
328 ret = rest_response_decode(inst, section, request, handle);
329 if (ret < 0) rcode = RLM_MODULE_FAIL;
330 else if (ret == 0) rcode = RLM_MODULE_OK;
331 else rcode = RLM_MODULE_UPDATED;
333 } else if (hcode < 500) {
334 rcode = RLM_MODULE_INVALID;
336 rcode = RLM_MODULE_FAIL;
342 case RLM_MODULE_INVALID:
343 case RLM_MODULE_FAIL:
344 case RLM_MODULE_USERLOCK:
345 rest_response_error(request, handle);
352 rlm_rest_cleanup(instance, section, handle);
354 fr_connection_release(inst->conn_pool, handle);
360 * Authenticate the user with the given password.
362 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, UNUSED REQUEST *request)
364 rlm_rest_t *inst = instance;
365 rlm_rest_section_t *section = &inst->authenticate;
369 int rcode = RLM_MODULE_OK;
372 VALUE_PAIR const *username;
373 VALUE_PAIR const *password;
375 if (!section->name) return RLM_MODULE_NOOP;
377 username = request->username;
378 if (!request->username) {
379 REDEBUG("Can't perform authentication, 'User-Name' attribute not found in the request");
381 return RLM_MODULE_INVALID;
384 password = request->password;
386 (password->da->attr != PW_USER_PASSWORD)) {
387 REDEBUG("You set 'Auth-Type = REST' for a request that does not contain a User-Password attribute!");
388 return RLM_MODULE_INVALID;
391 handle = fr_connection_get(inst->conn_pool);
392 if (!handle) return RLM_MODULE_FAIL;
394 ret = rlm_rest_perform(instance, section, handle, request, username->vp_strvalue, password->vp_strvalue);
396 rcode = RLM_MODULE_FAIL;
400 hcode = rest_get_handle_code(handle);
404 rcode = RLM_MODULE_NOTFOUND;
408 rcode = RLM_MODULE_USERLOCK;
413 * Attempt to parse content if there was any.
415 ret = rest_response_decode(inst, section, request, handle);
417 rcode = RLM_MODULE_FAIL;
421 rcode = RLM_MODULE_REJECT;
425 rcode = RLM_MODULE_OK;
430 * Attempt to parse content if there was any.
432 if ((hcode >= 200) && (hcode < 300)) {
433 ret = rest_response_decode(inst, section, request, handle);
434 if (ret < 0) rcode = RLM_MODULE_FAIL;
435 else if (ret == 0) rcode = RLM_MODULE_OK;
436 else rcode = RLM_MODULE_UPDATED;
438 } else if (hcode < 500) {
439 rcode = RLM_MODULE_INVALID;
441 rcode = RLM_MODULE_FAIL;
447 case RLM_MODULE_INVALID:
448 case RLM_MODULE_FAIL:
449 case RLM_MODULE_USERLOCK:
450 rest_response_error(request, handle);
457 rlm_rest_cleanup(instance, section, handle);
459 fr_connection_release(inst->conn_pool, handle);
465 * Send accounting info to a REST API endpoint
467 static rlm_rcode_t CC_HINT(nonnull) mod_accounting(void *instance, UNUSED REQUEST *request)
469 rlm_rest_t *inst = instance;
470 rlm_rest_section_t *section = &inst->accounting;
474 int rcode = RLM_MODULE_OK;
477 if (!section->name) return RLM_MODULE_NOOP;
479 handle = fr_connection_get(inst->conn_pool);
480 if (!handle) return RLM_MODULE_FAIL;
482 ret = rlm_rest_perform(inst, section, handle, request, NULL, NULL);
484 rcode = RLM_MODULE_FAIL;
488 hcode = rest_get_handle_code(handle);
490 rcode = RLM_MODULE_FAIL;
491 } else if (hcode == 204) {
492 rcode = RLM_MODULE_OK;
493 } else if ((hcode >= 200) && (hcode < 300)) {
494 ret = rest_response_decode(inst, section, request, handle);
495 if (ret < 0) rcode = RLM_MODULE_FAIL;
496 else if (ret == 0) rcode = RLM_MODULE_OK;
497 else rcode = RLM_MODULE_UPDATED;
499 rcode = RLM_MODULE_INVALID;
504 case RLM_MODULE_INVALID:
505 case RLM_MODULE_FAIL:
506 rest_response_error(request, handle);
513 rlm_rest_cleanup(inst, section, handle);
515 fr_connection_release(inst->conn_pool, handle);
521 * Send post-auth info to a REST API endpoint
523 static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, UNUSED REQUEST *request)
525 rlm_rest_t *inst = instance;
526 rlm_rest_section_t *section = &inst->post_auth;
530 int rcode = RLM_MODULE_OK;
533 if (!section->name) return RLM_MODULE_NOOP;
535 handle = fr_connection_get(inst->conn_pool);
536 if (!handle) return RLM_MODULE_FAIL;
538 ret = rlm_rest_perform(inst, section, handle, request, NULL, NULL);
540 rcode = RLM_MODULE_FAIL;
544 hcode = rest_get_handle_code(handle);
546 rcode = RLM_MODULE_FAIL;
547 } else if (hcode == 204) {
548 rcode = RLM_MODULE_OK;
549 } else if ((hcode >= 200) && (hcode < 300)) {
550 ret = rest_response_decode(inst, section, request, handle);
551 if (ret < 0) rcode = RLM_MODULE_FAIL;
552 else if (ret == 0) rcode = RLM_MODULE_OK;
553 else rcode = RLM_MODULE_UPDATED;
555 rcode = RLM_MODULE_INVALID;
560 case RLM_MODULE_INVALID:
561 case RLM_MODULE_FAIL:
562 rest_response_error(request, handle);
569 rlm_rest_cleanup(inst, section, handle);
571 fr_connection_release(inst->conn_pool, handle);
576 static int parse_sub_section(CONF_SECTION *parent, rlm_rest_section_t *config, rlm_components_t comp)
580 char const *name = section_type_value[comp].section;
582 cs = cf_section_sub_find(parent, name);
588 if (cf_section_parse(cs, config, section_config) < 0) {
594 * Add section name (Maybe add to headers later?).
601 if ((config->username && !config->password) || (!config->username && config->password)) {
602 cf_log_err_cs(cs, "'username' and 'password' must both be set or both be absent");
608 * Convert HTTP method auth and body type strings into their integer equivalents.
610 config->auth = fr_str2int(http_auth_table, config->auth_str, HTTP_AUTH_UNKNOWN);
611 if (config->auth == HTTP_AUTH_UNKNOWN) {
612 cf_log_err_cs(cs, "Unknown HTTP auth type '%s'", config->auth_str);
615 } else if ((config->auth != HTTP_AUTH_NONE) && !http_curl_auth[config->auth]) {
616 cf_log_err_cs(cs, "Unsupported HTTP auth type \"%s\", check libcurl version, OpenSSL build "
617 "configuration, then recompile this module", config->auth_str);
622 config->method = fr_str2int(http_method_table, config->method_str, HTTP_METHOD_CUSTOM);
625 * We don't have any custom user data, so we need to select the right encoder based
628 * To make this slightly more/less confusing, we accept both canonical body_types,
632 config->body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
633 if (config->body == HTTP_BODY_UNKNOWN) {
634 config->body = fr_str2int(http_content_type_table, config->body_str, HTTP_BODY_UNKNOWN);
637 if (config->body == HTTP_BODY_UNKNOWN) {
638 cf_log_err_cs(cs, "Unknown HTTP body type '%s'", config->body_str);
642 switch (http_body_type_supported[config->body]) {
643 case HTTP_BODY_UNSUPPORTED:
644 cf_log_err_cs(cs, "Unsupported HTTP body type \"%s\", please submit patches",
648 case HTTP_BODY_INVALID:
649 cf_log_err_cs(cs, "Invalid HTTP body type. \"%s\" is not a valid web API data "
650 "markup format", config->body_str);
657 * We have custom body data so we set HTTP_BODY_CUSTOM_XLAT, but also need to try and
658 * figure out what content-type to use. So if they've used the canonical form we
659 * need to convert it back into a proper HTTP content_type value.
662 http_body_type_t body;
664 config->body = HTTP_BODY_CUSTOM_XLAT;
666 body = fr_str2int(http_body_type_table, config->body_str, HTTP_BODY_UNKNOWN);
667 if (body != HTTP_BODY_UNKNOWN) {
668 config->body_str = fr_int2str(http_content_type_table, body, config->body_str);
672 if (config->force_to_str) {
673 config->force_to = fr_str2int(http_body_type_table, config->force_to_str, HTTP_BODY_UNKNOWN);
674 if (config->force_to == HTTP_BODY_UNKNOWN) {
675 config->force_to = fr_str2int(http_content_type_table, config->force_to_str, HTTP_BODY_UNKNOWN);
678 if (config->force_to == HTTP_BODY_UNKNOWN) {
679 cf_log_err_cs(cs, "Unknown forced response body type '%s'", config->force_to_str);
683 switch (http_body_type_supported[config->force_to]) {
684 case HTTP_BODY_UNSUPPORTED:
685 cf_log_err_cs(cs, "Unsupported forced response body type \"%s\", please submit patches",
686 config->force_to_str);
689 case HTTP_BODY_INVALID:
690 cf_log_err_cs(cs, "Invalid HTTP forced response body type. \"%s\" is not a valid web API data "
691 "markup format", config->force_to_str);
703 * Do any per-module initialization that is separate to each
704 * configured instance of the module. e.g. set up connections
705 * to external databases, read configuration files, set up
706 * dictionary entries, etc.
708 * If configuration information is given in the config section
709 * that must be referenced in later calls, store a handle to it
710 * in *instance otherwise put a null pointer there.
712 static int mod_instantiate(CONF_SECTION *conf, void *instance)
714 rlm_rest_t *inst = instance;
715 char const *xlat_name;
717 xlat_name = cf_section_name2(conf);
719 xlat_name = cf_section_name1(conf);
722 inst->xlat_name = xlat_name;
725 * Register the rest xlat function
727 xlat_register(inst->xlat_name, rest_xlat, rest_uri_escape, inst);
730 * Parse sub-section configs.
733 (parse_sub_section(conf, &inst->authorize, RLM_COMPONENT_AUTZ) < 0) ||
734 (parse_sub_section(conf, &inst->authenticate, RLM_COMPONENT_AUTH) < 0) ||
735 (parse_sub_section(conf, &inst->accounting, RLM_COMPONENT_ACCT) < 0) ||
737 /* @todo add behaviour for checksimul */
738 /* (parse_sub_section(conf, &inst->checksimul, RLM_COMPONENT_SESS) < 0) || */
739 (parse_sub_section(conf, &inst->post_auth, RLM_COMPONENT_POST_AUTH) < 0))
745 * Initialise REST libraries.
747 if (rest_init(inst) < 0) {
751 inst->conn_pool = fr_connection_pool_module_init(conf, inst, mod_conn_create, mod_conn_alive, NULL);
752 if (!inst->conn_pool) {
760 * Only free memory we allocated. The strings allocated via
761 * cf_section_parse() do not need to be freed.
763 static int mod_detach(void *instance)
765 rlm_rest_t *inst = instance;
767 fr_connection_pool_delete(inst->conn_pool);
769 xlat_unregister(inst->xlat_name, rest_xlat, instance);
771 /* Free any memory used by libcurl */
778 * The module name should be the only globally exported symbol.
779 * That is, everything else should be 'static'.
781 * If the module needs to temporarily modify it's instantiation
782 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
783 * The server will then take care of ensuring that the module
784 * is single-threaded.
786 module_t rlm_rest = {
789 RLM_TYPE_THREAD_SAFE, /* type */
792 mod_instantiate, /* instantiation */
793 mod_detach, /* detach */
795 mod_authenticate, /* authentication */
796 mod_authorize, /* authorization */
797 NULL, /* preaccounting */
798 mod_accounting, /* accounting */
799 NULL, /* checksimul */
800 NULL, /* pre-proxy */
801 NULL, /* post-proxy */
802 mod_post_auth /* post-auth */