pull/1212/merge
George Joseph 2 days ago committed by GitHub
commit 4acfeca702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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,66 @@ 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"
;connection_type = persistent : "persistent" or "per_call_config"
; Default: none
;uri = ws://localhost:8765 ; The URI needed to contact the remote server
; 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
;protocols = ari ; The websocket protocol expected by the server.
; Default: none
;username = username ; An authentication username if required by the server.
; Default: none
;password = password ; The authentication password for the username.
; Default: none
;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
;connection_timeout = 500 ; Connection timeout in milliseconds.
; Default: 500
;reconnect_interval = 1000 ; Number of milliseconds between (re)connection
; attempts.
; Default: 500
;reconnect_attempts = 4 ; The number of (re)connection attempts to make
; before returning an error. For persistent
; connections, set to -1 to retry indefinitely.
; Default: 4 for per_call connections and -1 for
; persistent connections.
;tls_enabled = no ; Set to "yes" to enable TLS connections.
; Default: no
;ca_list_file = /etc/pki/tls/cert.pem
; A file containing all CA certificates needed
; for the connection. Not needed if your server
; has a certificate from a recognized CA.
; Default: none
;ca_list_path = /etc/pki/ca-trust/extracted/pem/directory-hash
; A directory containing individual CA certificates
; as an alternative to ca_list_file. Rarely needed.
; Default: none
;cert_file = /etc/asterisk/cert.pem
; If the server requires you to have a client
; certificate, specify it here and if it wasn't
; issued by a recognized CA, make sure the matching
; CA certificate is available in ca_list_file or
; ca_list_path.
; Default: none
;priv_key_file = /etc/asterisk/privkey.pem
; The private key for the client certificate.
;verify_server_cert = no ; Verify that the server certificate is valid.
; Default: yes
;verify_server_hostname = no ; Verify that the hostname in the server's certificate
; matches the hostname in the URI configured above.
; Default: yes

@ -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,283 @@
<!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.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Outbound websocket configuration</synopsis>
<configOption name="type">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Must be "outbound_websocket".</synopsis>
</configOption>
<configOption name="uri">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Full URI to remote server.</synopsis>
</configOption>
<configOption name="protocols">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Comma separated list of protocols acceptable to the server.</synopsis>
</configOption>
<configOption name="apps">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Comma separated list of stasis applications that will use this websocket.</synopsis>
</configOption>
<configOption name="username">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Server authentication username if required.</synopsis>
</configOption>
<configOption name="password">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Server authentication password if required.</synopsis>
</configOption>
<configOption name="local_ari_user">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>The local ARI user to act as.</synopsis>
</configOption>
<configOption name="connection_type">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Single persistent connection or per-call configuration.</synopsis>
<description>
<enumlist>
<enum name="persistent"><para>Single persistent connection for all calls.</para></enum>
<enum name="per_call_config"><para>New connection for each call to the Stasis() dialplan app.</para></enum>
</enumlist>
</description>
</configOption>
<configOption name="subscribe_all">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Subscribe applications to all event</synopsis>
</configOption>
<configOption name="connection_timeout">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Connection timeout (ms).</synopsis>
</configOption>
<configOption name="reconnect_attempts">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>On failure, how many times should reconnection be attempted?</synopsis>
<description>
<para>If <literal>0</literal>, no reconnection will be attempted and
an error will be returned immediately on failure. If a negative
number, reconnection will be attempted forever. Any other positive value
will attempt reconnection the specified number of times before returning
an error.
</para>
</description>
</configOption>
<configOption name="reconnect_interval">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>How often should reconnection be attempted (ms)?</synopsis>
</configOption>
<configOption name="tls_enabled">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Enable TLS</synopsis>
</configOption>
<configOption name="ca_list_file">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>File containing the server's CA certificate. (optional)</synopsis>
</configOption>
<configOption name="ca_list_path">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>Path to a directory containing one or more hashed CA certificates. (optional)</synopsis>
</configOption>
<configOption name="cert_file">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>File containing a client certificate. (optional)</synopsis>
</configOption>
<configOption name="priv_key_file">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>File containing the client's private key. (optional)</synopsis>
</configOption>
<configOption name="verify_server_cert">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>If set to true, verify the server's certificate. (optional)</synopsis>
</configOption>
<configOption name="verify_server_hostname">
<since>
<version>20.14.0</version>
<version>21.9.0</version>
<version>22.4.0</version>
</since>
<synopsis>If set to true, verify that the server's hostname matches the common name in it's certificate. (optional)</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;
@ -6085,9 +6243,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
@ -6301,9 +6465,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.
*
@ -1612,11 +1644,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

@ -35,19 +35,54 @@ 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)
enum ari_websocket_type {
ARI_WS_TYPE_OUTBOUND_PERSISTENT = (1 << 0),
ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG = (1 << 1),
ARI_WS_TYPE_OUTBOUND_PER_CALL = (1 << 2),
ARI_WS_TYPE_INBOUND = (1 << 3),
ARI_WS_TYPE_ANY = (0xFFFFFFFF),
};
struct ari_ws_session {
enum ari_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 ari_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 +126,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;
if (++search->which > search->state) {
return CMP_MATCH;
}
return 0;
size_t wordlen = strlen(word);
void *object;
struct ao2_iterator i = ao2_iterator_init(container, 0);
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);
}
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));
}
ao2_ref(object, -1);
}
return 0;
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->connection_type),
owc->apps,
invalid_fields == ARI_OWC_FIELD_NONE ? "valid" : "INVALID",
owc->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->uri);
ast_cli(a->fd, "protocols = %s\n", owc->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->username);
ast_cli(a->fd, "password = %s\n", S_COR(owc->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->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->connect_timeout);
ast_cli(a->fd, "reconnect_attempts = %d\n", owc->reconnect_attempts);
ast_cli(a->fd, "reconnect_interval = %d\n", owc->reconnect_interval);
ast_cli(a->fd, "tls_enabled = %s\n", AST_CLI_YESNO(owc->tls_enabled));
ast_cli(a->fd, "ca_list_file = %s\n", owc->ca_list_file);
ast_cli(a->fd, "ca_list_path = %s\n", owc->ca_list_path);
ast_cli(a->fd, "cert_file = %s\n", owc->cert_file);
ast_cli(a->fd, "priv_key_file = %s\n", owc->priv_key_file);
ast_cli(a->fd, "verify_server = %s\n", AST_CLI_YESNO(owc->verify_server_cert));
ast_cli(a->fd, "verify_server = %s\n", AST_CLI_YESNO(owc->verify_server_cert));
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 == ARI_WS_TYPE_OUTBOUND_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));
}

File diff suppressed because it is too large Load Diff

@ -27,7 +27,11 @@
#include "asterisk/http.h"
#include "asterisk/json.h"
#include "asterisk/md5.h"
#include "asterisk/sorcery.h"
#include "asterisk/stringfields.h"
#include "ari_websockets.h"
/*! @{ */
@ -37,98 +41,172 @@
* \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;
};
enum ari_conf_owc_fields {
ARI_OWC_FIELD_NONE = 0,
ARI_OWC_FIELD_URI = (1 << 0),
ARI_OWC_FIELD_PROTOCOLS = (1 << 1),
ARI_OWC_FIELD_APPS = (1 << 2),
ARI_OWC_FIELD_USERNAME = (1 << 3),
ARI_OWC_FIELD_PASSWORD = (1 << 4),
ARI_OWC_FIELD_LOCAL_ARI_USER = (1 << 5),
ARI_OWC_FIELD_LOCAL_ARI_PASSWORD = (1 << 6),
ARI_OWC_FIELD_TLS_ENABLED = (1 << 7),
ARI_OWC_FIELD_CA_LIST_FILE = (1 << 8),
ARI_OWC_FIELD_CA_LIST_PATH = (1 << 9),
ARI_OWC_FIELD_CERT_FILE = (1 << 10),
ARI_OWC_FIELD_PRIV_KEY_FILE = (1 << 11),
ARI_OWC_FIELD_SUBSCRIBE_ALL = (1 << 12),
ARI_OWC_FIELD_CONNECTION_TYPE = (1 << 13),
ARI_OWC_FIELD_RECONNECT_INTERVAL = (1 << 14),
ARI_OWC_FIELD_RECONNECT_ATTEMPTS = (1 << 15),
ARI_OWC_FIELD_CONNECTION_TIMEOUT = (1 << 16),
ARI_OWC_FIELD_VERIFY_SERVER_CERT = (1 << 17),
ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME = (1 << 18),
ARI_OWC_NEEDS_RECONNECT = ARI_OWC_FIELD_URI | ARI_OWC_FIELD_PROTOCOLS
| ARI_OWC_FIELD_CONNECTION_TYPE
| ARI_OWC_FIELD_USERNAME | ARI_OWC_FIELD_PASSWORD
| ARI_OWC_FIELD_LOCAL_ARI_USER | ARI_OWC_FIELD_LOCAL_ARI_PASSWORD
| ARI_OWC_FIELD_TLS_ENABLED | ARI_OWC_FIELD_CA_LIST_FILE
| ARI_OWC_FIELD_CA_LIST_PATH | ARI_OWC_FIELD_CERT_FILE
| ARI_OWC_FIELD_PRIV_KEY_FILE | ARI_OWC_FIELD_VERIFY_SERVER_CERT
| ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME,
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(uri); /*!< Server URI */
AST_STRING_FIELD(protocols); /*!< Websocket protocols to use with server */
AST_STRING_FIELD(apps); /*!< Stasis apps using this connection */
AST_STRING_FIELD(username); /*!< Auth user name */
AST_STRING_FIELD(password); /*!< Auth password */
AST_STRING_FIELD(local_ari_user);/*!< The ARI user to act as */
AST_STRING_FIELD(local_ari_password);/*!< The password for the ARI user */
AST_STRING_FIELD(ca_list_file); /*!< CA file */
AST_STRING_FIELD(ca_list_path); /*!< CA path */
AST_STRING_FIELD(cert_file); /*!< Certificate file */
AST_STRING_FIELD(priv_key_file); /*!< Private key file */
);
int invalid; /*!< Invalid configuration */
enum ari_conf_owc_fields invalid_fields; /*!< Invalid fields */
enum ari_websocket_type connection_type; /*!< Connection type */
int connect_timeout; /*!< Connection timeout (ms) */
int subscribe_all; /*!< Subscribe to all events */
unsigned int reconnect_attempts; /*!< How many attempts before returning an error */
unsigned int reconnect_interval; /*!< How often to attempt a reconnect (ms) */
int tls_enabled; /*!< TLS enabled */
int verify_server_cert; /*!< Verify server certificate */
int verify_server_hostname; /*!< Verify server hostname */
};
/*!
* \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 ari_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 +216,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);
/*! @} */

@ -77,114 +77,6 @@
<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 +94,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 +281,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 +447,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 +464,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 +475,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 +484,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 +534,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 +543,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 +571,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 +906,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 +1121,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 +1163,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(is_enabled()) != AST_MODULE_LOAD_SUCCESS) {
unload_module();
return AST_MODULE_LOAD_DECLINE;
}
if (ari_websocket_load_module() != AST_MODULE_LOAD_SUCCESS) {
/*
* 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 +1201,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);
}

@ -169,6 +169,8 @@
"RecordingFinished",
"RecordingFailed",
"ApplicationMoveFailed",
"ApplicationRegistered",
"ApplicationUnregistered",
"ApplicationReplaced",
"BridgeCreated",
"BridgeDestroyed",
@ -367,6 +369,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