mirror of
https://github.com/asterisk/asterisk.git
synced 2025-09-02 03:02:04 +00:00
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
(cherry picked from commit 1c0d552155
)
This commit is contained in:
@@ -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))
|
||||
|
157
res/ari/ari_doc.xml
Normal file
157
res/ari/ari_doc.xml
Normal file
@@ -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_ */
|
||||
|
498
res/ari/cli.c
498
res/ari/cli.c
@@ -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,63 +115,37 @@ 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));
|
||||
}
|
||||
ao2_ref(object, -1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *complete_ari_user(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,
|
||||
};
|
||||
|
||||
conf = ast_ari_config_get();
|
||||
if (!conf) {
|
||||
ast_cli(a->fd, "Error getting ARI configuration\n");
|
||||
return CLI_FAILURE;
|
||||
}
|
||||
|
||||
user = ao2_callback_data(conf->users,
|
||||
ast_strlen_zero(a->word) ? 0 : OBJ_PARTIAL_KEY,
|
||||
complete_ari_user_search, (char*)a->word, &search);
|
||||
|
||||
return user ? ast_strdup(user->username) : NULL;
|
||||
}
|
||||
|
||||
static char *complete_ari_show_user(struct ast_cli_args *a)
|
||||
{
|
||||
if (a->pos == 3) {
|
||||
return complete_ari_user(a);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
ao2_iterator_destroy(&i);
|
||||
}
|
||||
|
||||
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);
|
||||
RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
|
||||
RAII_VAR(struct ao2_container *, users, ari_conf_get_users(), ao2_cleanup);
|
||||
|
||||
if (!users) {
|
||||
ast_cli(a->fd, "Error getting ARI configuration\n");
|
||||
return CLI_FAILURE;
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case CLI_INIT:
|
||||
@@ -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,55 +260,31 @@ 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));
|
||||
}
|
||||
ao2_ref(object, -1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *complete_ari_app(struct ast_cli_args *a, int include_all)
|
||||
{
|
||||
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;
|
||||
ao2_iterator_destroy(&i);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!apps) {
|
||||
ast_cli(a->fd, "Error getting ARI applications\n");
|
||||
return CLI_FAILURE;
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case CLI_INIT:
|
||||
@@ -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));
|
||||
}
|
||||
|
935
res/ari/config.c
935
res/ari/config.c
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
/*! User's password. */
|
||||
char password[ARI_PASSWORD_LEN];
|
||||
struct ari_conf_user {
|
||||
SORCERY_OBJECT(details);
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
/*! User's password. */
|
||||
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;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Initialize the ARI configuration
|
||||
*/
|
||||
int ast_ari_config_init(void);
|
||||
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 Reload 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_reload(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 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.
|
||||
*/
|
||||
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 (Re)load the ARI configuration
|
||||
*/
|
||||
int ari_conf_load(enum ari_conf_load_flags flags);
|
||||
|
||||
/*!
|
||||
* \brief Destroy the ARI configuration
|
||||
*/
|
||||
void ast_ari_config_destroy(void);
|
||||
void ari_conf_destroy(void);
|
||||
|
||||
/*!
|
||||
* \brief Get the current ARI configuration.
|
||||
*
|
||||
* This is an immutable object, so don't modify it. It is AO2 managed, so
|
||||
* ao2_cleanup() when you're done with it.
|
||||
*
|
||||
* \return ARI configuration object.
|
||||
* \retval NULL on error.
|
||||
*/
|
||||
struct ast_ari_conf *ast_ari_config_get(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);
|
||||
|
||||
/*! @} */
|
||||
|
185
res/res_ari.c
185
res/res_ari.c
@@ -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);
|
||||
|
||||
if (was_enabled && !is_enabled()) {
|
||||
is_now_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.",
|
||||
|
Reference in New Issue
Block a user