summaryrefslogtreecommitdiff
path: root/res/res_stasis_http.c
diff options
context:
space:
mode:
authorDavid M. Lee <dlee@digium.com>2013-07-03 16:33:13 +0000
committerDavid M. Lee <dlee@digium.com>2013-07-03 16:33:13 +0000
commit9ba976b19c3e553b8ff0715b20894de61520a300 (patch)
treec49720016f46bcc30e643dffc1caed3dafec7bed /res/res_stasis_http.c
parentc9a3d4562ddb1ed5b34f7d5530efd6aa695377c2 (diff)
ARI authentication.
This patch adds authentication support to ARI. Two authentication methods are supported. The first is HTTP Basic authentication, as specified in RFC 2617[1]. The second is by simply passing the username and password as an ?api_key query parameter (which allows swagger-ui[2] to authenticate more easily). ARI usernames and passwords are configured in the ari.conf file (formerly known as stasis_http.conf). The user may be set to `read_only`, which will prohibit the user from issuing POST, DELETE, etc. Also, the user's password may be specified in either plaintext, or encrypted using the crypt() function. Several other notes about the patch. * A few command line commands for seeing ARI config and status were also added. * The configuration parsing grew big enough that I extracted it to its own file. [1]: http://www.ietf.org/rfc/rfc2617.txt [2]: https://github.com/wordnik/swagger-ui (closes issue ASTERISK-21277) Review: https://reviewboard.asterisk.org/r/2649/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@393530 65c4cc65-6c06-0410-ace0-fbb531ad65f3
Diffstat (limited to 'res/res_stasis_http.c')
-rw-r--r--res/res_stasis_http.c281
1 files changed, 154 insertions, 127 deletions
diff --git a/res/res_stasis_http.c b/res/res_stasis_http.c
index 3ff6482b5..4b2e1ccd7 100644
--- a/res/res_stasis_http.c
+++ b/res/res_stasis_http.c
@@ -79,15 +79,31 @@
/*** DOCUMENTATION
<configInfo name="res_stasis_http" language="en_US">
<synopsis>HTTP binding for the Stasis API</synopsis>
- <configFile name="stasis_http.conf">
- <configObject name="global">
- <synopsis>Global configuration settings</synopsis>
+ <configFile name="ari.conf">
+ <configObject name="general">
+ <synopsis>General configuration settings</synopsis>
<configOption name="enabled">
<synopsis>Enable/disable the stasis-http module</synopsis>
</configOption>
<configOption name="pretty">
<synopsis>Responses from stasis-http are formatted to be human readable</synopsis>
</configOption>
+ <configOption name="auth_realm">
+ <synopsis>Realm to use for authentication. Defaults to Asterisk REST Interface.</synopsis>
+ </configOption>
+ </configObject>
+
+ <configObject name="user">
+ <synopsis>Per-user configuration settings</synopsis>
+ <configOption name="read_only">
+ <synopsis>When set to yes, user is only authorized for read-only requests</synopsis>
+ </configOption>
+ <configOption name="password">
+ <synopsis>Crypted or plaintext password (see password_format)</synopsis>
+ </configOption>
+ <configOption name="password_format">
+ <synopsis>password_format may be set to plain (the default) or crypt. When set to crypt, crypt(3) is used to validate the password. A crypted password can be generated using mkpasswd -m sha-512. When set to plain, the password is in plaintext</synopsis>
+ </configOption>
</configObject>
</configFile>
</configInfo>
@@ -97,112 +113,21 @@
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+#include "asterisk/astobj2.h"
#include "asterisk/module.h"
#include "asterisk/paths.h"
#include "asterisk/stasis_http.h"
-#include "asterisk/config_options.h"
+#include "stasis_http/internal.h"
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
-/*! \brief Global configuration options for stasis http. */
-struct conf_global_options {
- /*! Enabled by default, disabled if false. */
- int enabled:1;
- /*! Encoding format used during output (default compact). */
- enum ast_json_encoding_format format;
-};
-
-/*! \brief All configuration options for stasis http. */
-struct conf {
- /*! The general section configuration options. */
- struct conf_global_options *global;
-};
-
-/*! \brief Locking container for safe configuration access. */
-static AO2_GLOBAL_OBJ_STATIC(confs);
-
-/*! \brief Mapping of the stasis http conf struct's globals to the
- * general context in the config file. */
-static struct aco_type global_option = {
- .type = ACO_GLOBAL,
- .name = "global",
- .item_offset = offsetof(struct conf, global),
- .category = "^general$",
- .category_match = ACO_WHITELIST
-};
-
-static struct aco_type *global_options[] = ACO_TYPES(&global_option);
-
-/*! \brief Disposes of the stasis http conf object */
-static void conf_destructor(void *obj)
-{
- struct conf *cfg = obj;
- ao2_cleanup(cfg->global);
-}
-
-/*! \brief Creates the statis http conf object. */
-static void *conf_alloc(void)
-{
- struct conf *cfg;
-
- if (!(cfg = ao2_alloc(sizeof(*cfg), conf_destructor))) {
- return NULL;
- }
-
- if (!(cfg->global = ao2_alloc(sizeof(*cfg->global), NULL))) {
- ao2_ref(cfg, -1);
- return NULL;
- }
- return cfg;
-}
-
-/*! \brief The conf file that's processed for the module. */
-static struct aco_file conf_file = {
- /*! The config file name. */
- .filename = "stasis_http.conf",
- /*! The mapping object types to be processed. */
- .types = ACO_TYPES(&global_option),
-};
-
-CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc,
- .files = ACO_FILES(&conf_file));
-
-/*! \brief Bitfield handler since it is not possible to take address. */
-static int conf_bitfield_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
-{
- struct conf_global_options *global = obj;
-
- if (!strcasecmp(var->name, "enabled")) {
- global->enabled = ast_true(var->value);
- } else {
- return -1;
- }
-
- return 0;
-}
-
-/*! \brief Encoding format handler converts from boolean to enum. */
-static int encoding_format_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
-{
- struct conf_global_options *global = obj;
-
- if (!strcasecmp(var->name, "pretty")) {
- global->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT;
- } else {
- return -1;
- }
-
- return 0;
-}
-
/*! \brief Helper function to check if module is enabled. */
-static char is_enabled(void)
+static int is_enabled(void)
{
- RAII_VAR(struct conf *, cfg, ao2_global_obj_ref(confs), ao2_cleanup);
-
- return cfg->global->enabled;
+ RAII_VAR(struct ari_conf *, cfg, ari_config_get(), ao2_cleanup);
+ return cfg && cfg->general && cfg->general->enabled;
}
/*! Lock for \ref root_handler */
@@ -797,8 +722,67 @@ static void process_cors_request(struct ast_variable *headers,
enum ast_json_encoding_format stasis_http_json_format(void)
{
- RAII_VAR(struct conf *, cfg, ao2_global_obj_ref(confs), ao2_cleanup);
- return cfg->global->format;
+ RAII_VAR(struct ari_conf *, cfg, NULL, ao2_cleanup);
+ cfg = ari_config_get();
+ return cfg->general->format;
+}
+
+/*!
+ * \brief Authenticate a <code>?api_key=userid:password</code>
+ *
+ * \param api_key API key query parameter
+ * \return User object for the authenticated user.
+ * \return \c NULL if authentication failed.
+ */
+static struct ari_conf_user *authenticate_api_key(const char *api_key)
+{
+ RAII_VAR(char *, copy, NULL, ast_free);
+ char *username;
+ char *password;
+
+ password = copy = ast_strdup(api_key);
+ if (!copy) {
+ return NULL;
+ }
+
+ username = strsep(&password, ":");
+ if (!password) {
+ ast_log(LOG_WARNING, "Invalid api_key\n");
+ return NULL;
+ }
+
+ return ari_config_validate_user(username, password);
+}
+
+/*!
+ * \brief Authenticate an HTTP request.
+ *
+ * \param get_params GET parameters of the request.
+ * \param header HTTP headers.
+ * \return User object for the authenticated user.
+ * \return \c NULL if authentication failed.
+ */
+static struct ari_conf_user *authenticate_user(struct ast_variable *get_params,
+ struct ast_variable *headers)
+{
+ RAII_VAR(struct ast_http_auth *, http_auth, NULL, ao2_cleanup);
+ struct ast_variable *v;
+
+ /* HTTP Basic authentication */
+ http_auth = ast_http_get_auth(headers);
+ if (http_auth) {
+ return ari_config_validate_user(http_auth->userid,
+ http_auth->password);
+ }
+
+ /* ?api_key authentication */
+ for (v = get_params; v; v = v->next) {
+ if (strcasecmp("api_key", v->name) == 0) {
+ return authenticate_api_key(v->value);
+ }
+ }
+
+ return NULL;
}
/*!
@@ -822,8 +806,10 @@ static int stasis_http_callback(struct ast_tcptls_session_instance *ser,
struct ast_variable *get_params,
struct ast_variable *headers)
{
+ RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ast_str *, response_headers, ast_str_create(40), ast_free);
RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free);
+ RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
struct stasis_http_response response = {};
int ret = 0;
@@ -832,10 +818,45 @@ static int stasis_http_callback(struct ast_tcptls_session_instance *ser,
}
response.headers = ast_str_create(40);
+ if (!response.headers) {
+ return -1;
+ }
+
+ conf = ari_config_get();
+ if (!conf || !conf->general) {
+ return -1;
+ }
process_cors_request(headers, &response);
- if (ast_ends_with(uri, "/")) {
+ user = authenticate_user(get_params, headers);
+ if (!user) {
+ /* Per RFC 2617, section 1.2: The 401 (Unauthorized) response
+ * message is used by an origin server to challenge the
+ * authorization of a user agent. This response MUST include a
+ * WWW-Authenticate header field containing at least one
+ * challenge applicable to the requested resource.
+ */
+ response.response_code = 401;
+ response.response_text = "Unauthorized";
+
+ /* Section 1.2:
+ * realm = "realm" "=" realm-value
+ * realm-value = quoted-string
+ * Section 2:
+ * challenge = "Basic" realm
+ */
+ ast_str_append(&response.headers, 0,
+ "WWW-Authenticate: Basic realm=\"%s\"\r\n",
+ conf->general->auth_realm);
+ response.message = ast_json_pack("{s: s}",
+ "error", "Authentication required");
+ } else if (user->read_only && method != AST_HTTP_GET && method != AST_HTTP_OPTIONS) {
+ response.message = ast_json_pack("{s: s}",
+ "error", "Write access denied");
+ response.response_code = 403;
+ response.response_text = "Forbidden";
+ } else if (ast_ends_with(uri, "/")) {
remove_trailing_slash(uri, &response);
} else if (ast_begins_with(uri, "api-docs/")) {
/* Serving up API docs */
@@ -875,7 +896,8 @@ static int stasis_http_callback(struct ast_tcptls_session_instance *ser,
if (response.message && !ast_json_is_null(response.message)) {
ast_str_append(&response_headers, 0,
"Content-type: application/json\r\n");
- if (ast_json_dump_str_format(response.message, &response_body, stasis_http_json_format()) != 0) {
+ if (ast_json_dump_str_format(response.message, &response_body,
+ conf->general->format) != 0) {
/* Error encoding response */
response.response_code = 500;
response.response_text = "Internal Server Error";
@@ -909,38 +931,39 @@ static struct ast_http_uri http_uri = {
static int load_module(void)
{
- oom_json = ast_json_pack(
- "{s: s}", "error", "AllocationFailed");
-
- if (!oom_json) {
- /* Ironic */
- return AST_MODULE_LOAD_FAILURE;
- }
-
ast_mutex_init(&root_handler_lock);
- root_handler = root_handler_create();
+ /* root_handler may have been built during a declined load */
+ if (!root_handler) {
+ root_handler = root_handler_create();
+ }
if (!root_handler) {
return AST_MODULE_LOAD_FAILURE;
}
- if (aco_info_init(&cfg_info)) {
- aco_info_destroy(&cfg_info);
- return AST_MODULE_LOAD_DECLINE;
+ /* oom_json may have been built during a declined load */
+ if (!oom_json) {
+ oom_json = ast_json_pack(
+ "{s: s}", "error", "Allocation failed");
+ }
+ if (!oom_json) {
+ /* Ironic */
+ return AST_MODULE_LOAD_FAILURE;
}
- aco_option_register_custom(&cfg_info, "enabled", ACO_EXACT, global_options,
- "yes", conf_bitfield_handler, 0);
- aco_option_register_custom(&cfg_info, "pretty", ACO_EXACT, global_options,
- "no", encoding_format_handler, 0);
-
- if (aco_process_config(&cfg_info, 0)) {
- aco_info_destroy(&cfg_info);
+ if (ari_config_init() != 0) {
return AST_MODULE_LOAD_DECLINE;
}
if (is_enabled()) {
+ ast_debug(3, "ARI enabled\n");
ast_http_uri_link(&http_uri);
+ } else {
+ ast_debug(3, "ARI disabled\n");
+ }
+
+ if (ari_cli_register() != 0) {
+ return AST_MODULE_LOAD_FAILURE;
}
return AST_MODULE_LOAD_SUCCESS;
@@ -948,20 +971,22 @@ static int load_module(void)
static int unload_module(void)
{
- ast_json_unref(oom_json);
- oom_json = NULL;
+ ari_cli_unregister();
if (is_enabled()) {
+ ast_debug(3, "Disabling ARI\n");
ast_http_uri_unlink(&http_uri);
}
- aco_info_destroy(&cfg_info);
- ao2_global_obj_release(confs);
+ ari_config_destroy();
ao2_cleanup(root_handler);
root_handler = NULL;
ast_mutex_destroy(&root_handler_lock);
+ ast_json_unref(oom_json);
+ oom_json = NULL;
+
return 0;
}
@@ -969,13 +994,15 @@ static int reload_module(void)
{
char was_enabled = is_enabled();
- if (aco_process_config(&cfg_info, 1)) {
+ if (ari_config_reload() != 0) {
return AST_MODULE_LOAD_DECLINE;
}
if (was_enabled && !is_enabled()) {
+ ast_debug(3, "Disabling ARI\n");
ast_http_uri_unlink(&http_uri);
} else if (!was_enabled && is_enabled()) {
+ ast_debug(3, "Enabling ARI\n");
ast_http_uri_link(&http_uri);
}