ARI Outbound Websockets

Asterisk can now establish websocket sessions _to_ your ARI applications
as well as accepting websocket sessions _from_ them.
Full details: http://s.asterisk.net/ari-outbound-ws

Code change summary:
* Added an ast_vector_string_join() function,
* Added ApplicationRegistered and ApplicationUnregistered ARI events.
* Converted res/ari/config.c to use sorcery to process ari.conf.
* Added the "outbound-websocket" ARI config object.
* Refactored res/ari/ari_websockets.c to handle outbound websockets.
* Refactored res/ari/cli.c for the sorcery changeover.
* Updated res/res_stasis.c for the sorcery changeover.
* Updated apps/app_stasis.c to allow initiating per-call outbound websockets.
* Added CLI commands to manage ARI websockets.
* Added the new "outbound-websocket" object to ari.conf.sample.
* Moved the ARI XML documentation out of res_ari.c into res/ari/ari_doc.xml

UserNote: Asterisk can now establish websocket sessions _to_ your ARI applications
as well as accepting websocket sessions _from_ them.
Full details: http://s.asterisk.net/ari-outbound-ws
releases/20
George Joseph 6 months ago
parent 04a3e854d0
commit f82c7b2694

@ -25,12 +25,14 @@
/*** MODULEINFO
<depend>res_stasis</depend>
<depend>res_ari</depend>
<support_level>core</support_level>
***/
#include "asterisk.h"
#include "asterisk/app.h"
#include "asterisk/ari.h"
#include "asterisk/module.h"
#include "asterisk/pbx.h"
#include "asterisk/stasis.h"
@ -86,6 +88,7 @@ static const char *stasis = "Stasis";
static int app_exec(struct ast_channel *chan, const char *data)
{
char *parse = NULL;
char *connection_id;
int ret = -1;
AST_DECLARE_APP_ARGS(args,
@ -104,13 +107,35 @@ static int app_exec(struct ast_channel *chan, const char *data)
if (args.argc < 1) {
ast_log(LOG_WARNING, "Stasis app_name argument missing\n");
} else {
ret = stasis_app_exec(chan,
args.app_name,
args.argc - 1,
args.app_argv);
goto done;
}
if (stasis_app_is_registered(args.app_name)) {
ast_debug(3, "%s: App '%s' is already registered\n",
ast_channel_name(chan), args.app_name);
ret = stasis_app_exec(chan, args.app_name, args.argc - 1, args.app_argv);
goto done;
}
ast_debug(3, "%s: App '%s' is NOT already registered\n",
ast_channel_name(chan), args.app_name);
/*
* The app isn't registered so we need to see if we have a
* per-call outbound websocket config we can use.
* connection_id will be freed by ast_ari_close_per_call_websocket().
*/
connection_id = ast_ari_create_per_call_websocket(args.app_name, chan);
if (ast_strlen_zero(connection_id)) {
ast_log(LOG_WARNING,
"%s: Stasis app '%s' doesn't exist\n",
ast_channel_name(chan), args.app_name);
goto done;
}
ret = stasis_app_exec(chan, connection_id, args.argc - 1, args.app_argv);
ast_ari_close_per_call_websocket(connection_id);
done:
if (ret) {
/* set ret to 0 so pbx_core doesnt hangup the channel */
if (!ast_check_hangup(chan)) {
@ -140,5 +165,5 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Stasis dialplan applicat
.support_level = AST_MODULE_SUPPORT_CORE,
.load = load_module,
.unload = unload_module,
.requires = "res_stasis",
.requires = "res_stasis,res_ari",
);

@ -35,3 +35,24 @@ enabled = yes ; When set to no, ARI support is disabled.
; When set to plain, the password is in plaintext.
;
;password_format = plain
; Outbound Websocket Connections
;
;[connection1] ; The connection name
;type = outbound_websocket ; Must be "outbound_websocket"
;websocket_client_id = myid ; The id of a websocket client defined in
; websocket_client.conf.
; Default: none
;apps = app1, app2 ; A comma-separated list of Stasis applications
; that will be served by this connection.
; No other connection may serve these apps.
; Default: none
;subscribe_all = no ; If set to "yes", the server will receive all
; events just as though "subscribeAll=true" was
; specified on an incoming websocket connection.
; Default: no
;local_ari_user = local_user ; The name of a local ARI user defined above.
; This controls whether this connection can make
; read/write requests or is read-only.
; Default: none

@ -244,4 +244,51 @@ void ast_ari_response_created(struct ast_ari_response *response,
*/
void ast_ari_response_alloc_failed(struct ast_ari_response *response);
/*!
* \brief Create a per-call outbound websocket connection.
*
* \param app_name The app name.
* \param channel The channel to create the websocket for.
*
* This function should really only be called by app_stasis.
*
* A "per_call" websocket configuration must already exist in
* ari.conf that has 'app_name' in its 'apps' parameter.
*
* The channel uniqueid is used to create a unique app_id
* composed of "<app_name>-<channel_uniqueid>" which will be
* returned from this call. This ID will be used to register
* an ephemeral Stasis application and should be used as the
* app_name for the call to stasis_app_exec(). When
* stasis_app_exec() returns, ast_ari_close_per_call_websocket()
* must be called with the app_id to close the websocket.
*
* The channel unique id is also used to detect when the
* StasisEnd event is sent for the channel. It's how
* ast_ari_close_per_call_websocket() knows that all
* messages for the channel have been sent and it's safe
* to close the websocket.
*
* \retval The ephemeral application id or NULL if one could
* not be created. This pointer will be freed by
* ast_ari_close_per_call_websocket(). Do not free
* it yourself.
*/
char *ast_ari_create_per_call_websocket(const char *app_name,
struct ast_channel *channel);
/*!
* \brief Close a per-call outbound websocket connection.
*
* \param app_id The ephemeral application id returned by
* ast_ari_create_per_call_websocket().
*
* This function should really only be called by app_stasis.
*
* \note This call will block until all messages for the
* channel have been sent or 5 seconds has elapsed.
* After that, the websocket will be closed.
*/
void ast_ari_close_per_call_websocket(char *app_id);
#endif /* _ASTERISK_ARI_H */

@ -84,6 +84,17 @@ int ast_vector_string_split(struct ast_vector_string *dest,
const char *input, const char *delim, int flags,
int (*excludes_cmp)(const char *s1, const char *s2));
/*!
* \brief Join the elements of a string vector into a single string.
*
* \param vec Pointer to the vector.
* \param delim String to separate elements with.
*
* \retval Resulting string. Must be freed with ast_free.
*
*/
char *ast_vector_string_join(struct ast_vector_string *vec, const char *delim);
/*!
* \brief Define a vector structure with a read/write lock
*

@ -403,6 +403,25 @@ char *ast_read_line_from_buffer(char **buffer)
return start;
}
char *ast_vector_string_join(struct ast_vector_string *vec, const char *delim)
{
struct ast_str *buf = ast_str_create(256);
char *rtn;
int i;
if (!buf) {
return NULL;
}
for (i = 0; i < AST_VECTOR_SIZE(vec); i++) {
ast_str_append(&buf, 0, "%s%s", AST_VECTOR_GET(vec, i), delim);
}
ast_str_truncate(buf, -strlen(delim));
rtn = ast_strdup(ast_str_buffer(buf));
ast_free(buf);
return rtn;
}
int ast_vector_string_split(struct ast_vector_string *dest,
const char *input, const char *delim, int flags,
int (*excludes_cmp)(const char *s1, const char *s2))

@ -0,0 +1,157 @@
<!DOCTYPE docs SYSTEM "appdocsxml.dtd">
<?xml-stylesheet type="text/xsl" href="appdocsxml.xslt"?>
<docs xmlns:xi="http://www.w3.org/2001/XInclude">
<configInfo name="res_ari" language="en_US">
<synopsis>HTTP binding for the Stasis API</synopsis>
<configFile name="ari.conf">
<configObject name="general">
<since>
<version>12.0.0</version>
</since>
<synopsis>General configuration settings</synopsis>
<configOption name="enabled">
<since>
<version>12.0.0</version>
</since>
<synopsis>Enable/disable the ARI module</synopsis>
<description>
<para>This option enables or disables the ARI module.</para>
<note>
<para>ARI uses Asterisk's HTTP server, which must also be enabled in <filename>http.conf</filename>.</para>
</note>
</description>
<see-also>
<ref type="filename">http.conf</ref>
<ref type="link">https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/</ref>
</see-also>
</configOption>
<configOption name="websocket_write_timeout" default="100">
<since>
<version>11.11.0</version>
<version>12.4.0</version>
</since>
<synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis>
<description>
<para>If a websocket connection accepts input slowly, the timeout
for writes to it can be increased to keep it from being disconnected.
Value is in milliseconds.</para>
</description>
</configOption>
<configOption name="pretty">
<since>
<version>12.0.0</version>
</since>
<synopsis>Responses from ARI are formatted to be human readable</synopsis>
</configOption>
<configOption name="auth_realm">
<since>
<version>12.0.0</version>
</since>
<synopsis>Realm to use for authentication. Defaults to Asterisk REST Interface.</synopsis>
</configOption>
<configOption name="allowed_origins">
<since>
<version>12.0.0</version>
</since>
<synopsis>Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins.</synopsis>
</configOption>
<configOption name="channelvars">
<since>
<version>14.2.0</version>
</since>
<synopsis>Comma separated list of channel variables to display in channel json.</synopsis>
</configOption>
</configObject>
<configObject name="user">
<since>
<version>12.0.0</version>
</since>
<synopsis>Per-user configuration settings</synopsis>
<configOption name="type">
<since>
<version>13.30.0</version>
<version>16.7.0</version>
<version>17.1.0</version>
</since>
<synopsis>Define this configuration section as a user.</synopsis>
<description>
<enumlist>
<enum name="user"><para>Configure this section as a <replaceable>user</replaceable></para></enum>
</enumlist>
</description>
</configOption>
<configOption name="read_only">
<since>
<version>13.30.0</version>
<version>16.7.0</version>
<version>17.1.0</version>
</since>
<synopsis>When set to yes, user is only authorized for read-only requests</synopsis>
</configOption>
<configOption name="password">
<since>
<version>13.30.0</version>
<version>16.7.0</version>
<version>17.1.0</version>
</since>
<synopsis>Crypted or plaintext password (see password_format)</synopsis>
</configOption>
<configOption name="password_format">
<since>
<version>12.0.0</version>
</since>
<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>
<configObject name="outbound_websocket">
<since>
<version>20.15.0</version>
<version>21.10.0</version>
<version>22.5.0</version>
</since>
<synopsis>Outbound websocket configuration</synopsis>
<configOption name="type">
<since>
<version>20.15.0</version>
<version>21.10.0</version>
<version>22.5.0</version>
</since>
<synopsis>Must be "outbound_websocket".</synopsis>
</configOption>
<configOption name="websocket_client_id">
<since>
<version>20.15.0</version>
<version>21.10.0</version>
<version>22.5.0</version>
</since>
<synopsis>The ID of a connection defined in websocket_client.conf.</synopsis>
</configOption>
<configOption name="apps">
<since>
<version>20.15.0</version>
<version>21.10.0</version>
<version>22.5.0</version>
</since>
<synopsis>Comma separated list of stasis applications that will use this websocket.</synopsis>
</configOption>
<configOption name="local_ari_user">
<since>
<version>20.15.0</version>
<version>21.10.0</version>
<version>22.5.0</version>
</since>
<synopsis>The local ARI user to act as.</synopsis>
</configOption>
<configOption name="subscribe_all" default="no">
<since>
<version>20.15.0</version>
<version>21.10.0</version>
<version>22.5.0</version>
</since>
<synopsis>Subscribe applications to all event</synopsis>
</configOption>
</configObject>
</configFile>
</configInfo>
</docs>

@ -2610,6 +2610,85 @@ ari_validator ast_ari_validate_application_move_failed_fn(void)
return ast_ari_validate_application_move_failed;
}
int ast_ari_validate_application_registered(struct ast_json *json)
{
int res = 1;
struct ast_json_iter *iter;
int has_type = 0;
int has_application = 0;
int has_timestamp = 0;
for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
prop_is_valid = ast_ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered field asterisk_id failed validation\n");
res = 0;
}
} else
if (strcmp("type", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_type = 1;
prop_is_valid = ast_ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered field type failed validation\n");
res = 0;
}
} else
if (strcmp("application", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_application = 1;
prop_is_valid = ast_ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered field application failed validation\n");
res = 0;
}
} else
if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_timestamp = 1;
prop_is_valid = ast_ari_validate_date(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered field timestamp failed validation\n");
res = 0;
}
} else
{
ast_log(LOG_ERROR,
"ARI ApplicationRegistered has undocumented field %s\n",
ast_json_object_iter_key(iter));
res = 0;
}
}
if (!has_type) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered missing required field type\n");
res = 0;
}
if (!has_application) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered missing required field application\n");
res = 0;
}
if (!has_timestamp) {
ast_log(LOG_ERROR, "ARI ApplicationRegistered missing required field timestamp\n");
res = 0;
}
return res;
}
ari_validator ast_ari_validate_application_registered_fn(void)
{
return ast_ari_validate_application_registered;
}
int ast_ari_validate_application_replaced(struct ast_json *json)
{
int res = 1;
@ -2689,6 +2768,85 @@ ari_validator ast_ari_validate_application_replaced_fn(void)
return ast_ari_validate_application_replaced;
}
int ast_ari_validate_application_unregistered(struct ast_json *json)
{
int res = 1;
struct ast_json_iter *iter;
int has_type = 0;
int has_application = 0;
int has_timestamp = 0;
for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
prop_is_valid = ast_ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered field asterisk_id failed validation\n");
res = 0;
}
} else
if (strcmp("type", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_type = 1;
prop_is_valid = ast_ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered field type failed validation\n");
res = 0;
}
} else
if (strcmp("application", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_application = 1;
prop_is_valid = ast_ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered field application failed validation\n");
res = 0;
}
} else
if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_timestamp = 1;
prop_is_valid = ast_ari_validate_date(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered field timestamp failed validation\n");
res = 0;
}
} else
{
ast_log(LOG_ERROR,
"ARI ApplicationUnregistered has undocumented field %s\n",
ast_json_object_iter_key(iter));
res = 0;
}
}
if (!has_type) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered missing required field type\n");
res = 0;
}
if (!has_application) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered missing required field application\n");
res = 0;
}
if (!has_timestamp) {
ast_log(LOG_ERROR, "ARI ApplicationUnregistered missing required field timestamp\n");
res = 0;
}
return res;
}
ari_validator ast_ari_validate_application_unregistered_fn(void)
{
return ast_ari_validate_application_unregistered;
}
int ast_ari_validate_bridge_attended_transfer(struct ast_json *json)
{
int res = 1;
@ -5990,9 +6148,15 @@ int ast_ari_validate_event(struct ast_json *json)
if (strcmp("ApplicationMoveFailed", discriminator) == 0) {
return ast_ari_validate_application_move_failed(json);
} else
if (strcmp("ApplicationRegistered", discriminator) == 0) {
return ast_ari_validate_application_registered(json);
} else
if (strcmp("ApplicationReplaced", discriminator) == 0) {
return ast_ari_validate_application_replaced(json);
} else
if (strcmp("ApplicationUnregistered", discriminator) == 0) {
return ast_ari_validate_application_unregistered(json);
} else
if (strcmp("BridgeAttendedTransfer", discriminator) == 0) {
return ast_ari_validate_bridge_attended_transfer(json);
} else
@ -6203,9 +6367,15 @@ int ast_ari_validate_message(struct ast_json *json)
if (strcmp("ApplicationMoveFailed", discriminator) == 0) {
return ast_ari_validate_application_move_failed(json);
} else
if (strcmp("ApplicationRegistered", discriminator) == 0) {
return ast_ari_validate_application_registered(json);
} else
if (strcmp("ApplicationReplaced", discriminator) == 0) {
return ast_ari_validate_application_replaced(json);
} else
if (strcmp("ApplicationUnregistered", discriminator) == 0) {
return ast_ari_validate_application_unregistered(json);
} else
if (strcmp("BridgeAttendedTransfer", discriminator) == 0) {
return ast_ari_validate_bridge_attended_transfer(json);
} else

@ -603,6 +603,22 @@ int ast_ari_validate_application_move_failed(struct ast_json *json);
*/
ari_validator ast_ari_validate_application_move_failed_fn(void);
/*!
* \brief Validator for ApplicationRegistered.
*
* Notification that a Stasis app has been registered.
*
* \param json JSON object to validate.
* \retval True (non-zero) if valid.
* \retval False (zero) if invalid.
*/
int ast_ari_validate_application_registered(struct ast_json *json);
/*!
* \brief Function pointer to ast_ari_validate_application_registered().
*/
ari_validator ast_ari_validate_application_registered_fn(void);
/*!
* \brief Validator for ApplicationReplaced.
*
@ -621,6 +637,22 @@ int ast_ari_validate_application_replaced(struct ast_json *json);
*/
ari_validator ast_ari_validate_application_replaced_fn(void);
/*!
* \brief Validator for ApplicationUnregistered.
*
* Notification that a Stasis app has been unregistered.
*
* \param json JSON object to validate.
* \retval True (non-zero) if valid.
* \retval False (zero) if invalid.
*/
int ast_ari_validate_application_unregistered(struct ast_json *json);
/*!
* \brief Function pointer to ast_ari_validate_application_unregistered().
*/
ari_validator ast_ari_validate_application_unregistered_fn(void);
/*!
* \brief Validator for BridgeAttendedTransfer.
*
@ -1596,11 +1628,21 @@ ari_validator ast_ari_validate_application_fn(void);
* - args: List[string] (required)
* - channel: Channel (required)
* - destination: string (required)
* ApplicationRegistered
* - asterisk_id: string
* - type: string (required)
* - application: string (required)
* - timestamp: Date (required)
* ApplicationReplaced
* - asterisk_id: string
* - type: string (required)
* - application: string (required)
* - timestamp: Date (required)
* ApplicationUnregistered
* - asterisk_id: string
* - type: string (required)
* - application: string (required)
* - timestamp: Date (required)
* BridgeAttendedTransfer
* - asterisk_id: string
* - type: string (required)

File diff suppressed because it is too large Load Diff

@ -28,6 +28,7 @@
#include "asterisk/http.h"
#include "asterisk/json.h"
#include "asterisk/vector.h"
#include "asterisk/websocket_client.h"
struct ast_ari_events_event_websocket_args;
@ -35,19 +36,45 @@ struct ast_ari_events_event_websocket_args;
* which causes optional_api stuff to happen, which makes optional_api more
* difficult to debug. */
//struct ast_websocket_server;
struct ast_websocket;
/*
* Since we create a "stasis-<appname>" dialplan context for each
* stasis app, we need to make sure that the total length will be
* <= AST_MAX_CONTEXT
*/
#define STASIS_CONTEXT_PREFIX "stasis-"
#define STASIS_CONTEXT_PREFIX_LEN (sizeof(STASIS_CONTEXT_PREFIX) - 1)
#define ARI_MAX_APP_NAME_LEN (AST_MAX_CONTEXT - STASIS_CONTEXT_PREFIX_LEN)
struct ari_ws_session {
enum ast_websocket_type type; /*!< The type of websocket session. */
struct ast_websocket *ast_ws_session; /*!< The parent websocket session. */
int (*validator)(struct ast_json *); /*!< The message validator. */
struct ao2_container *websocket_apps; /*!< List of Stasis apps registered to
struct ast_vector_string websocket_apps; /*!< List of Stasis apps registered to
the websocket session. */
int subscribe_all; /*!< Flag indicating if all events are subscribed to. */
AST_VECTOR(, struct ast_json *) message_queue; /*!< Container for holding delayed messages. */
char *app_name; /*!< The name of the Stasis application. */
char *remote_addr; /*!< The remote address. */
struct ari_conf_outbound_websocket *owc; /*!< The outbound websocket configuration. */
pthread_t thread; /*!< The thread that handles the websocket. */
char *channel_id; /*!< The channel id for per-call websocket. */
char *channel_name; /*!< The channel name for per-call websocket. */
int stasis_end_sent; /*!< Flag indicating if the StasisEnd message was sent. */
int connected; /*!< Flag indicating if the websocket is connected. */
int closing; /*!< Flag indicating if the session is closing. */
char session_id[]; /*!< The id for the websocket session. */
};
struct ao2_container* ari_websocket_get_sessions(void);
struct ari_ws_session *ari_websocket_get_session(const char *session_id);
struct ari_ws_session *ari_websocket_get_session_by_app(const char *app_name);
const char *ari_websocket_type_to_str(enum ast_websocket_type type);
void ari_websocket_shutdown(struct ari_ws_session *session);
void ari_websocket_shutdown_all(void);
int ari_outbound_websocket_start(struct ari_conf_outbound_websocket *owc);
/*!
* \internal
* \brief Send a JSON event to a websocket.
@ -91,6 +118,6 @@ void ari_handle_websocket(struct ast_tcptls_session_instance *ser,
struct ast_variable *headers);
int ari_websocket_unload_module(void);
int ari_websocket_load_module(void);
int ari_websocket_load_module(int is_enabled);
#endif /* ARI_WEBSOCKETS_H_ */

@ -27,11 +27,13 @@
#include "asterisk/astobj2.h"
#include "asterisk/cli.h"
#include "asterisk/stasis_app.h"
#include "asterisk/uuid.h"
#include "internal.h"
#include "ari_websockets.h"
static char *ari_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ari_conf_general *, general, NULL, ao2_cleanup);
switch (cmd) {
case CLI_INIT:
@ -50,43 +52,42 @@ static char *ari_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
return CLI_SHOWUSAGE;
}
conf = ast_ari_config_get();
general = ari_conf_get_general();
if (!conf) {
if (!general) {
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, "Enabled: %s\n", AST_CLI_YESNO(general->enabled));
ast_cli(a->fd, "Output format: ");
if (conf->general->format & AST_JSON_PRETTY) {
if (general->format & AST_JSON_PRETTY) {
ast_cli(a->fd, "pretty");
} else {
ast_cli(a->fd, "compact");
}
ast_cli(a->fd, "\n");
ast_cli(a->fd, "Auth realm: %s\n", conf->general->auth_realm);
ast_cli(a->fd, "Allowed Origins: %s\n", conf->general->allowed_origins);
ast_cli(a->fd, "User count: %d\n", ao2_container_count(conf->users));
ast_cli(a->fd, "Auth realm: %s\n", general->auth_realm);
ast_cli(a->fd, "Allowed Origins: %s\n", general->allowed_origins);
return CLI_SUCCESS;
}
static int show_users_cb(void *obj, void *arg, int flags)
{
struct ast_ari_conf_user *user = obj;
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);
ast_sorcery_object_get_id(user));
return 0;
}
static char *ari_show_users(struct ast_cli_entry *e, int cmd,
struct ast_cli_args *a)
{
RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ao2_container *, users, NULL, ao2_cleanup);
switch (cmd) {
case CLI_INIT:
@ -105,8 +106,8 @@ static char *ari_show_users(struct ast_cli_entry *e, int cmd,
return CLI_SHOWUSAGE;
}
conf = ast_ari_config_get();
if (!conf) {
users = ari_conf_get_users();
if (!users) {
ast_cli(a->fd, "Error getting ARI configuration\n");
return CLI_FAILURE;
}
@ -114,64 +115,38 @@ static char *ari_show_users(struct ast_cli_entry *e, int cmd,
ast_cli(a->fd, "r/o? Username\n");
ast_cli(a->fd, "---- --------\n");
ao2_callback(conf->users, OBJ_NODATA, show_users_cb, a);
ao2_callback(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)
static void complete_sorcery_object(struct ao2_container *container,
const char *word)
{
struct user_complete *search = data;
size_t wordlen = strlen(word);
void *object;
struct ao2_iterator i = ao2_iterator_init(container, 0);
if (++search->which > search->state) {
return CMP_MATCH;
while ((object = ao2_iterator_next(&i))) {
const char *id = ast_sorcery_object_get_id(object);
if (!strncasecmp(word, id, wordlen)) {
ast_cli_completion_add(ast_strdup(id));
}
return 0;
ao2_ref(object, -1);
}
ao2_iterator_destroy(&i);
}
static char *complete_ari_user(struct ast_cli_args *a)
static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
struct user_complete search = {
.state = a->n,
};
RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
RAII_VAR(struct ao2_container *, users, ari_conf_get_users(), ao2_cleanup);
conf = ast_ari_config_get();
if (!conf) {
if (!users) {
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 ast_ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
switch (cmd) {
case CLI_INIT:
e->command = "ari show user";
@ -180,7 +155,8 @@ static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args
" Shows a specific ARI user\n";
return NULL;
case CLI_GENERATE:
return complete_ari_show_user(a);
complete_sorcery_object(users, a->word);
return NULL;
default:
break;
}
@ -189,20 +165,13 @@ static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args
return CLI_SHOWUSAGE;
}
conf = ast_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);
user = ari_conf_get_user(a->argv[3]);
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, "Username: %s\n", ast_sorcery_object_get_id(user));
ast_cli(a->fd, "Read only?: %s\n", AST_CLI_YESNO(user->read_only));
return CLI_SUCCESS;
@ -281,7 +250,7 @@ static char *ari_show_apps(struct ast_cli_entry *e, int cmd, struct ast_cli_args
ast_cli(a->fd, "=========================\n");
it_apps = ao2_iterator_init(apps, 0);
while ((app = ao2_iterator_next(&it_apps))) {
ast_cli(a->fd, "%-25.25s\n", app);
ast_cli(a->fd, "%s\n", app);
ao2_ref(app, -1);
}
@ -291,56 +260,32 @@ static char *ari_show_apps(struct ast_cli_entry *e, int cmd, struct ast_cli_args
return CLI_SUCCESS;
}
struct app_complete {
/*! Nth app to search for */
int state;
/*! Which app currently on */
int which;
};
static int complete_ari_app_search(void *obj, void *arg, void *data, int flags)
static void complete_app(struct ao2_container *container,
const char *word)
{
struct app_complete *search = data;
size_t wordlen = strlen(word);
void *object;
struct ao2_iterator i = ao2_iterator_init(container, 0);
if (++search->which > search->state) {
return CMP_MATCH;
while ((object = ao2_iterator_next(&i))) {
if (!strncasecmp(word, object, wordlen)) {
ast_cli_completion_add(ast_strdup(object));
}
return 0;
ao2_ref(object, -1);
}
ao2_iterator_destroy(&i);
}
static char *complete_ari_app(struct ast_cli_args *a, int include_all)
static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
void *app;
RAII_VAR(struct ao2_container *, apps, stasis_app_get_all(), ao2_cleanup);
RAII_VAR(char *, app, NULL, ao2_cleanup);
struct app_complete search = {
.state = a->n,
};
if (a->pos != 3) {
return NULL;
}
if (!apps) {
ast_cli(a->fd, "Error getting ARI applications\n");
return CLI_FAILURE;
}
if (include_all && ast_strlen_zero(a->word)) {
ast_str_container_add(apps, " all");
}
app = ao2_callback_data(apps,
ast_strlen_zero(a->word) ? 0 : OBJ_SEARCH_PARTIAL_KEY,
complete_ari_app_search, (char*)a->word, &search);
return app ? ast_strdup(app) : NULL;
}
static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
void *app;
switch (cmd) {
case CLI_INIT:
e->command = "ari show app";
@ -350,7 +295,8 @@ static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args
;
return NULL;
case CLI_GENERATE:
return complete_ari_app(a, 0);
complete_app(apps, a->word);
return NULL;
default:
break;
}
@ -373,9 +319,15 @@ static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args
static char *ari_set_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
RAII_VAR(struct ao2_container *, apps, stasis_app_get_all(), ao2_cleanup);
void *app;
int debug;
if (!apps) {
ast_cli(a->fd, "Error getting ARI applications\n");
return CLI_FAILURE;
}
switch (cmd) {
case CLI_INIT:
e->command = "ari set debug";
@ -385,7 +337,14 @@ static char *ari_set_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args
;
return NULL;
case CLI_GENERATE:
return complete_ari_app(a, 1);
if (a->argc == 3) {
ast_cli_completion_add(ast_strdup("all"));
complete_app(apps, a->word);
} else if (a->argc == 4) {
ast_cli_completion_add(ast_strdup("on"));
ast_cli_completion_add(ast_strdup("off"));
}
return NULL;
default:
break;
}
@ -418,6 +377,309 @@ static char *ari_set_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args
return CLI_SUCCESS;
}
static int show_owc_cb(void *obj, void *arg, int flags)
{
struct ari_conf_outbound_websocket *owc = obj;
const char *id = ast_sorcery_object_get_id(owc);
enum ari_conf_owc_fields invalid_fields = ari_conf_owc_get_invalid_fields(id);
struct ast_cli_args *a = arg;
ast_cli(a->fd, "%-32s %-15s %-32s %-7s %s\n",
id,
ari_websocket_type_to_str(owc->websocket_client->connection_type),
owc->apps,
invalid_fields == ARI_OWC_FIELD_NONE ? "valid" : "INVALID",
owc->websocket_client->uri);
return 0;
}
#define DASHES "----------------------------------------------------------------------"
static char *ari_show_owcs(struct ast_cli_entry *e, int cmd,
struct ast_cli_args *a)
{
RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup);
switch (cmd) {
case CLI_INIT:
e->command = "ari show outbound-websockets";
e->usage =
"Usage: ari show outbound-websockets\n"
" Shows all ARI outbound-websockets\n";
return NULL;
case CLI_GENERATE:
return NULL;
default:
break;
}
if (a->argc != 3) {
return CLI_SHOWUSAGE;
}
owcs = ari_conf_get_owcs();
if (!owcs) {
ast_cli(a->fd, "Error getting ARI configuration\n");
return CLI_FAILURE;
}
ast_cli(a->fd, "%-32s %-15s %-32s %-7s %s\n", "Name", "Type", "Apps", "Status", "URI");
ast_cli(a->fd, "%.*s %.*s %.*s %.*s %.*s\n", 32, DASHES, 15, DASHES, 32, DASHES, 7, DASHES, 64, DASHES);
ao2_callback(owcs, OBJ_NODATA, show_owc_cb, a);
return CLI_SUCCESS;
}
static char *ari_show_owc(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup);
RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
const char *id = NULL;
enum ari_conf_owc_fields invalid_fields;
switch (cmd) {
case CLI_INIT:
e->command = "ari show outbound-websocket";
e->usage =
"Usage: ari show outbound-websocket <connection id>\n"
" Shows a specific ARI outbound websocket\n";
return NULL;
case CLI_GENERATE:
complete_sorcery_object(owcs, a->word);
return NULL;
default:
break;
}
if (a->argc != 4) {
return CLI_SHOWUSAGE;
}
owc = ari_conf_get_owc(a->argv[3]);
if (!owc) {
ast_cli(a->fd, "Error getting ARI configuration\n");
return CLI_FAILURE;
}
id = ast_sorcery_object_get_id(owc);
invalid_fields = ari_conf_owc_get_invalid_fields(id);
ast_cli(a->fd, "[%s] %s\n", id,
invalid_fields == ARI_OWC_FIELD_NONE ? "" : "**INVALID**");
ast_cli(a->fd, "uri = %s\n", owc->websocket_client->uri);
ast_cli(a->fd, "protocols = %s\n", owc->websocket_client->protocols);
ast_cli(a->fd, "apps = %s%s\n", owc->apps,
invalid_fields & ARI_OWC_FIELD_APPS ? " (invalid)" : "");
ast_cli(a->fd, "username = %s\n", owc->websocket_client->username);
ast_cli(a->fd, "password = %s\n", S_COR(owc->websocket_client->password, "********", ""));
ast_cli(a->fd, "local_ari_user = %s%s\n", owc->local_ari_user,
invalid_fields & ARI_OWC_FIELD_LOCAL_ARI_USER ? " (invalid)" : "");
ast_cli(a->fd, "connection_type = %s\n", ari_websocket_type_to_str(owc->websocket_client->connection_type));
ast_cli(a->fd, "subscribe_all = %s\n", AST_CLI_YESNO(owc->subscribe_all));
ast_cli(a->fd, "connec_timeout = %d\n", owc->websocket_client->connect_timeout);
ast_cli(a->fd, "reconnect_attempts = %d\n", owc->websocket_client->reconnect_attempts);
ast_cli(a->fd, "reconnect_interval = %d\n", owc->websocket_client->reconnect_interval);
ast_cli(a->fd, "tls_enabled = %s\n", AST_CLI_YESNO(owc->websocket_client->tls_enabled));
ast_cli(a->fd, "ca_list_file = %s\n", owc->websocket_client->ca_list_file);
ast_cli(a->fd, "ca_list_path = %s\n", owc->websocket_client->ca_list_path);
ast_cli(a->fd, "cert_file = %s\n", owc->websocket_client->cert_file);
ast_cli(a->fd, "priv_key_file = %s\n", owc->websocket_client->priv_key_file);
ast_cli(a->fd, "verify_server = %s\n", AST_CLI_YESNO(owc->websocket_client->verify_server_cert));
ast_cli(a->fd, "verify_server_hostname = %s\n", AST_CLI_YESNO(owc->websocket_client->verify_server_hostname));
ast_cli(a->fd, "\n");
return CLI_SUCCESS;
}
static char *ari_start_owc(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup);
RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
if (!owcs) {
ast_cli(a->fd, "Error getting ARI configuration\n");
return CLI_FAILURE ;
}
switch (cmd) {
case CLI_INIT:
e->command = "ari start outbound-websocket";
e->usage =
"Usage: ari start outbound-websocket <connection id>\n"
" Starts a specific ARI outbound websocket\n";
return NULL;
case CLI_GENERATE:
complete_sorcery_object(owcs, a->word);
return NULL;
default:
break;
}
if (a->argc != 4) {
return CLI_SHOWUSAGE;
}
owc = ari_conf_get_owc(a->argv[3]);
if (!owc) {
ast_cli(a->fd, "Error getting ARI configuration\n");
return CLI_FAILURE;
}
ast_cli(a->fd, "Starting websocket session for outbound-websocket '%s'\n", a->argv[3]);
if (ari_outbound_websocket_start(owc) != 0) {
ast_cli(a->fd, "Error starting outbound websocket\n");
return CLI_FAILURE ;
}
return CLI_SUCCESS;
}
static int show_sessions_cb(void *obj, void *arg, int flags)
{
struct ari_ws_session *session = obj;
struct ast_cli_args *a = arg;
char *apps = ast_vector_string_join(&session->websocket_apps, ",");
ast_cli(a->fd, "%-*s %-15s %-32s %-5s %s\n",
AST_UUID_STR_LEN,
session->session_id,
ari_websocket_type_to_str(session->type),
S_OR(session->remote_addr, "N/A"),
session->type == AST_WS_TYPE_CLIENT_PER_CALL_CONFIG
? "N/A" : (session->connected ? "Up" : "Down"),
S_OR(apps, ""));
ast_free(apps);
return 0;
}
#define DASHES "----------------------------------------------------------------------"
static char *ari_show_sessions(struct ast_cli_entry *e, int cmd,
struct ast_cli_args *a)
{
RAII_VAR(struct ao2_container *, sessions, NULL, ao2_cleanup);
switch (cmd) {
case CLI_INIT:
e->command = "ari show websocket sessions";
e->usage =
"Usage: ari show websocket sessions\n"
" Shows all ARI websocket sessions\n";
return NULL;
case CLI_GENERATE:
return NULL;
default:
break;
}
if (a->argc != 4) {
return CLI_SHOWUSAGE;
}
sessions = ari_websocket_get_sessions();
if (!sessions) {
ast_cli(a->fd, "Error getting websocket sessions\n");
return CLI_FAILURE;
}
ast_cli(a->fd, "%-*.*s %-15.15s %-32.32s %-5.5s %-16.16s\n",
AST_UUID_STR_LEN, AST_UUID_STR_LEN,
"Connection ID",
"Type",
"RemoteAddr",
"State",
"Apps"
);
ast_cli(a->fd, "%-*.*s %-15.15s %-32.32s %-5.5s %-16.16s\n",
AST_UUID_STR_LEN, AST_UUID_STR_LEN, DASHES, DASHES, DASHES, DASHES, DASHES);
ao2_callback(sessions, OBJ_NODATA, show_sessions_cb, a);
return CLI_SUCCESS;
}
static char *ari_shut_sessions(struct ast_cli_entry *e, int cmd,
struct ast_cli_args *a)
{
switch (cmd) {
case CLI_INIT:
e->command = "ari shutdown websocket sessions";
e->usage =
"Usage: ari shutdown websocket sessions\n"
" Shuts down all ARI websocket sessions\n";
return NULL;
case CLI_GENERATE:
return NULL;
default:
break;
}
if (a->argc != 4) {
return CLI_SHOWUSAGE;
}
ast_cli(a->fd, "Shutting down all websocket sessions\n");
ari_websocket_shutdown_all();
return CLI_SUCCESS;
}
static void complete_session(struct ao2_container *container,
const char *word)
{
size_t wordlen = strlen(word);
struct ari_ws_session *session;
struct ao2_iterator i = ao2_iterator_init(container, 0);
while ((session = ao2_iterator_next(&i))) {
if (!strncasecmp(word, session->session_id, wordlen)) {
ast_cli_completion_add(ast_strdup(session->session_id));
}
ao2_ref(session, -1);
}
ao2_iterator_destroy(&i);
}
static char *ari_shut_session(struct ast_cli_entry *e, int cmd,
struct ast_cli_args *a)
{
RAII_VAR(struct ari_ws_session *, session, NULL, ao2_cleanup);
RAII_VAR(struct ao2_container *, sessions, ari_websocket_get_sessions(), ao2_cleanup);
if (!sessions) {
ast_cli(a->fd, "Error getting ARI configuration\n");
return CLI_FAILURE ;
}
switch (cmd) {
case CLI_INIT:
e->command = "ari shutdown websocket session";
e->usage =
"Usage: ari shutdown websocket session <id>\n"
" Shuts down ARI websocket session\n";
return NULL;
case CLI_GENERATE:
complete_session(sessions, a->word);
return NULL;
default:
break;
}
if (a->argc != 5) {
return CLI_SHOWUSAGE;
}
session = ari_websocket_get_session(a->argv[4]);
if (!session) {
ast_cli(a->fd, "Websocket session '%s' not found\n", a->argv[4]);
return CLI_FAILURE ;
}
ast_cli(a->fd, "Shutting down websocket session '%s'\n", a->argv[4]);
ari_websocket_shutdown(session);
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"),
@ -426,12 +688,18 @@ static struct ast_cli_entry cli_ari[] = {
AST_CLI_DEFINE(ari_show_apps, "List registered ARI applications"),
AST_CLI_DEFINE(ari_show_app, "Display details of a registered ARI application"),
AST_CLI_DEFINE(ari_set_debug, "Enable/disable debugging of an ARI application"),
AST_CLI_DEFINE(ari_show_owcs, "List outbound websocket connections"),
AST_CLI_DEFINE(ari_show_owc, "Show outbound websocket connection"),
AST_CLI_DEFINE(ari_start_owc, "Start outbound websocket connection"),
AST_CLI_DEFINE(ari_show_sessions, "Show websocket sessions"),
AST_CLI_DEFINE(ari_shut_session, "Shutdown websocket session"),
AST_CLI_DEFINE(ari_shut_sessions, "Shutdown websocket sessions"),
};
int ast_ari_cli_register(void) {
int ari_cli_register(void) {
return ast_cli_register_multiple(cli_ari, ARRAY_LEN(cli_ari));
}
void ast_ari_cli_unregister(void) {
void ari_cli_unregister(void) {
ast_cli_unregister_multiple(cli_ari, ARRAY_LEN(cli_ari));
}

@ -22,239 +22,584 @@
* \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/websocket_client.h"
#include "asterisk/app.h"
#include "asterisk/channel.h"
#include "asterisk/vector.h"
#include "internal.h"
/*! \brief Locking container for safe configuration access. */
static AO2_GLOBAL_OBJ_STATIC(confs);
/*! \brief Mapping of the ARI 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 ast_ari_conf, general),
.category = "general",
.category_match = ACO_WHITELIST_EXACT,
static struct ast_sorcery *sorcery;
struct outbound_websocket_state {
enum ari_conf_owc_fields invalid_fields;
char id[0];
};
static struct aco_type *general_options[] = ACO_TYPES(&general_option);
#define OWC_STATES_BUCKETS 13
struct ao2_container *owc_states = NULL;
/*! \brief Encoding format handler converts from boolean to enum. */
static int encoding_format_handler(const struct aco_option *opt,
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);
ao2_cleanup(owc->websocket_client);
owc->websocket_client = NULL;
}
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;
}
static int outbound_websocket_websocket_client_id_from_str(const struct aco_option *opt,
struct ast_variable *var, void *obj)
{
struct ast_ari_conf_general *general = obj;
struct ari_conf_outbound_websocket *owc = obj;
if (!strcasecmp(var->name, "pretty")) {
general->format = ast_true(var->value) ?
AST_JSON_PRETTY : AST_JSON_COMPACT;
} else {
if (ast_strlen_zero(var->value)) {
ast_log(LOG_ERROR, "%s: Outbound websocket missing websocket client id\n",
ast_sorcery_object_get_id(owc));
return -1;
}
owc->websocket_client = ast_websocket_client_retrieve_by_id(var->value);
if (!owc->websocket_client) {
ast_log(LOG_ERROR, "%s: Outbound websocket invalid websocket client id '%s'\n",
ast_sorcery_object_get_id(owc), var->value);
return -1;
}
if (ast_string_field_set(owc, websocket_client_id, var->value) != 0) {
return -1;
}
return 0;
}
static int outbound_websocket_websocket_client_id_to_str(const void *obj, const intptr_t *args, char **buf)
{
const struct ari_conf_outbound_websocket *owc = obj;
if (!owc->websocket_client) {
return -1;
}
*buf = ast_strdup(owc->websocket_client_id);
return 0;
}
/*! \brief Parses the ast_ari_password_format enum from a config file */
static int password_format_handler(const struct aco_option *opt,
struct ast_variable *var, void *obj)
/*!
* \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 ast_ari_conf_user *user = obj;
struct ari_conf_outbound_websocket *owc = obj;
const char *id = ast_sorcery_object_get_id(owc);
int res = 0;
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;
ast_debug(3, "%s: Initializing outbound websocket\n", id);
if (ast_strlen_zero(owc->apps)) {
ast_log(LOG_WARNING, "%s: Outbound websocket missing apps\n", id);
res = -1;
} else {
return -1;
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;
}
return 0;
}
}
/*! \brief Destructor for \ref ast_ari_conf_user */
static void user_dtor(void *obj)
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);
}
/* 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)
{
struct ast_ari_conf_user *user = obj;
ast_debug(3, "Disposing of user %s\n", user->username);
ast_free(user->username);
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;
}
/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */
static void *user_alloc(const char *cat)
static int outbound_websocket_validate_cb(void *obj, void *args, int flags)
{
RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
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 (!cat) {
return NULL;
if (AST_VECTOR_INIT(&apps, 5) != 0) {
return 0;
}
ast_debug(3, "Allocating user %s\n", cat);
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;
}
user = ao2_alloc_options(sizeof(*user), user_dtor,
AO2_ALLOC_OPT_LOCK_NOLOCK);
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;
}
user->username = ast_strdup(cat);
if (!user->username) {
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;
}
ao2_ref(user, +1);
return user;
return ast_sorcery_retrieve_by_id(sorcery, "outbound_websocket", id);
}
/*! \brief Sorting function for use with red/black tree */
static int user_sort_cmp(const void *obj_left, const void *obj_right, int flags)
struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app(
const char *app_name, unsigned int ws_type)
{
const struct ast_ari_conf_user *user_left = obj_left;
const struct ast_ari_conf_user *user_right = obj_right;
const char *key_right = obj_right;
int cmp;
struct ari_conf_outbound_websocket *owc = NULL;
struct ao2_container *owcs = NULL;
struct ao2_iterator i;
switch (flags & OBJ_SEARCH_MASK) {
case OBJ_SEARCH_OBJECT:
key_right = user_right->username;
/* Fall through */
case OBJ_SEARCH_KEY:
cmp = strcasecmp(user_left->username, key_right);
break;
case OBJ_SEARCH_PARTIAL_KEY:
/*
* We could also use a partial key struct containing a length
* so strlen() does not get called for every comparison instead.
*/
cmp = strncasecmp(user_left->username, key_right, strlen(key_right));
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->websocket_client->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 ast_websocket_type type)
{
switch (type) {
case AST_WS_TYPE_CLIENT_PERSISTENT:
return "persistent";
case AST_WS_TYPE_CLIENT_PER_CALL:
return "per_call";
case AST_WS_TYPE_CLIENT_PER_CALL_CONFIG:
return "per_call_config";
case AST_WS_TYPE_INBOUND:
return "inbound";
case AST_WS_TYPE_ANY:
return "any";
default:
/* Sort can only work on something with a full or partial key. */
ast_assert(0);
cmp = 0;
break;
return "unknown";
}
return cmp;
}
/*! \brief \ref aco_type item_find function */
static void *user_find(struct ao2_container *tmp_container, const char *cat)
enum ari_conf_owc_fields ari_conf_owc_detect_changes(
struct ari_conf_outbound_websocket *old_owc,
struct ari_conf_outbound_websocket *new_owc)
{
if (!cat) {
return NULL;
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;
int changes_found = 0;
ast_debug(2, "%s: Detecting changes\n", new_id);
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) {
changes_found = 1;
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, "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 {
ast_debug(2, "%s: Unknown change %s\n", new_id, v->name);
}
}
if (!changes_found) {
ast_debug(2, "%s: No changes found %p %p\n", new_id,
old_owc->websocket_client,new_owc->websocket_client);
}
changed |= ast_websocket_client_get_field_diff(
old_owc->websocket_client, new_owc->websocket_client);
return changed;
return ao2_find(tmp_container, cat, OBJ_SEARCH_KEY);
}
static struct aco_type user_option = {
.type = ACO_ITEM,
.name = "user",
.category_match = ACO_BLACKLIST_EXACT,
.category = "general",
.matchfield = "type",
.matchvalue = "user",
.item_alloc = user_alloc,
.item_find = user_find,
.item_offset = offsetof(struct ast_ari_conf, users),
};
/*! \brief \ref ast_ari_conf destructor. */
static void general_dtor(void *obj)
{
struct ari_conf_general *cfg = obj;
static struct aco_type *global_user[] = ACO_TYPES(&user_option);
ast_string_field_free_memory(cfg);
}
static void conf_general_dtor(void *obj)
static void *general_alloc(const char *name)
{
struct ast_ari_conf_general *general = obj;
struct ari_conf_general *general = ast_sorcery_generic_alloc(
sizeof(*general), general_dtor);
ast_string_field_free_memory(general);
if (!general) {
return NULL;
}
/*! \brief \ref ast_ari_conf destructor. */
static void conf_destructor(void *obj)
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 ast_ari_conf *cfg = 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);
ao2_cleanup(cfg->general);
ao2_cleanup(cfg->users);
ast_channel_set_ari_vars(args.argc, args.vars);
return 0;
}
/*! \brief Allocate an \ref ast_ari_conf for config parsing */
static void *conf_alloc(void)
/*! \brief Encoding format handler converts from boolean to enum. */
static int general_pretty_from_str(const struct aco_option *opt,
struct ast_variable *var, void *obj)
{
struct ast_ari_conf *cfg;
struct ari_conf_general *general = obj;
general->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT;
cfg = ao2_alloc_options(sizeof(*cfg), conf_destructor,
AO2_ALLOC_OPT_LOCK_NOLOCK);
if (!cfg) {
return 0;
}
struct ari_conf_general* ari_conf_get_general(void)
{
if (!sorcery) {
return NULL;
}
cfg->general = ao2_alloc_options(sizeof(*cfg->general), conf_general_dtor,
AO2_ALLOC_OPT_LOCK_NOLOCK);
return ast_sorcery_retrieve_by_id(sorcery, "general", "general");
}
cfg->users = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, user_sort_cmp, NULL);
static int general_pretty_to_str(const void *obj, const intptr_t *args, char **buf)
{
const struct ari_conf_general *general = obj;
if (!cfg->users
|| !cfg->general
|| ast_string_field_init(cfg->general, 64)
|| aco_set_defaults(&general_option, "general", cfg->general)) {
ao2_ref(cfg, -1);
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;
}
return cfg;
if (ast_string_field_init(user, 64) != 0) {
ao2_cleanup(user);
user = NULL;
}
#define CONF_FILENAME "ari.conf"
return user;
}
/*! \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),
};
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;
}
CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc,
.files = ACO_FILES(&conf_file));
return 0;
}
struct ast_ari_conf *ast_ari_config_get(void)
/*! \brief Parses the ast_ari_password_format enum from a config file */
static int user_password_format_from_str(const struct aco_option *opt,
struct ast_variable *var, void *obj)
{
struct ast_ari_conf *res = ao2_global_obj_ref(confs);
if (!res) {
ast_log(LOG_ERROR,
"Error obtaining config from " CONF_FILENAME "\n");
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 res;
return 0;
}
struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
const char *password)
static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf)
{
RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
int is_valid = 0;
const struct ari_conf_user *user = obj;
conf = ast_ari_config_get();
if (!conf) {
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;
}
user = ao2_find(conf->users, username, OBJ_SEARCH_KEY);
if (!user) {
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;
}
if (ast_strlen_zero(user->password)) {
ast_log(LOG_WARNING,
"User '%s' missing password; authentication failed\n",
user->username);
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;
}
@ -268,120 +613,184 @@ struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
}
if (!is_valid) {
return NULL;
ao2_cleanup(user);
user = 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)
int ari_sorcery_observer_add(const char *object_type,
const struct ast_sorcery_observer *callbacks)
{
struct ast_ari_conf_user *user = obj;
if (ast_strlen_zero(user->password)) {
ast_log(LOG_WARNING, "User '%s' missing password\n",
user->username);
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;
}
/*! \brief Load (or reload) configuration. */
static int process_config(int reload)
static struct ast_sorcery_observer observer_callbacks = {
.loaded = outbound_websockets_validate,
};
static void ws_client_load(const char *name)
{
RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
ast_sorcery_force_reload_object(sorcery, "outbound_websocket");
}
static struct ast_sorcery_observer ws_client_observer_callbacks = {
.loaded = ws_client_load,
};
switch (aco_process_config(&cfg_info, reload)) {
case ACO_PROCESS_ERROR:
AO2_STRING_FIELD_HASH_FN(outbound_websocket_state, id)
AO2_STRING_FIELD_CMP_FN(outbound_websocket_state, id)
static int ari_conf_init(void)
{
int res = 0;
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;
case ACO_PROCESS_OK:
case ACO_PROCESS_UNCHANGED:
break;
}
conf = ast_ari_config_get();
if (!conf) {
ast_assert(0); /* We just configured; it should be there */
if (!(sorcery = ast_sorcery_open())) {
ast_log(LOG_ERROR, "Failed to open sorcery\n");
return -1;
}
if (conf->general->enabled) {
if (ao2_container_count(conf->users) == 0) {
ast_log(LOG_ERROR, "No configured users for ARI\n");
} else {
ao2_callback(conf->users, OBJ_NODATA, validate_user_cb, NULL);
}
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;
}
return 0;
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;
}
#define MAX_VARS 128
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;
}
static int channelvars_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
{
char *parse = NULL;
AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(vars)[MAX_VARS];
);
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;
}
parse = ast_strdupa(var->value);
AST_STANDARD_APP_ARGS(args, parse);
ast_sorcery_object_field_register_nodoc(sorcery, "general", "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_register_sf(general, ari_conf_general, auth_realm, auth_realm, "Asterisk REST Interface");
ast_sorcery_register_sf(general, ari_conf_general, allowed_origins, allowed_origins, "");
ast_sorcery_register_sf(general, ari_conf_general, channelvars, channelvars, "");
ast_sorcery_register_bool(general, ari_conf_general, enabled, enabled, "yes");
ast_sorcery_register_cust(general, pretty, "no");
ast_sorcery_register_int(general, ari_conf_general, websocket_write_timeout, write_timeout,
AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT);
ast_sorcery_object_field_register(sorcery, "user", "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_register_sf(user, ari_conf_user, password, password, "");
ast_sorcery_register_bool(user, ari_conf_user, read_only, read_only, "no");
ast_sorcery_register_cust(user, password_format, "plain");
ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_register_cust(outbound_websocket, websocket_client_id, "");
ast_sorcery_register_sf(outbound_websocket, ari_conf_outbound_websocket, apps, apps, "");
ast_sorcery_register_sf(outbound_websocket, ari_conf_outbound_websocket, local_ari_user, local_ari_user, "");
ast_sorcery_register_bool(outbound_websocket, ari_conf_outbound_websocket, subscribe_all, subscribe_all, "no");
res = ast_websocket_client_observer_add(&ws_client_observer_callbacks);
if (res < 0) {
ast_log(LOG_WARNING, "Failed to register websocket client observer\n");
ast_sorcery_unref(sorcery);
sorcery = NULL;
return -1;
}
ast_channel_set_ari_vars(args.argc, args.vars);
return 0;
}
int ast_ari_config_init(void)
int ari_conf_load(enum ari_conf_load_flags flags)
{
if (aco_info_init(&cfg_info)) {
aco_info_destroy(&cfg_info);
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";
ast_websocket_client_reload();
} 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_websocket_client_observer_remove(&ws_client_observer_callbacks);
/* ARI general category options */
aco_option_register(&cfg_info, "enabled", ACO_EXACT, general_options,
"yes", OPT_BOOL_T, 1,
FLDSET(struct ast_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 ast_ari_conf_general, auth_realm),
ARI_AUTH_REALM_LEN);
aco_option_register(&cfg_info, "allowed_origins", ACO_EXACT, general_options,
"", OPT_STRINGFIELD_T, 0,
STRFLDSET(struct ast_ari_conf_general, allowed_origins));
aco_option_register(&cfg_info, "websocket_write_timeout", ACO_EXACT, general_options,
AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT_STR, OPT_INT_T, PARSE_IN_RANGE,
FLDSET(struct ast_ari_conf_general, write_timeout), 1, INT_MAX);
aco_option_register_custom(&cfg_info, "channelvars", ACO_EXACT, general_options,
"", channelvars_handler, 0);
/* ARI type=user category options */
aco_option_register(&cfg_info, "type", ACO_EXACT, global_user, NULL,
OPT_NOOP_T, 0, 0);
aco_option_register(&cfg_info, "read_only", ACO_EXACT, global_user,
"no", OPT_BOOL_T, 1,
FLDSET(struct ast_ari_conf_user, read_only));
aco_option_register(&cfg_info, "password", ACO_EXACT, global_user,
"", OPT_CHAR_ARRAY_T, 0,
FLDSET(struct ast_ari_conf_user, password), ARI_PASSWORD_LEN);
aco_option_register_custom(&cfg_info, "password_format", ACO_EXACT,
global_user, "plain", password_format_handler, 0);
return process_config(0);
}
int ast_ari_config_reload(void)
{
return process_config(1);
}
void ast_ari_config_destroy(void)
{
aco_info_destroy(&cfg_info);
ao2_global_obj_release(confs);
ast_sorcery_unref(sorcery);
sorcery = NULL;
ao2_cleanup(owc_states);
}

@ -27,7 +27,12 @@
#include "asterisk/http.h"
#include "asterisk/json.h"
#include "asterisk/md5.h"
#include "asterisk/sorcery.h"
#include "asterisk/stringfields.h"
#include "asterisk/websocket_client.h"
#include "ari_websockets.h"
/*! @{ */
@ -37,98 +42,139 @@
* \return 0 on success.
* \return Non-zero on error.
*/
int ast_ari_cli_register(void);
int ari_cli_register(void);
/*!
* \brief Unregister CLI commands for ARI.
*/
void ast_ari_cli_unregister(void);
void ari_cli_unregister(void);
/*! @} */
/*! @{ */
struct ast_ari_conf_general;
/*! \brief All configuration options for ARI. */
struct ast_ari_conf {
/*! The general section configuration options. */
struct ast_ari_conf_general *general;
/*! Configured users */
struct ao2_container *users;
};
/*! Max length for auth_realm field */
#define ARI_AUTH_REALM_LEN 256
/*! \brief Global configuration options for ARI. */
struct ast_ari_conf_general {
struct ari_conf_general {
SORCERY_OBJECT(details);
AST_DECLARE_STRING_FIELDS(
/*! Allowed CORS origins */
AST_STRING_FIELD(allowed_origins);
/*! Authentication realm */
AST_STRING_FIELD(auth_realm);
/*! Channel variables */
AST_STRING_FIELD(channelvars);
);
/*! Enabled by default, disabled if false. */
int enabled;
/*! Write timeout for websocket connections */
int write_timeout;
/*! Encoding format used during output (default compact). */
enum ast_json_encoding_format format;
/*! Authentication realm */
char auth_realm[ARI_AUTH_REALM_LEN];
AST_DECLARE_STRING_FIELDS(
AST_STRING_FIELD(allowed_origins);
);
};
/*! \brief Password format */
enum ast_ari_password_format {
enum ari_user_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 ast_ari_conf_user {
/*! Username for authentication */
char *username;
struct ari_conf_user {
SORCERY_OBJECT(details);
AST_DECLARE_STRING_FIELDS(
/*! User's password. */
char password[ARI_PASSWORD_LEN];
AST_STRING_FIELD(password);
);
/*! Format for the password field */
enum ast_ari_password_format password_format;
enum ari_user_password_format password_format;
/*! If true, user cannot execute change operations */
int read_only;
};
enum ari_conf_owc_fields {
ARI_OWC_FIELD_NONE = 0,
ARI_OWC_FIELD_WEBSOCKET_CONNECTION_ID = (1 << AST_WS_CLIENT_FIELD_USER_START),
ARI_OWC_FIELD_APPS = (1 << (AST_WS_CLIENT_FIELD_USER_START + 1)),
ARI_OWC_FIELD_LOCAL_ARI_USER = (1 << (AST_WS_CLIENT_FIELD_USER_START + 2)),
ARI_OWC_FIELD_LOCAL_ARI_PASSWORD = (1 << (AST_WS_CLIENT_FIELD_USER_START + 3)),
ARI_OWC_FIELD_SUBSCRIBE_ALL = (1 << (AST_WS_CLIENT_FIELD_USER_START + 4)),
ARI_OWC_NEEDS_RECONNECT = AST_WS_CLIENT_NEEDS_RECONNECT
| ARI_OWC_FIELD_WEBSOCKET_CONNECTION_ID | ARI_OWC_FIELD_LOCAL_ARI_USER
| ARI_OWC_FIELD_LOCAL_ARI_PASSWORD,
ARI_OWC_NEEDS_REREGISTER = ARI_OWC_FIELD_APPS | ARI_OWC_FIELD_SUBSCRIBE_ALL,
};
struct ari_conf_outbound_websocket {
SORCERY_OBJECT(details);
AST_DECLARE_STRING_FIELDS(
AST_STRING_FIELD(websocket_client_id); /*!< The ID of the websocket client to use */
AST_STRING_FIELD(apps); /*!< Stasis apps using this connection */
AST_STRING_FIELD(local_ari_user);/*!< The ARI user to act as */
AST_STRING_FIELD(local_ari_password); /*!< The password for the ARI user */
);
int invalid; /*!< Invalid configuration */
int subscribe_all; /*!< Subscribe to all events */
struct ast_websocket_client *websocket_client; /*!< The websocket client */
};
/*!
* \brief Initialize the ARI configuration
* \brief Detect changes between two outbound websocket configurations.
*
* \param old_owc The old outbound websocket configuration.
* \param new_owc The new outbound websocket configuration.
* \return A bitmask of changed fields.
*/
int ast_ari_config_init(void);
enum ari_conf_owc_fields ari_conf_owc_detect_changes(
struct ari_conf_outbound_websocket *old_owc,
struct ari_conf_outbound_websocket *new_owc);
/*!
* \brief Reload the ARI configuration
* \brief Get the outbound websocket configuration for a Stasis app.
*
* \param app_name The application name to search for.
* \param ws_type An OR'd list of ari_websocket_types or ARI_WS_TYPE_ANY.
*
* \retval ARI outbound websocket configuration object.
* \retval NULL if not found.
*/
int ast_ari_config_reload(void);
struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app(
const char *app_name, unsigned int ws_type);
enum ari_conf_load_flags {
ARI_CONF_INIT = (1 << 0), /*!< Initialize sorcery */
ARI_CONF_RELOAD = (1 << 1), /*!< Reload sorcery */
ARI_CONF_LOAD_GENERAL = (1 << 2), /*!< Load general config */
ARI_CONF_LOAD_USER = (1 << 3), /*!< Load user config */
ARI_CONF_LOAD_OWC = (1 << 4), /*!< Load outbound websocket config */
ARI_CONF_LOAD_ALL = ( /*!< Load all configs */
ARI_CONF_LOAD_GENERAL
| ARI_CONF_LOAD_USER
| ARI_CONF_LOAD_OWC),
};
/*!
* \brief Destroy the ARI configuration
* \brief (Re)load the ARI configuration
*/
void ast_ari_config_destroy(void);
int ari_conf_load(enum ari_conf_load_flags flags);
/*!
* \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.
* \retval NULL on error.
* \brief Destroy the ARI configuration
*/
struct ast_ari_conf *ast_ari_config_get(void);
void ari_conf_destroy(void);
struct ari_conf_general* ari_conf_get_general(void);
struct ao2_container *ari_conf_get_users(void);
struct ari_conf_user *ari_conf_get_user(const char *username);
struct ao2_container *ari_conf_get_owcs(void);
struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id);
enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id);
const char *ari_websocket_type_to_str(enum ast_websocket_type type);
int ari_sorcery_observer_add(const char *object_type,
const struct ast_sorcery_observer *callbacks);
int ari_sorcery_observer_remove(const char *object_type,
const struct ast_sorcery_observer *callbacks);
/*!
* \brief Validated a user's credentials.
@ -138,7 +184,7 @@ struct ast_ari_conf *ast_ari_config_get(void);
* \return User object.
* \retval NULL if username or password is invalid.
*/
struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
struct ari_conf_user *ari_conf_validate_user(const char *username,
const char *password);
/*! @} */

@ -74,117 +74,10 @@
/*** MODULEINFO
<depend type="module">res_http_websocket</depend>
<depend type="module">res_stasis</depend>
<depend type="module">res_websocket_client</depend>
<support_level>core</support_level>
***/
/*** DOCUMENTATION
<configInfo name="res_ari" language="en_US">
<synopsis>HTTP binding for the Stasis API</synopsis>
<configFile name="ari.conf">
<configObject name="general">
<since>
<version>12.0.0</version>
</since>
<synopsis>General configuration settings</synopsis>
<configOption name="enabled">
<since>
<version>12.0.0</version>
</since>
<synopsis>Enable/disable the ARI module</synopsis>
<description>
<para>This option enables or disables the ARI module.</para>
<note>
<para>ARI uses Asterisk's HTTP server, which must also be enabled in <filename>http.conf</filename>.</para>
</note>
</description>
<see-also>
<ref type="filename">http.conf</ref>
<ref type="link">https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/</ref>
</see-also>
</configOption>
<configOption name="websocket_write_timeout" default="100">
<since>
<version>11.11.0</version>
<version>12.4.0</version>
</since>
<synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis>
<description>
<para>If a websocket connection accepts input slowly, the timeout
for writes to it can be increased to keep it from being disconnected.
Value is in milliseconds.</para>
</description>
</configOption>
<configOption name="pretty">
<since>
<version>12.0.0</version>
</since>
<synopsis>Responses from ARI are formatted to be human readable</synopsis>
</configOption>
<configOption name="auth_realm">
<since>
<version>12.0.0</version>
</since>
<synopsis>Realm to use for authentication. Defaults to Asterisk REST Interface.</synopsis>
</configOption>
<configOption name="allowed_origins">
<since>
<version>12.0.0</version>
</since>
<synopsis>Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins.</synopsis>
</configOption>
<configOption name="channelvars">
<since>
<version>14.2.0</version>
</since>
<synopsis>Comma separated list of channel variables to display in channel json.</synopsis>
</configOption>
</configObject>
<configObject name="user">
<since>
<version>12.0.0</version>
</since>
<synopsis>Per-user configuration settings</synopsis>
<configOption name="type">
<since>
<version>13.30.0</version>
<version>16.7.0</version>
<version>17.1.0</version>
</since>
<synopsis>Define this configuration section as a user.</synopsis>
<description>
<enumlist>
<enum name="user"><para>Configure this section as a <replaceable>user</replaceable></para></enum>
</enumlist>
</description>
</configOption>
<configOption name="read_only">
<since>
<version>13.30.0</version>
<version>16.7.0</version>
<version>17.1.0</version>
</since>
<synopsis>When set to yes, user is only authorized for read-only requests</synopsis>
</configOption>
<configOption name="password">
<since>
<version>13.30.0</version>
<version>16.7.0</version>
<version>17.1.0</version>
</since>
<synopsis>Crypted or plaintext password (see password_format)</synopsis>
</configOption>
<configOption name="password_format">
<since>
<version>12.0.0</version>
</since>
<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>
***/
#include "asterisk.h"
#include "ari/internal.h"
@ -202,8 +95,8 @@
/*! \brief Helper function to check if module is enabled. */
static int is_enabled(void)
{
RAII_VAR(struct ast_ari_conf *, cfg, ast_ari_config_get(), ao2_cleanup);
return cfg && cfg->general && cfg->general->enabled;
RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
return general && general->enabled;
}
/*! Lock for \ref root_handler */
@ -389,9 +282,9 @@ static void add_allow_header(struct stasis_rest_handlers *handler,
static int origin_allowed(const char *origin)
{
RAII_VAR(struct ast_ari_conf *, cfg, ast_ari_config_get(), ao2_cleanup);
RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
char *allowed = ast_strdupa(cfg->general->allowed_origins);
char *allowed = ast_strdupa(general ? general->allowed_origins : "");
char *current;
while ((current = strsep(&allowed, ","))) {
@ -555,7 +448,7 @@ static void handle_options(struct stasis_rest_handlers *handler,
* \return User object for the authenticated user.
* \retval NULL if authentication failed.
*/
static struct ast_ari_conf_user *authenticate_api_key(const char *api_key)
static struct ari_conf_user *authenticate_api_key(const char *api_key)
{
RAII_VAR(char *, copy, NULL, ast_free);
char *username;
@ -572,7 +465,7 @@ static struct ast_ari_conf_user *authenticate_api_key(const char *api_key)
return NULL;
}
return ast_ari_config_validate_user(username, password);
return ari_conf_validate_user(username, password);
}
/*!
@ -583,7 +476,7 @@ static struct ast_ari_conf_user *authenticate_api_key(const char *api_key)
* \return User object for the authenticated user.
* \retval NULL if authentication failed.
*/
static struct ast_ari_conf_user *authenticate_user(struct ast_variable *get_params,
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);
@ -592,7 +485,7 @@ static struct ast_ari_conf_user *authenticate_user(struct ast_variable *get_para
/* HTTP Basic authentication */
http_auth = ast_http_get_auth(headers);
if (http_auth) {
return ast_ari_config_validate_user(http_auth->userid,
return ari_conf_validate_user(http_auth->userid,
http_auth->password);
}
@ -642,8 +535,8 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
struct stasis_rest_handlers *handler = NULL;
struct stasis_rest_handlers *wildcard_handler = NULL;
RAII_VAR(struct ast_variable *, path_vars, NULL, ast_variables_destroy);
RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
RAII_VAR(struct ast_ari_conf *, conf, ast_ari_config_get(), ao2_cleanup);
RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
char *path = ast_strdupa(uri);
char *path_segment = NULL;
@ -651,7 +544,7 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
SCOPE_ENTER(3, "Request: %s %s, path:%s\n", ast_get_http_method(method), uri, path);
if (!conf || !conf->general) {
if (!general) {
if (ser && source == ARI_INVOKE_SOURCE_REST) {
ast_http_request_close_on_completion(ser);
}
@ -679,7 +572,7 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
*/
ast_str_append(&response->headers, 0,
"WWW-Authenticate: Basic realm=\"%s\"\r\n",
conf->general->auth_realm);
general->auth_realm);
SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n",
response->response_code, response->response_text);
} else if (!ast_fully_booted) {
@ -1014,9 +907,8 @@ static void process_cors_request(struct ast_variable *headers,
enum ast_json_encoding_format ast_ari_json_format(void)
{
RAII_VAR(struct ast_ari_conf *, cfg, NULL, ao2_cleanup);
cfg = ast_ari_config_get();
return cfg->general->format;
RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
return general ? general->format : AST_JSON_COMPACT;
}
/*!
@ -1230,14 +1122,14 @@ static int unload_module(void)
{
ari_websocket_unload_module();
ast_ari_cli_unregister();
ari_cli_unregister();
if (is_enabled()) {
ast_debug(3, "Disabling ARI\n");
ast_http_uri_unlink(&http_uri);
}
ast_ari_config_destroy();
ari_conf_destroy();
ao2_cleanup(root_handler);
root_handler = NULL;
@ -1272,12 +1164,33 @@ static int load_module(void)
return AST_MODULE_LOAD_DECLINE;
}
if (ast_ari_config_init() != 0) {
/*
* ari_websocket_load_module() needs to know if ARI is enabled
* globally so it needs the "general" config to be loaded but it
* also needs to register a sorcery object observer for
* "outbound_websocket" BEFORE the outbound_websocket configs are loaded.
* outbound_websocket in turn needs the users to be loaded so we'll
* initialize sorcery and load "general" and "user" configs first, then
* load the websocket module, then load the "outbound_websocket" configs
* which will fire the observers.
*/
if (ari_conf_load(ARI_CONF_INIT | ARI_CONF_LOAD_GENERAL | ARI_CONF_LOAD_USER) != 0) {
unload_module();
return AST_MODULE_LOAD_DECLINE;
}
if (ari_websocket_load_module() != AST_MODULE_LOAD_SUCCESS) {
if (ari_websocket_load_module(is_enabled()) != AST_MODULE_LOAD_SUCCESS) {
unload_module();
return AST_MODULE_LOAD_DECLINE;
}
/*
* Now we can load the outbound_websocket configs which will
* fire the observers.
*/
ari_conf_load(ARI_CONF_LOAD_OWC);
if (ari_cli_register() != 0) {
unload_module();
return AST_MODULE_LOAD_DECLINE;
}
@ -1289,26 +1202,22 @@ static int load_module(void)
ast_debug(3, "ARI disabled\n");
}
if (ast_ari_cli_register() != 0) {
unload_module();
return AST_MODULE_LOAD_DECLINE;
}
return AST_MODULE_LOAD_SUCCESS;
}
static int reload_module(void)
{
char was_enabled = is_enabled();
int is_now_enabled = 0;
if (ast_ari_config_reload() != 0) {
return AST_MODULE_LOAD_DECLINE;
}
ari_conf_load(ARI_CONF_RELOAD | ARI_CONF_LOAD_ALL);
is_now_enabled = is_enabled();
if (was_enabled && !is_enabled()) {
if (was_enabled && !is_now_enabled) {
ast_debug(3, "Disabling ARI\n");
ast_http_uri_unlink(&http_uri);
} else if (!was_enabled && is_enabled()) {
} else if (!was_enabled && is_now_enabled) {
ast_debug(3, "Enabling ARI\n");
ast_http_uri_link(&http_uri);
}
@ -1321,6 +1230,6 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_
.load = load_module,
.unload = unload_module,
.reload = reload_module,
.requires = "http,res_stasis,res_http_websocket",
.requires = "http,res_stasis,res_http_websocket,res_websocket_client",
.load_pri = AST_MODPRI_APP_DEPEND,
);

@ -169,6 +169,8 @@
"RecordingFinished",
"RecordingFailed",
"ApplicationMoveFailed",
"ApplicationRegistered",
"ApplicationUnregistered",
"ApplicationReplaced",
"BridgeCreated",
"BridgeDestroyed",
@ -366,6 +368,16 @@
}
}
},
"ApplicationRegistered": {
"id": "ApplicationRegistered",
"description": "Notification that a Stasis app has been registered.",
"properties": {}
},
"ApplicationUnregistered": {
"id": "ApplicationUnregistered",
"description": "Notification that a Stasis app has been unregistered.",
"properties": {}
},
"ApplicationReplaced": {
"id": "ApplicationReplaced",
"description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.",

Loading…
Cancel
Save