diff --git a/configs/ari.conf.sample b/configs/ari.conf.sample new file mode 100644 index 0000000000000000000000000000000000000000..11e2b065e77ab54e305ae3dea023bf5ab4220187 --- /dev/null +++ b/configs/ari.conf.sample @@ -0,0 +1,23 @@ +[general] +enabled = yes ; When set to no, stasis-http support is disabled. +;pretty = no ; When set to yes, responses from stasis-http are +; ; formatted to be human readable. +;allowed_origins = ; Comma separated list of allowed origins, for +; ; Cross-Origin Resource Sharing. May be set to * to +; ; allow all origins. +;auth_realm = ; Realm to use for authentication. Defaults to Asterisk +; ; REST Interface. + +;[user-username] +;read_only = no ; When set to yes, user is only authorized for +; ; read-only requests. +; +;password = ; Crypted or plaintext password (see password_format). +; +; 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. +; +;password_format = plain diff --git a/configs/stasis_http.conf.sample b/configs/stasis_http.conf.sample deleted file mode 100644 index 1527a32be2a324226e6028f1f58f974623a0811e..0000000000000000000000000000000000000000 --- a/configs/stasis_http.conf.sample +++ /dev/null @@ -1,25 +0,0 @@ -[general] -enabled = yes ; When set to no, stasis-http support is disabled -;pretty = no ; When set to yes, responses from stasis-http are -; ; formatted to be human readable -;allowed_origins = ; Comma separated list of allowed origins, for -; ; Cross-Origin Resource Sharing. May be set to * to allow -; ; all origins. - -;[user-username] -;read_only = no ; When set to yes, user is only authorized for -; ; read-only requests -; -; If a password is specified, user must authenticate using HTTP Basic -; authentication. If no password is specified, then the user may authenticate -; simply by adding ?api_key=username to their requests. -; -;password = ; Crypted or plaintext password (see crypt_password) -; -; crypt_password may be set to crypt (the default) or plain. When set to crypt, -; crypt(3) is used to encrypt the password. A crypted password can be generated -; using mkpasswd -m sha-512. -; -; When set to plain, the password is in plaintext -; -;crypt_password = plain diff --git a/main/Makefile b/main/Makefile index 2949653651ee62ad537d226f9c5451e0027bac96..62ae6d4fb3b96a2a48e2c105aa889627a5d64267 100644 --- a/main/Makefile +++ b/main/Makefile @@ -37,6 +37,7 @@ AST_LIBS+=$(SQLITE3_LIB) AST_LIBS+=$(ASTSSL_LIBS) AST_LIBS+=$(JANSSON_LIB) AST_LIBS+=$(UUID_LIB) +AST_LIBS+=$(CRYPT_LIB) ifneq ($(findstring $(OSARCH), linux-gnu uclinux linux-uclibc kfreebsd-gnu),) ifneq ($(findstring LOADABLE_MODULES,$(MENUSELECT_CFLAGS)),) @@ -153,6 +154,7 @@ db.o: _ASTCFLAGS+=$(SQLITE3_INCLUDE) asterisk.o: _ASTCFLAGS+=$(LIBEDIT_INCLUDE) cli.o: _ASTCFLAGS+=$(LIBEDIT_INCLUDE) json.o: _ASTCFLAGS+=$(JANSSON_INCLUDE) +util.o: _ASTCFLAGS+=$(CRYPT_INCLUDE) uuid.o: _ASTCFLAGS+=$(UUID_INCLUDE) ifneq ($(findstring ENABLE_UPLOADS,$(MENUSELECT_CFLAGS)),) diff --git a/main/http.c b/main/http.c index c7dc623a5cc047de8dd2509c81e11629b46bf571..d459eb1841dd84a872d468204486179d89b6a67b 100644 --- a/main/http.c +++ b/main/http.c @@ -867,6 +867,93 @@ struct ast_variable *ast_http_get_cookies(struct ast_variable *headers) return cookies; } +static struct ast_http_auth *auth_create(const char *userid, + const char *password) +{ + RAII_VAR(struct ast_http_auth *, auth, NULL, ao2_cleanup); + size_t userid_len; + size_t password_len; + + if (!userid || !password) { + ast_log(LOG_ERROR, "Invalid userid/password\n"); + return NULL; + } + + userid_len = strlen(userid) + 1; + password_len = strlen(password) + 1; + + /* Allocate enough room to store everything in one memory block */ + auth = ao2_alloc(sizeof(*auth) + userid_len + password_len, NULL); + if (!auth) { + return NULL; + } + + /* Put the userid right after the struct */ + auth->userid = (char *)(auth + 1); + strcpy(auth->userid, userid); + + /* Put the password right after the userid */ + auth->password = auth->userid + userid_len; + strcpy(auth->password, password); + + ao2_ref(auth, +1); + return auth; +} + +#define BASIC_PREFIX "Basic " +#define BASIC_LEN 6 /*!< strlen(BASIC_PREFIX) */ + +struct ast_http_auth *ast_http_get_auth(struct ast_variable *headers) +{ + struct ast_variable *v; + + for (v = headers; v; v = v->next) { + const char *base64; + char decoded[256] = {}; + char *username; + char *password; + int cnt; + + if (strcasecmp("Authorization", v->name) != 0) { + continue; + } + + if (!ast_begins_with(v->value, BASIC_PREFIX)) { + ast_log(LOG_DEBUG, + "Unsupported Authorization scheme\n"); + continue; + } + + /* Basic auth header parsing. RFC 2617, section 2. + * credentials = "Basic" basic-credentials + * basic-credentials = base64-user-pass + * base64-user-pass = <base64 encoding of user-pass, + * except not limited to 76 char/line> + * user-pass = userid ":" password + */ + + base64 = v->value + BASIC_LEN; + + /* This will truncate "userid:password" lines to + * sizeof(decoded). The array is long enough that this shouldn't + * be a problem */ + cnt = ast_base64decode((unsigned char*)decoded, base64, + sizeof(decoded) - 1); + ast_assert(cnt < sizeof(decoded)); + + /* Split the string at the colon */ + password = decoded; + username = strsep(&password, ":"); + if (!password) { + ast_log(LOG_WARNING, "Invalid Authorization header\n"); + return NULL; + } + + return auth_create(username, password); + } + + return NULL; +} static void *httpd_helper_thread(void *data) { diff --git a/main/utils.c b/main/utils.c index 1007254875eae441ac19113dfd4474393a872976..208a4d32614d01f0bab6fdbe808ed8cb3c94ceb6 100644 --- a/main/utils.c +++ b/main/utils.c @@ -32,12 +32,14 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include <ctype.h> +#include <fcntl.h> #include <sys/stat.h> #include <sys/stat.h> - -#include <fcntl.h> - #include <sys/syscall.h> +#include <unistd.h> +#if defined(HAVE_CRYPT_R) +#include <crypt.h> +#endif #if defined(__APPLE__) #include <mach/mach.h> #elif defined(HAVE_SYS_THR_H) @@ -2271,6 +2273,171 @@ int ast_get_tid(void) return ret; } +/*! + * \brief Max length of a salt string. + * + * $[1,5,6]$[a–zA–Z0–9./]{1,16}$, plus null terminator + */ +#define MAX_SALT_LEN 21 + +static char salt_chars[] = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "./"; + +/*! Randomly select a character for a salt string */ +static char gen_salt_char(void) +{ + int which = ast_random_double() * 64; + return salt_chars[which]; +} + +/*! + * \brief Generates a salt to try with crypt. + * + * If given an empty string, will generate a salt for the most secure algorithm + * to try with crypt(). If given a previously generated salt, the algorithm will + * be lowered by one level of security. + * + * \param[out] current_salt Output string in which to generate the salt. + * This can be an empty string, or the results of a + * prior gen_salt call. + * \param max_len Length of \a current_salt. + * \return 0 on success. + * \return Non-zero on error. + */ +static int gen_salt(char *current_salt, size_t maxlen) +{ + int i; + + if (maxlen < MAX_SALT_LEN || current_salt == NULL) { + return -1; + } + + switch (current_salt[0]) { + case '\0': + /* Initial generation; $6$ = SHA-512 */ + *current_salt++ = '$'; + *current_salt++ = '6'; + *current_salt++ = '$'; + for (i = 0; i < 16; ++i) { + *current_salt++ = gen_salt_char(); + } + *current_salt++ = '$'; + *current_salt++ = '\0'; + return 0; + case '$': + switch (current_salt[1]) { + case '6': + /* Downgrade to SHA-256 */ + current_salt[1] = '5'; + return 0; + case '5': + /* Downgrade to MD5 */ + current_salt[1] = '1'; + return 0; + case '1': + /* Downgrade to traditional crypt */ + *current_salt++ = gen_salt_char(); + *current_salt++ = gen_salt_char(); + *current_salt++ = '\0'; + return 0; + default: + /* Unrecognized algorithm */ + return -1; + } + default: + /* Was already as insecure as it gets */ + return -1; + } + +} + +#if defined(HAVE_CRYPT_R) + +char *ast_crypt(const char *key, const char *salt) +{ + struct crypt_data data = {}; + const char *crypted = crypt_r(key, salt, &data); + + /* Crypt may return success even if it doesn't recognize the salt. But + * in those cases it always mangles the salt in some way. + */ + if (!crypted || !ast_begins_with(crypted, salt)) { + return NULL; + } + + return ast_strdup(crypted); +} + +int ast_crypt_validate(const char *key, const char *expected) +{ + struct crypt_data data = {}; + return strcmp(expected, crypt_r(key, expected, &data)) == 0; +} + +#elif defined(HAVE_CRYPT) + +/* crypt is not reentrant. A global mutex is neither ideal nor perfect, but good + * enough if crypt_r support is unavailable + */ +AST_MUTEX_DEFINE_STATIC(crypt_mutex); + +char *ast_crypt(const char *key, const char *salt) +{ + const char *crypted; + SCOPED_MUTEX(lock, &crypt_mutex); + + crypted = crypt(key, salt); + + /* Crypt may return success even if it doesn't recognize the salt. But + * in those cases it always mangles the salt in some way. + */ + if (!crypted || !ast_begins_with(crypted, salt)) { + return NULL; + } + + return ast_strdup(crypted); +} + +int ast_crypt_validate(const char *key, const char *expected) +{ + SCOPED_MUTEX(lock, &crypt_mutex); + return strcmp(expected, crypt(key, expected)) == 0; +} + +#else /* No crypt support */ + +char *ast_crypt(const char *key, const char *salt) +{ + ast_log(LOG_WARNING, + "crypt() support not available; cannot encrypt password\n"); + return NULL; +} + +int ast_crypt_validate(const char *key, const char *expected) +{ + ast_log(LOG_WARNING, + "crypt() support not available; cannot validate password\n"); + return 0; +} + +#endif /* No crypt support */ + +char *ast_crypt_encrypt(const char *key) +{ + char salt[MAX_SALT_LEN] = {}; + while (gen_salt(salt, sizeof(salt)) == 0) { + char *crypted = ast_crypt(key, salt); + if (crypted) { + return crypted; + } + } + return NULL; +} + + char *ast_utils_which(const char *binary, char *fullpath, size_t fullpath_size) { const char *envPATH = getenv("PATH"); diff --git a/makeopts.in b/makeopts.in index 95e69b817f97ad03ba6f311be3783b374d768bc6..401e8f1af0bb4cfaee7430b759727a7b858ab46c 100644 --- a/makeopts.in +++ b/makeopts.in @@ -281,6 +281,9 @@ SRTP_INCLUDE=@SRTP_INCLUDE@ OPENSSL_INCLUDE=@OPENSSL_INCLUDE@ OPENSSL_LIB=@OPENSSL_LIB@ +CRYPT_INCLUDE=@CRYPT_INCLUDE@ +CRYPT_LIB=@CRYPT_LIB@ + CRYPTO_INCLUDE=@CRYPTO_INCLUDE@ CRYPTO_LIB=@CRYPTO_LIB@ diff --git a/res/Makefile b/res/Makefile index 1310dae3a08666d191f13296564a3fd7f6226126..588bc2e726f5968e91ae532c1578e76fcd3ca9e5 100644 --- a/res/Makefile +++ b/res/Makefile @@ -80,8 +80,8 @@ clean:: $(if $(filter res_parking,$(EMBEDDED_MODS)),modules.link,res_parking.so): $(subst .c,.o,$(wildcard parking/*.c)) $(subst .c,.o,$(wildcard parking/*.c)): _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_parking) -res_stasis_http.so: stasis_http/ari_websockets.o -stasis_http/ari_websockets.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_stasis_http_asterisk) +res_stasis_http.so: stasis_http/cli.o stasis_http/config.o stasis_http/ari_websockets.o +stasis_http/cli.o stasis_http/config.o stasis_http/ari_websockets.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_stasis_http) res_ari_model.so: stasis_http/ari_model_validators.o stasis_http/ari_model_validators.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_model) diff --git a/res/res_stasis_http.c b/res/res_stasis_http.c index 3ff6482b5bcd1daaa5e20c0670d7b09da35682f3..4b2e1ccd75c25b32abe2264c9be8abaeed45aab3 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); } diff --git a/res/stasis_http/cli.c b/res/stasis_http/cli.c new file mode 100644 index 0000000000000000000000000000000000000000..98d082b2c62394f55f7dbf3e16dcdb8477a88659 --- /dev/null +++ b/res/stasis_http/cli.c @@ -0,0 +1,266 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II <dlee@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Command line for ARI. + * \author David M. Lee, II <dlee@digium.com> + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/astobj2.h" +#include "asterisk/cli.h" +#include "internal.h" + +static char *ari_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup); + + switch (cmd) { + case CLI_INIT: + e->command = "ari show status"; + e->usage = + "Usage: ari show status\n" + " Shows all ARI settings\n"; + return NULL; + case CLI_GENERATE: + return NULL; + default: + break; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + conf = ari_config_get(); + + if (!conf) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "ARI Status:\n"); + ast_cli(a->fd, "Enabled: %s\n", AST_CLI_YESNO(conf->general->enabled)); + ast_cli(a->fd, "Output format: "); + switch (conf->general->format) { + case AST_JSON_COMPACT: + ast_cli(a->fd, "compact"); + break; + case AST_JSON_PRETTY: + ast_cli(a->fd, "pretty"); + break; + } + ast_cli(a->fd, "\n"); + ast_cli(a->fd, "Auth realm: %s\n", conf->general->auth_realm); + ast_cli(a->fd, "User count: %d\n", ao2_container_count(conf->users)); + return CLI_SUCCESS; +} + +static int show_users_cb(void *obj, void *arg, int flags) +{ + struct ari_conf_user *user = obj; + struct ast_cli_args *a = arg; + + ast_cli(a->fd, "%-4s %s\n", + AST_CLI_YESNO(user->read_only), + user->username); + return 0; +} + +static char *ari_show_users(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup); + + switch (cmd) { + case CLI_INIT: + e->command = "ari show users"; + e->usage = + "Usage: ari show users\n" + " Shows all ARI users\n"; + return NULL; + case CLI_GENERATE: + return NULL; + default: + break; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + conf = ari_config_get(); + if (!conf) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "r/o? Username\n"); + ast_cli(a->fd, "---- --------\n"); + + ao2_callback(conf->users, OBJ_NODATA, show_users_cb, a); + + return CLI_SUCCESS; +} + +struct user_complete { + /*! Nth user to search for */ + int state; + /*! Which user currently on */ + int which; +}; + +static int complete_ari_user_search(void *obj, void *arg, void *data, int flags) +{ + struct user_complete *search = data; + + if (++search->which > search->state) { + return CMP_MATCH; + } + return 0; +} + +static char *complete_ari_user(struct ast_cli_args *a) +{ + RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup); + RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); + + struct user_complete search = { + .state = a->n, + }; + + conf = ari_config_get(); + if (!conf) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + + user = ao2_callback_data(conf->users, + ast_strlen_zero(a->word) ? 0 : OBJ_PARTIAL_KEY, + complete_ari_user_search, (char*)a->word, &search); + + return user ? ast_strdup(user->username) : NULL; +} + +static char *complete_ari_show_user(struct ast_cli_args *a) +{ + if (a->pos == 3) { + return complete_ari_user(a); + } + + return NULL; +} + +static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup); + RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); + + switch (cmd) { + case CLI_INIT: + e->command = "ari show user"; + e->usage = + "Usage: ari show user <username>\n" + " Shows a specific ARI user\n"; + return NULL; + case CLI_GENERATE: + return complete_ari_show_user(a); + default: + break; + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + conf = ari_config_get(); + + if (!conf) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + + user = ao2_find(conf->users, a->argv[3], OBJ_KEY); + if (!user) { + ast_cli(a->fd, "User '%s' not found\n", a->argv[3]); + return CLI_SUCCESS; + } + + ast_cli(a->fd, "Username: %s\n", user->username); + ast_cli(a->fd, "Read only?: %s\n", AST_CLI_YESNO(user->read_only)); + + return CLI_SUCCESS; +} + +static char *ari_mkpasswd(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(char *, crypted, NULL, ast_free); + + switch (cmd) { + case CLI_INIT: + e->command = "ari mkpasswd"; + e->usage = + "Usage: ari mkpasswd <password>\n" + " Encrypts a password for use in ari.conf\n" + " Be aware that the password will be shown in the\n" + " command line history. The mkpasswd shell command\n" + " may be preferable.\n" + ; + return NULL; + case CLI_GENERATE: + return NULL; + default: + break; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + crypted = ast_crypt_encrypt(a->argv[2]); + if (!crypted) { + ast_cli(a->fd, "Failed to encrypt password\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, + "; Copy the following two lines into ari.conf\n"); + ast_cli(a->fd, "password_format = crypt\n"); + ast_cli(a->fd, "password = %s\n", crypted); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_ari[] = { + AST_CLI_DEFINE(ari_show, "Show ARI settings"), + AST_CLI_DEFINE(ari_show_users, "List ARI users"), + AST_CLI_DEFINE(ari_show_user, "List single ARI user"), + AST_CLI_DEFINE(ari_mkpasswd, "Encrypts a password"), +}; + +int ari_cli_register(void) { + return ast_cli_register_multiple(cli_ari, ARRAY_LEN(cli_ari)); +} + +void ari_cli_unregister(void) { + ast_cli_unregister_multiple(cli_ari, ARRAY_LEN(cli_ari)); +} diff --git a/res/stasis_http/config.c b/res/stasis_http/config.c new file mode 100644 index 0000000000000000000000000000000000000000..f02fabea450c1233d4e84a7589676becea533db9 --- /dev/null +++ b/res/stasis_http/config.c @@ -0,0 +1,341 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II <dlee@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Config framework stuffz for ARI. + * \author David M. Lee, II <dlee@digium.com> + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/config_options.h" +#include "internal.h" + +/*! \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 general_option = { + .type = ACO_GLOBAL, + .name = "general", + .item_offset = offsetof(struct ari_conf, general), + .category = "^general$", + .category_match = ACO_WHITELIST, +}; + +static struct aco_type *general_options[] = ACO_TYPES(&general_option); + +/*! \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 ari_conf_general *general = obj; + + if (!strcasecmp(var->name, "pretty")) { + general->format = ast_true(var->value) ? + AST_JSON_PRETTY : AST_JSON_COMPACT; + } else { + return -1; + } + + return 0; +} + +/*! \brief Parses the ari_password_format enum from a config file */ +static int password_format_handler(const struct aco_option *opt, + struct ast_variable *var, void *obj) +{ + struct ari_conf_user *user = obj; + + if (strcasecmp(var->value, "plain") == 0) { + user->password_format = ARI_PASSWORD_FORMAT_PLAIN; + } else if (strcasecmp(var->value, "crypt") == 0) { + user->password_format = ARI_PASSWORD_FORMAT_CRYPT; + } else { + return -1; + } + + return 0; +} + +/*! \brief Destructor for \ref ari_conf_user */ +static void user_dtor(void *obj) +{ + struct ari_conf_user *user = obj; + ast_debug(3, "Disposing of user %s\n", user->username); + ast_free(user->username); +} + +/*! \brief Allocate an \ref ari_conf_user for config parsing */ +static void *user_alloc(const char *cat) +{ + RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); + const char *username; + + if (!cat) { + return NULL; + } + + username = strchr(cat, '-') + 1; + + if (!username) { + ast_log(LOG_ERROR, "Invalid user category '%s'\n", cat); + return NULL; + } + + ast_debug(3, "Allocating user %s\n", cat); + + user = ao2_alloc_options(sizeof(*user), user_dtor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!user) { + return NULL; + } + + user->username = ast_strdup(username); + if (!user->username) { + return NULL; + } + + ao2_ref(user, +1); + return user; +} + +/*! \brief Sorting function for use with red/black tree */ +static int user_sort_cmp(const void *obj_left, const void *obj_right, int flags) +{ + const struct ari_conf_user *user_left = obj_left; + + if (flags & OBJ_PARTIAL_KEY) { + const char *key_right = obj_right; + return strncasecmp(user_left->username, key_right, + strlen(key_right)); + } else if (flags & OBJ_KEY) { + const char *key_right = obj_right; + return strcasecmp(user_left->username, key_right); + } else { + const struct ari_conf_user *user_right = obj_right; + const char *key_right = user_right->username; + return strcasecmp(user_left->username, key_right); + } +} + +/*! \brief \ref aco_type item_find function */ +static void *user_find(struct ao2_container *tmp_container, const char *cat) +{ + const char *username; + + if (!cat) { + return NULL; + } + + username = strchr(cat, '-') + 1; + return ao2_find(tmp_container, username, OBJ_KEY); +} + +static struct aco_type user_option = { + .type = ACO_ITEM, + .name = "user", + .category_match = ACO_WHITELIST, + .category = "^user-.+$", + .item_alloc = user_alloc, + .item_find = user_find, + .item_offset = offsetof(struct ari_conf, users), +}; + +static struct aco_type *user[] = ACO_TYPES(&user_option); + +/*! \brief \ref ari_conf destructor. */ +static void conf_destructor(void *obj) +{ + struct ari_conf *cfg = obj; + ao2_cleanup(cfg->general); + ao2_cleanup(cfg->users); +} + +/*! \brief Allocate an \ref ari_conf for config parsing */ +static void *conf_alloc(void) +{ + RAII_VAR(struct ari_conf *, cfg, NULL, ao2_cleanup); + + cfg = ao2_alloc_options(sizeof(*cfg), conf_destructor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!cfg) { + return NULL; + } + + cfg->general = ao2_alloc_options(sizeof(*cfg->general), NULL, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!cfg->general) { + return NULL; + } + + cfg->users = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, + AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, user_sort_cmp, NULL); + + ao2_ref(cfg, +1); + return cfg; +} + +#define CONF_FILENAME "ari.conf" + +/*! \brief The conf file that's processed for the module. */ +static struct aco_file conf_file = { + /*! The config file name. */ + .filename = CONF_FILENAME, + /*! The mapping object types to be processed. */ + .types = ACO_TYPES(&general_option, &user_option), +}; + +CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc, + .files = ACO_FILES(&conf_file)); + +struct ari_conf *ari_config_get(void) +{ + struct ari_conf *res = ao2_global_obj_ref(confs); + if (!res) { + ast_log(LOG_ERROR, + "Error obtaining config from " CONF_FILENAME "\n"); + } + return res; +} + +struct ari_conf_user *ari_config_validate_user(const char *username, + const char *password) +{ + RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup); + RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); + int is_valid = 0; + + conf = ari_config_get(); + if (!conf) { + return NULL; + } + + user = ao2_find(conf->users, username, OBJ_KEY); + if (!user) { + return NULL; + } + + if (ast_strlen_zero(user->password)) { + ast_log(LOG_WARNING, + "User '%s' missing password; authentication failed\n", + user->username); + return NULL; + } + + switch (user->password_format) { + case ARI_PASSWORD_FORMAT_PLAIN: + is_valid = strcmp(password, user->password) == 0; + break; + case ARI_PASSWORD_FORMAT_CRYPT: + is_valid = ast_crypt_validate(password, user->password); + break; + } + + if (!is_valid) { + return NULL; + } + + ao2_ref(user, +1); + return user; +} + +/*! \brief Callback to validate a user object */ +static int validate_user_cb(void *obj, void *arg, int flags) +{ + struct ari_conf_user *user = obj; + + if (ast_strlen_zero(user->password)) { + ast_log(LOG_WARNING, "User '%s' missing password\n", + user->username); + } + + return 0; +} + +/*! \brief Load (or reload) configuration. */ +static int process_config(int reload) +{ + RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup); + + switch (aco_process_config(&cfg_info, reload)) { + case ACO_PROCESS_ERROR: + return -1; + case ACO_PROCESS_OK: + case ACO_PROCESS_UNCHANGED: + break; + } + + conf = ari_config_get(); + if (!conf) { + ast_assert(0); /* We just configured; it should be there */ + return -1; + } + + if (ao2_container_count(conf->users) == 0) { + ast_log(LOG_ERROR, "No configured users for ARI\n"); + } + + ao2_callback(conf->users, OBJ_NODATA, validate_user_cb, NULL); + + return 0; +} + +int ari_config_init(void) +{ + if (aco_info_init(&cfg_info)) { + aco_info_destroy(&cfg_info); + return -1; + } + + aco_option_register(&cfg_info, "enabled", ACO_EXACT, general_options, + "yes", OPT_BOOL_T, 1, + FLDSET(struct ari_conf_general, enabled)); + aco_option_register_custom(&cfg_info, "pretty", ACO_EXACT, + general_options, "no", encoding_format_handler, 0); + aco_option_register(&cfg_info, "auth_realm", ACO_EXACT, general_options, + "Asterisk REST Interface", OPT_CHAR_ARRAY_T, 0, + FLDSET(struct ari_conf_general, auth_realm), + ARI_AUTH_REALM_LEN); + + aco_option_register(&cfg_info, "read_only", ACO_EXACT, user, + "no", OPT_BOOL_T, 1, + FLDSET(struct ari_conf_user, read_only)); + aco_option_register(&cfg_info, "password", ACO_EXACT, user, + "", OPT_CHAR_ARRAY_T, 0, + FLDSET(struct ari_conf_user, password), ARI_PASSWORD_LEN); + aco_option_register_custom(&cfg_info, "password_format", ACO_EXACT, + user, "plain", password_format_handler, 0); + + return process_config(0); +} + +int ari_config_reload(void) +{ + return process_config(1); +} + +void ari_config_destroy(void) +{ + aco_info_destroy(&cfg_info); + ao2_global_obj_release(confs); +} diff --git a/res/stasis_http/internal.h b/res/stasis_http/internal.h new file mode 100644 index 0000000000000000000000000000000000000000..659f4a2aeed2744570c9724d1a1aa89294eef681 --- /dev/null +++ b/res/stasis_http/internal.h @@ -0,0 +1,139 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II <dlee@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef STASIS_HTTP_INTERNAL_H_ +#define STASIS_HTTP_INTERNAL_H_ + +/*! \file + * + * \brief Internal API's for res_stasis_http. + * \author David M. Lee, II <dlee@digium.com> + */ + +#include "asterisk/json.h" + +/*! @{ */ + +/*! + * \brief Register CLI commands for ARI. + * + * \return 0 on success. + * \return Non-zero on error. + */ +int ari_cli_register(void); + +/*! + * \brief Unregister CLI commands for ARI. + */ +void ari_cli_unregister(void); + +/*! @} */ + +/*! @{ */ + +struct ari_conf_general; + +/*! \brief All configuration options for stasis http. */ +struct ari_conf { + /*! The general section configuration options. */ + struct ari_conf_general *general; + /*! Configured users */ + struct ao2_container *users; +}; + +/*! Max length for auth_realm field */ +#define ARI_AUTH_REALM_LEN 80 + +/*! \brief Global configuration options for stasis http. */ +struct ari_conf_general { + /*! Enabled by default, disabled if false. */ + int enabled; + /*! Encoding format used during output (default compact). */ + enum ast_json_encoding_format format; + /*! Authentication realm */ + char auth_realm[ARI_AUTH_REALM_LEN]; +}; + +/*! \brief Password format */ +enum ari_password_format { + /*! \brief Plaintext password */ + ARI_PASSWORD_FORMAT_PLAIN, + /*! crypt(3) password */ + ARI_PASSWORD_FORMAT_CRYPT, +}; + +/*! + * \brief User's password mx length. + * + * If 256 seems like a lot, a crypt SHA-512 has over 106 characters. + */ +#define ARI_PASSWORD_LEN 256 + +/*! \brief Per-user configuration options */ +struct ari_conf_user { + /*! Username for authentication */ + char *username; + /*! User's password. */ + char password[ARI_PASSWORD_LEN]; + /*! Format for the password field */ + enum ari_password_format password_format; + /*! If true, user cannot execute change operations */ + int read_only; +}; + +/*! + * \brief Initialize the ARI configuration + */ +int ari_config_init(void); + +/*! + * \brief Reload the ARI configuration + */ +int ari_config_reload(void); + +/*! + * \brief Destroy the ARI configuration + */ +void ari_config_destroy(void); + +/*! + * \brief Get the current ARI configuration. + * + * This is an immutable object, so don't modify it. It is AO2 managed, so + * ao2_cleanup() when you're done with it. + * + * \return ARI configuration object. + * \return \c NULL on error. + */ +struct ari_conf *ari_config_get(void); + +/*! + * \brief Validated a user's credentials. + * + * \param username Name of the user. + * \param password User's password. + * \return User object. + * \return \c NULL if username or password is invalid. + */ +struct ari_conf_user *ari_config_validate_user(const char *username, + const char *password); + +/*! @} */ + + +#endif /* STASIS_HTTP_INTERNAL_H_ */