mirror of https://github.com/asterisk/asterisk
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
863 lines
24 KiB
863 lines
24 KiB
/*
|
|
* 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 <limits.h>
|
|
|
|
#include "asterisk.h"
|
|
|
|
#include "asterisk/sorcery.h"
|
|
#include "asterisk/config_options.h"
|
|
#include "asterisk/http_websocket.h"
|
|
#include "asterisk/app.h"
|
|
#include "asterisk/channel.h"
|
|
#include "asterisk/vector.h"
|
|
#include "internal.h"
|
|
|
|
static struct ast_sorcery *sorcery;
|
|
|
|
struct outbound_websocket_state {
|
|
enum ari_conf_owc_fields invalid_fields;
|
|
char id[0];
|
|
};
|
|
|
|
#define OWC_STATES_BUCKETS 13
|
|
struct ao2_container *owc_states = NULL;
|
|
|
|
static void outbound_websocket_dtor(void *obj)
|
|
{
|
|
struct ari_conf_outbound_websocket *owc = obj;
|
|
|
|
ast_debug(3, "%s: Disposing of outbound websocket config\n",
|
|
ast_sorcery_object_get_id(owc));
|
|
ast_string_field_free_memory(owc);
|
|
}
|
|
|
|
static void *outbound_websocket_alloc(const char *id)
|
|
{
|
|
struct ari_conf_outbound_websocket *owc = NULL;
|
|
|
|
owc = ast_sorcery_generic_alloc(sizeof(*owc), outbound_websocket_dtor);
|
|
if (!owc) {
|
|
return NULL;
|
|
}
|
|
|
|
if (ast_string_field_init(owc, 1024) != 0) {
|
|
ao2_cleanup(owc);
|
|
return NULL;
|
|
}
|
|
|
|
ast_debug(2, "%s: Allocated outbound websocket config\n", id);
|
|
return owc;
|
|
}
|
|
|
|
/*! \brief Parses the ast_ari_conf_outbound_ws_type enum from a config file */
|
|
static int outbound_websocket_connection_type_handler(const struct aco_option *opt,
|
|
struct ast_variable *var, void *obj)
|
|
{
|
|
struct ari_conf_outbound_websocket *ows = obj;
|
|
|
|
if (strcasecmp(var->value, "persistent") == 0) {
|
|
ows->connection_type = ARI_WS_TYPE_OUTBOUND_PERSISTENT;
|
|
} else if (strcasecmp(var->value, "per_call_config") == 0) {
|
|
ows->connection_type = ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG;
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int outbound_websocket_connection_type_to_str(const void *obj, const intptr_t *args, char **buf)
|
|
{
|
|
const struct ari_conf_outbound_websocket *owc = obj;
|
|
|
|
if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) {
|
|
*buf = ast_strdup("persistent");
|
|
} else if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
|
|
*buf = ast_strdup("per_call_config");
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Can't use INT_MIN because it's an expression
|
|
* and macro substitutions using stringify can't
|
|
* handle that.
|
|
*/
|
|
#define DEFAULT_RECONNECT_ATTEMPTS -2147483648
|
|
|
|
/*!
|
|
* \brief Callback to initialize an outbound websocket object
|
|
* \retval 0 on success
|
|
* \retval CMP_MATCH on error which will cause the object to be removed
|
|
*/
|
|
static int outbound_websocket_apply(const struct ast_sorcery *sorcery, void *obj)
|
|
{
|
|
struct ari_conf_outbound_websocket *owc = obj;
|
|
const char *id = ast_sorcery_object_get_id(owc);
|
|
int res = 0;
|
|
|
|
ast_debug(3, "%s: Initializing outbound websocket\n", id);
|
|
|
|
if (ast_strlen_zero(owc->uri)) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket missing uri\n", id);
|
|
res = -1;
|
|
}
|
|
if (ast_strlen_zero(owc->protocols)) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket missing protocols\n", id);
|
|
res = -1;
|
|
}
|
|
if (ast_strlen_zero(owc->apps)) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket missing apps\n", id);
|
|
res = -1;
|
|
} else {
|
|
char *apps = ast_strdupa(owc->apps);
|
|
char *app;
|
|
while ((app = ast_strsep(&apps, ',', AST_STRSEP_STRIP))) {
|
|
|
|
if (ast_strlen_zero(app)) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket has empty app\n", id);
|
|
res = -1;
|
|
}
|
|
if (strlen(app) > ARI_MAX_APP_NAME_LEN) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket app '%s' > %d characters\n",
|
|
id, app, (int)ARI_MAX_APP_NAME_LEN);
|
|
res = -1;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if (ast_strlen_zero(owc->local_ari_user)) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket missing local_ari_user\n", id);
|
|
res = -1;
|
|
}
|
|
|
|
if (res != 0) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket configuration failed\n", id);
|
|
} else {
|
|
ast_debug(3, "%s: Outbound websocket configuration succeeded\n", id);
|
|
|
|
if (owc->reconnect_attempts == DEFAULT_RECONNECT_ATTEMPTS) {
|
|
if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) {
|
|
owc->reconnect_attempts = INT_MAX;
|
|
} else {
|
|
owc->reconnect_attempts = 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Reminder: If res is -1, the config will be discarded. */
|
|
return res;
|
|
}
|
|
|
|
enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id)
|
|
{
|
|
RAII_VAR(struct outbound_websocket_state *, state, NULL, ao2_cleanup);
|
|
|
|
state = ao2_find(owc_states, id, OBJ_SEARCH_KEY);
|
|
return state ? state->invalid_fields : ARI_OWC_FIELD_NONE;
|
|
}
|
|
|
|
static int outbound_websocket_validate_cb(void *obj, void *args, int flags)
|
|
{
|
|
struct ari_conf_outbound_websocket *owc = obj;
|
|
struct ari_conf_outbound_websocket *other_owc = NULL;
|
|
RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup);
|
|
struct ao2_iterator it;
|
|
const char *id = ast_sorcery_object_get_id(owc);
|
|
struct ast_vector_string apps = { 0, };
|
|
struct ari_conf_user *user = NULL;
|
|
struct outbound_websocket_state *state = NULL;
|
|
int res = 0;
|
|
|
|
ast_debug(2, "%s: Validating outbound websocket\n", id);
|
|
|
|
owcs = ari_conf_get_owcs();
|
|
if (!owcs || ao2_container_count(owcs) == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (AST_VECTOR_INIT(&apps, 5) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
res = ast_vector_string_split(&apps, owc->apps, ",", 0, NULL);
|
|
if (res != 0) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket apps '%s' failed to split\n",
|
|
id, owc->apps);
|
|
AST_VECTOR_RESET(&apps, ast_free_ptr);
|
|
AST_VECTOR_FREE(&apps);
|
|
return 0;
|
|
}
|
|
|
|
state = ao2_find(owc_states, id, OBJ_SEARCH_KEY);
|
|
if (!state) {
|
|
state = ao2_alloc(sizeof(*state) + strlen(id) + 1, NULL);
|
|
if (!state) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket state allocation failed\n", id);
|
|
AST_VECTOR_RESET(&apps, ast_free_ptr);
|
|
AST_VECTOR_FREE(&apps);
|
|
return 0;
|
|
}
|
|
strcpy(state->id, id); /* Safe */
|
|
ast_debug(3, "%s: Created new outbound websocket state\n", id);
|
|
} else {
|
|
ast_debug(3, "%s: Outbound websocket state already exists\n", id);
|
|
}
|
|
state->invalid_fields = ARI_OWC_FIELD_NONE;
|
|
|
|
/*
|
|
* Check all other owcs to make sure we don't have
|
|
* duplicate apps.
|
|
*/
|
|
it = ao2_iterator_init(owcs, 0);
|
|
while ((other_owc = ao2_iterator_next(&it))) {
|
|
const char *other_id = ast_sorcery_object_get_id(other_owc);
|
|
if (!ast_strings_equal(other_id, id)) {
|
|
int i = 0;
|
|
for (i = 0; i < AST_VECTOR_SIZE(&apps); i++) {
|
|
const char *app = AST_VECTOR_GET(&apps, i);
|
|
if (ast_in_delimited_string(app, other_owc->apps, ',')) {
|
|
ast_log(LOG_WARNING,
|
|
"%s: Outbound websocket '%s' is also trying to register app '%s'\n",
|
|
id, other_id, app);
|
|
state->invalid_fields |= ARI_OWC_FIELD_APPS;
|
|
}
|
|
}
|
|
}
|
|
ao2_cleanup(other_owc);
|
|
if (owc->invalid) {
|
|
break;
|
|
}
|
|
}
|
|
ao2_iterator_destroy(&it);
|
|
AST_VECTOR_RESET(&apps, ast_free_ptr);
|
|
AST_VECTOR_FREE(&apps);
|
|
|
|
/*
|
|
* Check that the local_ari_user is valid and has
|
|
* a plain text password.
|
|
*/
|
|
user = ast_sorcery_retrieve_by_id(sorcery, "user", owc->local_ari_user);
|
|
if (!user) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' not found\n",
|
|
id, owc->local_ari_user);
|
|
state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
|
|
} else {
|
|
if (user->password_format != ARI_PASSWORD_FORMAT_PLAIN) {
|
|
ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' password MUST be plain text\n",
|
|
id, owc->local_ari_user);
|
|
state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
|
|
}
|
|
if (ast_string_field_set(owc, local_ari_password, user->password) != 0) {
|
|
state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
|
|
}
|
|
}
|
|
ao2_cleanup(user);
|
|
|
|
/*
|
|
* The container has AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE set so
|
|
* this is an insert or replace operation.
|
|
*/
|
|
ao2_link(owc_states, state);
|
|
ao2_cleanup(state);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int outbound_websocket_state_cleanup(void *obj, void *arg, int flags)
|
|
{
|
|
struct outbound_websocket_state *state = obj;
|
|
struct ari_conf_outbound_websocket *owc = ari_conf_get_owc(state->id);
|
|
int res = 0;
|
|
|
|
if (!owc) {
|
|
ast_debug(3, "%s: Cleaning up orphaned outbound websocket state\n", state->id);
|
|
res = CMP_MATCH;
|
|
}
|
|
ao2_cleanup(owc);
|
|
|
|
return res;
|
|
}
|
|
|
|
static void outbound_websockets_validate(const char *name)
|
|
{
|
|
RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
|
|
|
|
ao2_callback(owcs, OBJ_NODATA, outbound_websocket_validate_cb, NULL);
|
|
/* Clean up any states whose configs have disappeared. */
|
|
ao2_callback(owc_states, OBJ_NODATA | OBJ_UNLINK,
|
|
outbound_websocket_state_cleanup, NULL);
|
|
}
|
|
|
|
struct ao2_container *ari_conf_get_owcs(void)
|
|
{
|
|
if (!sorcery) {
|
|
return NULL;
|
|
}
|
|
|
|
return ast_sorcery_retrieve_by_fields(sorcery, "outbound_websocket",
|
|
AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
|
|
}
|
|
|
|
struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id)
|
|
{
|
|
if (!sorcery) {
|
|
return NULL;
|
|
}
|
|
|
|
return ast_sorcery_retrieve_by_id(sorcery, "outbound_websocket", id);
|
|
}
|
|
|
|
struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app(
|
|
const char *app_name, unsigned int ws_type)
|
|
{
|
|
struct ari_conf_outbound_websocket *owc = NULL;
|
|
struct ao2_container *owcs = NULL;
|
|
struct ao2_iterator i;
|
|
|
|
if (ast_strlen_zero(app_name)) {
|
|
return NULL;
|
|
}
|
|
|
|
ast_debug(3, "Checking outbound websockets for app '%s'\n", app_name);
|
|
|
|
owcs = ari_conf_get_owcs();
|
|
if (!owcs || ao2_container_count(owcs) == 0) {
|
|
ast_debug(3, "No outbound websockets found\n");
|
|
return NULL;
|
|
}
|
|
|
|
i = ao2_iterator_init(owcs, 0);
|
|
while ((owc = ao2_iterator_next(&i))) {
|
|
const char *id = ast_sorcery_object_get_id(owc);
|
|
|
|
ast_debug(3, "%s: Checking outbound websocket apps '%s' for app '%s'\n",
|
|
id, owc->apps, app_name);
|
|
if (owc->connection_type & ws_type
|
|
&& ast_in_delimited_string(app_name, owc->apps, ',')) {
|
|
ast_debug(3, "%s: Found correct websocket type for apps '%s' for app '%s'\n",
|
|
id, owc->apps, app_name);
|
|
break;
|
|
}
|
|
ao2_cleanup(owc);
|
|
}
|
|
ao2_iterator_destroy(&i);
|
|
ao2_cleanup(owcs);
|
|
if (!owc) {
|
|
ast_debug(3, "No outbound websocket found for app '%s'\n", app_name);
|
|
}
|
|
|
|
return owc;
|
|
}
|
|
|
|
const char *ari_websocket_type_to_str(enum ari_websocket_type type)
|
|
{
|
|
switch (type) {
|
|
case ARI_WS_TYPE_OUTBOUND_PERSISTENT:
|
|
return "persistent";
|
|
case ARI_WS_TYPE_OUTBOUND_PER_CALL:
|
|
return "per_call";
|
|
case ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG:
|
|
return "per_call_config";
|
|
case ARI_WS_TYPE_INBOUND:
|
|
return "inbound";
|
|
case ARI_WS_TYPE_ANY:
|
|
return "any";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
enum ari_conf_owc_fields ari_conf_owc_detect_changes(
|
|
struct ari_conf_outbound_websocket *old_owc,
|
|
struct ari_conf_outbound_websocket *new_owc)
|
|
{
|
|
enum ari_conf_owc_fields changed = ARI_OWC_FIELD_NONE;
|
|
const char *new_id = ast_sorcery_object_get_id(new_owc);
|
|
RAII_VAR(struct ast_variable *, changes, NULL, ast_variables_destroy);
|
|
struct ast_variable *v = NULL;
|
|
int res = 0;
|
|
|
|
res = ast_sorcery_diff(sorcery, old_owc, new_owc, &changes);
|
|
if (res != 0) {
|
|
ast_log(LOG_WARNING, "%s: Failed to create changeset\n", new_id);
|
|
return ARI_OWC_FIELD_NONE;
|
|
}
|
|
|
|
for (v = changes; v; v = v->next) {
|
|
ast_debug(2, "%s: %s changed to %s\n", new_id, v->name, v->value);
|
|
if (ast_strings_equal(v->name, "apps")) {
|
|
changed |= ARI_OWC_FIELD_APPS;
|
|
} else if (ast_strings_equal(v->name, "subscribe_all")) {
|
|
changed |= ARI_OWC_FIELD_SUBSCRIBE_ALL;
|
|
} else if (ast_strings_equal(v->name, "connection_type")) {
|
|
changed |= ARI_OWC_FIELD_CONNECTION_TYPE;
|
|
} else if (ast_strings_equal(v->name, "uri")) {
|
|
changed |= ARI_OWC_FIELD_URI;
|
|
} else if (ast_strings_equal(v->name, "protocols")) {
|
|
changed |= ARI_OWC_FIELD_PROTOCOLS;
|
|
} else if (ast_strings_equal(v->name, "username")) {
|
|
changed |= ARI_OWC_FIELD_USERNAME;
|
|
} else if (ast_strings_equal(v->name, "password")) {
|
|
changed |= ARI_OWC_FIELD_PASSWORD;
|
|
} else if (ast_strings_equal(v->name, "local_ari_user")) {
|
|
changed |= ARI_OWC_FIELD_LOCAL_ARI_USER;
|
|
} else if (ast_strings_equal(v->name, "local_ari_password")) {
|
|
changed |= ARI_OWC_FIELD_LOCAL_ARI_PASSWORD;
|
|
} else if (ast_strings_equal(v->name, "tls_enabled")) {
|
|
changed |= ARI_OWC_FIELD_TLS_ENABLED;
|
|
} else if (ast_strings_equal(v->name, "ca_list_file")) {
|
|
changed |= ARI_OWC_FIELD_CA_LIST_FILE;
|
|
} else if (ast_strings_equal(v->name, "ca_list_path")) {
|
|
changed |= ARI_OWC_FIELD_CA_LIST_PATH;
|
|
} else if (ast_strings_equal(v->name, "cert_file")) {
|
|
changed |= ARI_OWC_FIELD_CERT_FILE;
|
|
} else if (ast_strings_equal(v->name, "priv_key_file")) {
|
|
changed |= ARI_OWC_FIELD_PRIV_KEY_FILE;
|
|
} else if (ast_strings_equal(v->name, "reconnect_interval")) {
|
|
changed |= ARI_OWC_FIELD_RECONNECT_INTERVAL;
|
|
} else if (ast_strings_equal(v->name, "reconnect_attempts")) {
|
|
changed |= ARI_OWC_FIELD_RECONNECT_ATTEMPTS;
|
|
} else if (ast_strings_equal(v->name, "connection_timeout")) {
|
|
changed |= ARI_OWC_FIELD_CONNECTION_TIMEOUT;
|
|
} else if (ast_strings_equal(v->name, "verify_server_cert")) {
|
|
changed |= ARI_OWC_FIELD_VERIFY_SERVER_CERT;
|
|
} else if (ast_strings_equal(v->name, "verify_server_hostname")) {
|
|
changed |= ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME;
|
|
} else {
|
|
ast_debug(2, "%s: Unknown change %s\n", new_id, v->name);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
/*! \brief \ref ast_ari_conf destructor. */
|
|
static void general_dtor(void *obj)
|
|
{
|
|
struct ari_conf_general *cfg = obj;
|
|
|
|
ast_string_field_free_memory(cfg);
|
|
}
|
|
|
|
static void *general_alloc(const char *name)
|
|
{
|
|
struct ari_conf_general *general = ast_sorcery_generic_alloc(
|
|
sizeof(*general), general_dtor);
|
|
|
|
if (!general) {
|
|
return NULL;
|
|
}
|
|
|
|
if (ast_string_field_init(general, 64) != 0) {
|
|
return NULL;
|
|
}
|
|
|
|
return general;
|
|
}
|
|
|
|
#define MAX_VARS 128
|
|
|
|
static int general_apply(const struct ast_sorcery *sorcery, void *obj)
|
|
{
|
|
struct ari_conf_general *general = obj;
|
|
char *parse = NULL;
|
|
AST_DECLARE_APP_ARGS(args,
|
|
AST_APP_ARG(vars)[MAX_VARS];
|
|
);
|
|
|
|
ast_debug(2, "Initializing general config\n");
|
|
|
|
parse = ast_strdupa(general->channelvars);
|
|
AST_STANDARD_APP_ARGS(args, parse);
|
|
|
|
ast_channel_set_ari_vars(args.argc, args.vars);
|
|
return 0;
|
|
}
|
|
|
|
/*! \brief Encoding format handler converts from boolean to enum. */
|
|
static int general_pretty_handler(const struct aco_option *opt,
|
|
struct ast_variable *var, void *obj)
|
|
{
|
|
struct ari_conf_general *general = obj;
|
|
|
|
general->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct ari_conf_general* ari_conf_get_general(void)
|
|
{
|
|
if (!sorcery) {
|
|
return NULL;
|
|
}
|
|
|
|
return ast_sorcery_retrieve_by_id(sorcery, "general", "general");
|
|
}
|
|
|
|
static int general_pretty_to_str(const void *obj, const intptr_t *args, char **buf)
|
|
{
|
|
const struct ari_conf_general *general = obj;
|
|
|
|
if (general->format == AST_JSON_PRETTY) {
|
|
*buf = ast_strdup("yes");
|
|
} else {
|
|
*buf = ast_strdup("no");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*! \brief Destructor for \ref ast_ari_conf_user */
|
|
static void user_dtor(void *obj)
|
|
{
|
|
struct ari_conf_user *user = obj;
|
|
ast_string_field_free_memory(user);
|
|
ast_debug(3, "%s: Disposing of user\n", ast_sorcery_object_get_id(user));
|
|
}
|
|
|
|
/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */
|
|
static void *user_alloc(const char *cat)
|
|
{
|
|
struct ari_conf_user *user = ast_sorcery_generic_alloc(
|
|
sizeof(*user), user_dtor);
|
|
|
|
if (!user) {
|
|
return NULL;
|
|
}
|
|
|
|
if (ast_string_field_init(user, 64) != 0) {
|
|
ao2_cleanup(user);
|
|
user = NULL;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
static int user_apply(const struct ast_sorcery *sorcery, void *obj)
|
|
{
|
|
struct ari_conf_user *user = obj;
|
|
const char *id = ast_sorcery_object_get_id(user);
|
|
|
|
ast_debug(2, "%s: Initializing user\n", id);
|
|
|
|
if (ast_strlen_zero(user->password)) {
|
|
ast_log(LOG_WARNING, "%s: User missing password\n", id);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*! \brief Parses the ast_ari_password_format enum from a config file */
|
|
static int user_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;
|
|
}
|
|
|
|
static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf)
|
|
{
|
|
const struct ari_conf_user *user = obj;
|
|
|
|
if (user->password_format == ARI_PASSWORD_FORMAT_CRYPT) {
|
|
*buf = ast_strdup("crypt");
|
|
} else {
|
|
*buf = ast_strdup("plain");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
struct ao2_container *ari_conf_get_users(void)
|
|
{
|
|
if (!sorcery) {
|
|
return NULL;
|
|
}
|
|
|
|
return ast_sorcery_retrieve_by_fields(sorcery, "user",
|
|
AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
|
|
}
|
|
|
|
struct ari_conf_user *ari_conf_get_user(const char *username)
|
|
{
|
|
if (!sorcery) {
|
|
return NULL;
|
|
}
|
|
|
|
return ast_sorcery_retrieve_by_id(sorcery, "user", username);
|
|
}
|
|
|
|
/*
|
|
* This is called by res_ari.c to validate the user and password
|
|
* for the websocket connection.
|
|
*/
|
|
struct ari_conf_user *ari_conf_validate_user(const char *username,
|
|
const char *password)
|
|
{
|
|
struct ari_conf_user *user = NULL;
|
|
int is_valid = 0;
|
|
|
|
if (ast_strlen_zero(username) || ast_strlen_zero(password)) {
|
|
return NULL;
|
|
}
|
|
|
|
user = ast_sorcery_retrieve_by_id(sorcery, "user", username);
|
|
if (!user) {
|
|
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) {
|
|
ao2_cleanup(user);
|
|
user = NULL;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
int ari_sorcery_observer_add(const char *object_type,
|
|
const struct ast_sorcery_observer *callbacks)
|
|
{
|
|
if (!sorcery) {
|
|
return -1;
|
|
}
|
|
return ast_sorcery_observer_add(sorcery, object_type, callbacks);
|
|
}
|
|
|
|
int ari_sorcery_observer_remove(const char *object_type,
|
|
const struct ast_sorcery_observer *callbacks)
|
|
{
|
|
if (!sorcery) {
|
|
return -1;
|
|
}
|
|
ast_sorcery_observer_remove(sorcery, object_type, callbacks);
|
|
return 0;
|
|
}
|
|
|
|
static void outbound_websocket_created_cb(const void *obj)
|
|
{
|
|
const struct ari_conf_outbound_websocket *owc = obj;
|
|
|
|
ast_debug(3, "%s: Outbound XXXXXXXXXXXX websocket created\n", ast_sorcery_object_get_id(owc));
|
|
}
|
|
|
|
static struct ast_sorcery_observer observer_callbacks = {
|
|
.created = outbound_websocket_created_cb,
|
|
.loaded = outbound_websockets_validate,
|
|
};
|
|
|
|
#define ari_conf_bool(object, option, field, def_value) \
|
|
ast_sorcery_object_field_register(sorcery, #object, #option, \
|
|
def_value, OPT_YESNO_T, 1, \
|
|
FLDSET(struct ari_conf_ ## object, field))
|
|
|
|
#define _stringify(val) #val
|
|
#define ari_conf_int(object, option, field, def_value) \
|
|
ast_sorcery_object_field_register(sorcery, #object, #option, \
|
|
_stringify(def_value), OPT_INT_T, PARSE_IN_RANGE, \
|
|
FLDSET(struct ari_conf_ ## object, field), INT_MIN, INT_MAX)
|
|
|
|
#define ari_conf_uint(object, option, field, def_value) \
|
|
ast_sorcery_object_field_register(sorcery, #object, #option, \
|
|
_stringify(def_value), OPT_UINT_T, PARSE_IN_RANGE, \
|
|
FLDSET(struct ari_conf_ ## object, field), 0, UINT_MAX)
|
|
|
|
#define ari_conf_sf(object, option, field, def_value) \
|
|
ast_sorcery_object_field_register(sorcery, #object, #option, \
|
|
def_value, OPT_STRINGFIELD_T, 0, \
|
|
STRFLDSET(struct ari_conf_ ##object, field))
|
|
|
|
#define ari_conf_cust(object, option, def_value) \
|
|
ast_sorcery_object_field_register_custom(sorcery, #object, #option, \
|
|
def_value, object ## _ ## option ## _handler, \
|
|
object ## _ ## option ## _to_str, NULL, 0, 0)
|
|
|
|
AO2_STRING_FIELD_HASH_FN(outbound_websocket_state, id)
|
|
AO2_STRING_FIELD_CMP_FN(outbound_websocket_state, id)
|
|
|
|
static int ari_conf_init(void)
|
|
{
|
|
ast_debug(2, "Initializing ARI configuration\n");
|
|
|
|
owc_states = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX,
|
|
AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, OWC_STATES_BUCKETS,
|
|
outbound_websocket_state_hash_fn, NULL,
|
|
outbound_websocket_state_cmp_fn);
|
|
if (!owc_states) {
|
|
ast_log(LOG_ERROR, "Failed to allocate outbound websocket states\n");
|
|
return -1;
|
|
}
|
|
|
|
if (!(sorcery = ast_sorcery_open())) {
|
|
ast_log(LOG_ERROR, "Failed to open sorcery\n");
|
|
return -1;
|
|
}
|
|
|
|
ast_sorcery_apply_default(sorcery, "general", "config",
|
|
"ari.conf,criteria=type=general,single_object=yes,explicit_name=general");
|
|
ast_sorcery_apply_default(sorcery, "user", "config",
|
|
"ari.conf,criteria=type=user");
|
|
ast_sorcery_apply_default(sorcery, "outbound_websocket", "config",
|
|
"ari.conf,criteria=type=outbound_websocket");
|
|
|
|
if (ast_sorcery_object_register(sorcery, "general", general_alloc, NULL, general_apply)) {
|
|
ast_log(LOG_ERROR, "Failed to register ARI general object with sorcery\n");
|
|
ast_sorcery_unref(sorcery);
|
|
sorcery = NULL;
|
|
return -1;
|
|
}
|
|
|
|
if (ast_sorcery_object_register(sorcery, "user", user_alloc, NULL, user_apply)) {
|
|
ast_log(LOG_ERROR, "Failed to register ARI user object with sorcery\n");
|
|
ast_sorcery_unref(sorcery);
|
|
sorcery = NULL;
|
|
return -1;
|
|
}
|
|
|
|
if (ast_sorcery_object_register(sorcery, "outbound_websocket", outbound_websocket_alloc,
|
|
NULL, outbound_websocket_apply)) {
|
|
ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket object with sorcery\n");
|
|
ast_sorcery_unref(sorcery);
|
|
sorcery = NULL;
|
|
return -1;
|
|
}
|
|
|
|
if (ast_sorcery_observer_add(sorcery, "outbound_websocket", &observer_callbacks)) {
|
|
ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket observer with sorcery\n");
|
|
ast_sorcery_unref(sorcery);
|
|
sorcery = NULL;
|
|
return -1;
|
|
}
|
|
|
|
ast_sorcery_object_field_register_nodoc(sorcery, "general", "type", "", OPT_NOOP_T, 0, 0);
|
|
ari_conf_sf(general, auth_realm, auth_realm, "Asterisk REST Interface");
|
|
ari_conf_sf(general, allowed_origins, allowed_origins, "");
|
|
ari_conf_sf(general, channelvars, channelvars, "");
|
|
ari_conf_bool(general, enabled, enabled, "yes");
|
|
ari_conf_cust(general, pretty, "no");
|
|
ari_conf_int(general, websocket_write_timeout, write_timeout,
|
|
AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT);
|
|
|
|
|
|
ast_sorcery_object_field_register(sorcery, "user", "type", "", OPT_NOOP_T, 0, 0);
|
|
ari_conf_sf(user, password, password, "");
|
|
ari_conf_bool(user, read_only, read_only, "no");
|
|
ari_conf_cust(user, password_format, "plain");
|
|
|
|
ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0);
|
|
ari_conf_sf(outbound_websocket, uri, uri, "");
|
|
ari_conf_sf(outbound_websocket, protocols, protocols, "");
|
|
ari_conf_sf(outbound_websocket, apps, apps, "");
|
|
ari_conf_sf(outbound_websocket, username, username, "");
|
|
ari_conf_sf(outbound_websocket, password, password, "");
|
|
ari_conf_sf(outbound_websocket, local_ari_user, local_ari_user, "");
|
|
ari_conf_sf(outbound_websocket, ca_list_file, ca_list_file, "");
|
|
ari_conf_sf(outbound_websocket, ca_list_path, ca_list_path, "");
|
|
ari_conf_sf(outbound_websocket, cert_file, cert_file, "");
|
|
ari_conf_sf(outbound_websocket, priv_key_file, priv_key_file, "");
|
|
ari_conf_bool(outbound_websocket, subscribe_all, subscribe_all, "no");
|
|
ari_conf_bool(outbound_websocket, tls_enabled, tls_enabled, "no");
|
|
ari_conf_bool(outbound_websocket, verify_server_cert, verify_server_cert, "yes");
|
|
ari_conf_bool(outbound_websocket, verify_server_hostname, verify_server_hostname, "yes");
|
|
ari_conf_cust(outbound_websocket, connection_type, "");
|
|
ari_conf_int(outbound_websocket, connection_timeout, connect_timeout, 500);
|
|
ari_conf_int(outbound_websocket, reconnect_attempts, reconnect_attempts, DEFAULT_RECONNECT_ATTEMPTS);
|
|
ari_conf_int(outbound_websocket, reconnect_interval, reconnect_interval, 500);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int ari_conf_load(enum ari_conf_load_flags flags)
|
|
{
|
|
void (*loader)(const struct ast_sorcery *sorcery, const char *type);
|
|
const char *msg_prefix;
|
|
|
|
if (flags & ARI_CONF_RELOAD) {
|
|
loader = ast_sorcery_reload_object;
|
|
msg_prefix= "Reloading";
|
|
} else {
|
|
loader = ast_sorcery_load_object;
|
|
msg_prefix= "Loading";
|
|
}
|
|
|
|
if (flags & ARI_CONF_INIT) {
|
|
if (ari_conf_init() != 0) {
|
|
ast_log(LOG_ERROR, "Failed to initialize ARI configuration\n");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!sorcery) {
|
|
ast_log(LOG_ERROR, "ARI configuration not initialized\n");
|
|
return -1;
|
|
}
|
|
|
|
if (flags & ARI_CONF_LOAD_GENERAL) {
|
|
ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "general");
|
|
loader(sorcery, "general");
|
|
}
|
|
|
|
if (flags & ARI_CONF_LOAD_USER) {
|
|
ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "user");
|
|
loader(sorcery, "user");
|
|
}
|
|
|
|
if (flags & ARI_CONF_LOAD_OWC) {
|
|
ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "outbound_websocket");
|
|
loader(sorcery, "outbound_websocket");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void ari_conf_destroy(void)
|
|
{
|
|
ast_sorcery_unref(sorcery);
|
|
sorcery = NULL;
|
|
ao2_cleanup(owc_states);
|
|
}
|