diff --git a/apps/app_stasis.c b/apps/app_stasis.c
index bbe7718cc8..5809b06fd3 100644
--- a/apps/app_stasis.c
+++ b/apps/app_stasis.c
@@ -25,12 +25,14 @@
/*** MODULEINFO
res_stasis
+ res_ari
core
***/
#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",
);
diff --git a/configs/samples/ari.conf.sample b/configs/samples/ari.conf.sample
index 5ce3166bfa..be47297279 100644
--- a/configs/samples/ari.conf.sample
+++ b/configs/samples/ari.conf.sample
@@ -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
+
diff --git a/include/asterisk/ari.h b/include/asterisk/ari.h
index 165b6a867d..a3285c13f9 100644
--- a/include/asterisk/ari.h
+++ b/include/asterisk/ari.h
@@ -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 "-" 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 */
diff --git a/include/asterisk/vector.h b/include/asterisk/vector.h
index 8276a7c497..e71db648b7 100644
--- a/include/asterisk/vector.h
+++ b/include/asterisk/vector.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
*
diff --git a/main/strings.c b/main/strings.c
index f370466776..a524d55025 100644
--- a/main/strings.c
+++ b/main/strings.c
@@ -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))
diff --git a/res/ari/ari_doc.xml b/res/ari/ari_doc.xml
new file mode 100644
index 0000000000..c31a00ffe9
--- /dev/null
+++ b/res/ari/ari_doc.xml
@@ -0,0 +1,283 @@
+
+
+
+
+ HTTP binding for the Stasis API
+
+
+
+ 12.0.0
+
+ General configuration settings
+
+
+ 12.0.0
+
+ Enable/disable the ARI module
+
+ This option enables or disables the ARI module.
+
+ ARI uses Asterisk's HTTP server, which must also be enabled in http.conf.
+
+
+
+ [http.conf]
+ [https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/]
+
+
+
+
+ 11.11.0
+ 12.4.0
+
+ The timeout (in milliseconds) to set on WebSocket connections.
+
+ 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.
+
+
+
+
+ 12.0.0
+
+ Responses from ARI are formatted to be human readable
+
+
+
+ 12.0.0
+
+ Realm to use for authentication. Defaults to Asterisk REST Interface.
+
+
+
+ 12.0.0
+
+ Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins.
+
+
+
+ 14.2.0
+
+ Comma separated list of channel variables to display in channel json.
+
+
+
+
+
+ 12.0.0
+
+ Per-user configuration settings
+
+
+ 13.30.0
+ 16.7.0
+ 17.1.0
+
+ Define this configuration section as a user.
+
+
+ Configure this section as a user
+
+
+
+
+
+ 13.30.0
+ 16.7.0
+ 17.1.0
+
+ When set to yes, user is only authorized for read-only requests
+
+
+
+ 13.30.0
+ 16.7.0
+ 17.1.0
+
+ Crypted or plaintext password (see password_format)
+
+
+
+ 12.0.0
+
+ 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
+
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Outbound websocket configuration
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Must be "outbound_websocket".
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Full URI to remote server.
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Comma separated list of protocols acceptable to the server.
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Comma separated list of stasis applications that will use this websocket.
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Server authentication username if required.
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Server authentication password if required.
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ The local ARI user to act as.
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Single persistent connection or per-call configuration.
+
+
+ Single persistent connection for all calls.
+ New connection for each call to the Stasis() dialplan app.
+
+
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Subscribe applications to all event
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Connection timeout (ms).
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ On failure, how many times should reconnection be attempted?
+
+ If 0, 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.
+
+
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ How often should reconnection be attempted (ms)?
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Enable TLS
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ File containing the server's CA certificate. (optional)
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ Path to a directory containing one or more hashed CA certificates. (optional)
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ File containing a client certificate. (optional)
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ File containing the client's private key. (optional)
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ If set to true, verify the server's certificate. (optional)
+
+
+
+ 20.14.0
+ 21.9.0
+ 22.4.0
+
+ If set to true, verify that the server's hostname matches the common name in it's certificate. (optional)
+
+
+
+
+
\ No newline at end of file
diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c
index a1dda282b8..5ef3821112 100644
--- a/res/ari/ari_model_validators.c
+++ b/res/ari/ari_model_validators.c
@@ -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
diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h
index a351b44304..58701c2af1 100644
--- a/res/ari/ari_model_validators.h
+++ b/res/ari/ari_model_validators.h
@@ -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)
diff --git a/res/ari/ari_websockets.c b/res/ari/ari_websockets.c
index 3712dd7d10..005f986af3 100644
--- a/res/ari/ari_websockets.c
+++ b/res/ari/ari_websockets.c
@@ -21,15 +21,17 @@
#include "resource_events.h"
#include "ari_websockets.h"
#include "internal.h"
-#if defined(AST_DEVMODE)
#include "ari_model_validators.h"
-#endif
#include "asterisk/app.h"
#include "asterisk/ari.h"
#include "asterisk/astobj2.h"
#include "asterisk/http_websocket.h"
#include "asterisk/module.h"
+#include "asterisk/pbx.h"
#include "asterisk/stasis_app.h"
+#include "asterisk/time.h"
+#include "asterisk/uuid.h"
+#include "asterisk/vector.h"
/*! \file
@@ -38,30 +40,36 @@
* \author David M. Lee, II
*/
-/*! Number of buckets for the event session registry. Remember to keep it a prime number! */
-#define ARI_WS_SESSION_NUM_BUCKETS 23
+/*! Number of buckets for the ari_ws_session registry. Remember to keep it a prime number! */
+#define SESSION_REGISTRY_NUM_BUCKETS 23
-/*! Number of buckets for a websocket apps container. Remember to keep it a prime number! */
-#define APPS_NUM_BUCKETS 7
+/*! Initial size of websocket session apps vector */
+#define APPS_INIT_SIZE 7
-/*! Initial size of a message queue. */
+/*! Initial size of the websocket session message queue. */
#define MESSAGES_INIT_SIZE 23
+#define ARI_CONTEXT_REGISTRAR "res_ari"
-/*! \brief Local registry for created \ref event_session objects. */
-static struct ao2_container *ari_ws_session_registry;
+/*! \brief Local registry for created \ref ari_ws_session objects. */
+static struct ao2_container *session_registry;
struct ast_websocket_server *ast_ws_server;
-#define MAX_VALS 128
+#if defined(AST_DEVMODE)
+ ari_validator ari_validate_message_fn = ast_ari_validate_message;
+#else
+ /*!
+ * \brief Validator that always succeeds.
+ */
+ static int null_validator(struct ast_json *json)
+ {
+ return 1;
+ }
+
+ ari_validator ari_validate_message_fn = null_validator;
+#endif
-/*!
- * \brief Validator that always succeeds.
- */
-static int null_validator(struct ast_json *json)
-{
- return 1;
-}
#define VALIDATION_FAILED \
"{" \
@@ -69,16 +77,18 @@ static int null_validator(struct ast_json *json)
" \"message\": \"Message validation failed\"" \
"}"
-static int ari_ws_session_write(
- struct ari_ws_session *ari_ws_session,
- struct ast_json *message)
+static int session_write(struct ari_ws_session *session, struct ast_json *message)
{
RAII_VAR(char *, str, NULL, ast_json_free);
+ if (!session || !session->ast_ws_session || !message) {
+ return -1;
+ }
+
#ifdef AST_DEVMODE
- if (!ari_ws_session->validator(message)) {
+ if (!session->validator(message)) {
ast_log(LOG_ERROR, "Outgoing message failed validation\n");
- return ast_websocket_write_string(ari_ws_session->ast_ws_session, VALIDATION_FAILED);
+ return ast_websocket_write_string(session->ast_ws_session, VALIDATION_FAILED);
}
#endif
@@ -89,73 +99,108 @@ static int ari_ws_session_write(
return -1;
}
- if (ast_websocket_write_string(ari_ws_session->ast_ws_session, str)) {
+ if (ast_websocket_write_string(session->ast_ws_session, str)) {
ast_log(LOG_NOTICE, "Problem occurred during websocket write to %s, websocket closed\n",
- ast_sockaddr_stringify(ast_websocket_remote_address(ari_ws_session->ast_ws_session)));
+ ast_sockaddr_stringify(ast_websocket_remote_address(session->ast_ws_session)));
return -1;
}
return 0;
}
-/*!
- * \internal
- * \brief Updates the websocket session.
- *
- * \details If the value of the \c ws_session is not \c NULL and there are messages in the
- * event session's \c message_queue, the messages are dispatched and removed from
- * the queue.
- *
- * \param ari_ws_session The ARI websocket session
- * \param ast_ws_session The Asterisk websocket session
- */
-static int ari_ws_session_update(
- struct ari_ws_session *ari_ws_session,
- struct ast_websocket *ast_ws_session)
+static void session_send_or_queue(struct ari_ws_session *session,
+ struct ast_json *message, const char *msg_type, const char *app_name,
+ int debug_app)
{
- RAII_VAR(struct ast_ari_conf *, config, ast_ari_config_get(), ao2_cleanup);
- int i;
+ const char *msg_timestamp, *msg_ast_id;
- if (ast_ws_session == NULL) {
- return -1;
+ msg_timestamp = S_OR(
+ ast_json_string_get(ast_json_object_get(message, "timestamp")), "");
+ if (ast_strlen_zero(msg_timestamp)) {
+ if (ast_json_object_set(message, "timestamp", ast_json_timeval(ast_tvnow(), NULL))) {
+ ast_log(LOG_ERROR,
+ "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n",
+ session->remote_addr, msg_type, app_name);
+ return;
+ }
}
- if (config == NULL || config->general == NULL) {
- return -1;
- }
+ msg_ast_id = S_OR(
+ ast_json_string_get(ast_json_object_get(message, "asterisk_id")), "");
+ if (ast_strlen_zero(msg_ast_id)) {
+ char eid[20];
- if (ast_websocket_set_nonblock(ast_ws_session) != 0) {
- ast_log(LOG_ERROR,
- "ARI web socket failed to set nonblock; closing: %s\n",
- strerror(errno));
- return -1;
+ if (ast_json_object_set(message, "asterisk_id",
+ ast_json_string_create(ast_eid_to_str(eid, sizeof(eid), &ast_eid_default)))) {
+ ao2_unlock(session);
+ ast_log(LOG_ERROR,
+ "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n",
+ session->remote_addr, msg_type, app_name);
+ }
}
- if (ast_websocket_set_timeout(ast_ws_session, config->general->write_timeout)) {
- ast_log(LOG_WARNING, "Failed to set write timeout %d on ARI web socket\n",
- config->general->write_timeout);
- }
+ if (!session->ast_ws_session) {
+ /* If the websocket is NULL, the message goes to the queue */
+ if (AST_VECTOR_APPEND(&session->message_queue, message) == 0) {
+ ast_json_ref(message);
+ }
+ /*
+ * If the msg_type one of the Application* types, the websocket
+ * might not be there yet so don't log.
+ */
+ if (!ast_begins_with(msg_type, "Application")) {
+ ast_log(LOG_WARNING,
+ "%s: Queued '%s' message for Stasis app '%s'; websocket is not ready\n",
+ session->remote_addr,
+ msg_type,
+ app_name);
+ }
+ } else {
- ao2_ref(ast_ws_session, +1);
- ari_ws_session->ast_ws_session = ast_ws_session;
- ao2_lock(ari_ws_session);
- for (i = 0; i < AST_VECTOR_SIZE(&ari_ws_session->message_queue); i++) {
- struct ast_json *msg = AST_VECTOR_GET(&ari_ws_session->message_queue, i);
- ari_ws_session_write(ari_ws_session, msg);
- ast_json_unref(msg);
+ if (DEBUG_ATLEAST(4) || debug_app) {
+ char *str = ast_json_dump_string_format(message, AST_JSON_PRETTY);
+
+ ast_verbose("<--- Sending ARI event to %s --->\n%s\n",
+ session->remote_addr,
+ str);
+ ast_json_free(str);
+ }
+ session_write(session, message);
}
+}
- AST_VECTOR_RESET(&ari_ws_session->message_queue, AST_VECTOR_ELEM_CLEANUP_NOOP);
- ao2_unlock(ari_ws_session);
+static void session_send_app_event(struct ari_ws_session *session,
+ const char *event_type, const char *app_name)
+{
+ char eid[20];
+ int debug_app = stasis_app_get_debug_by_name(app_name);
+ struct ast_json *msg = ast_json_pack("{s:s, s:o?, s:s, s:s }",
+ "type", event_type,
+ "timestamp", ast_json_timeval(ast_tvnow(), NULL),
+ "application", app_name,
+ "asterisk_id", ast_eid_to_str(eid, sizeof(eid), &ast_eid_default));
- return 0;
+ if (!msg) {
+ return;
+ }
+ ast_debug(3, "%s: Sending '%s' event to app '%s'\n", session->session_id,
+ event_type, app_name);
+ /*
+ * We don't want to use ari_websocket_send_event() here because
+ * the app may be unregistered which will cause stasis_app_event_allowed
+ * to return false.
+ */
+ session_send_or_queue(session, msg, event_type, app_name, debug_app);
+ ast_json_unref(msg);
}
-static struct ast_json *ari_ws_session_read(
- struct ari_ws_session *ari_ws_session)
+static struct ast_json *session_read(struct ari_ws_session *session)
{
RAII_VAR(struct ast_json *, message, NULL, ast_json_unref);
- if (ast_websocket_fd(ari_ws_session->ast_ws_session) < 0) {
+ if (!session || !session->ast_ws_session) {
+ return NULL;
+ }
+ if (ast_websocket_fd(session->ast_ws_session) < 0) {
return NULL;
}
@@ -167,7 +212,7 @@ static struct ast_json *ari_ws_session_read(
int fragmented;
res = ast_wait_for_input(
- ast_websocket_fd(ari_ws_session->ast_ws_session), -1);
+ ast_websocket_fd(session->ast_ws_session), -1);
if (res <= 0) {
ast_log(LOG_WARNING, "WebSocket poll error: %s\n",
@@ -175,7 +220,7 @@ static struct ast_json *ari_ws_session_read(
return NULL;
}
- res = ast_websocket_read(ari_ws_session->ast_ws_session, &payload,
+ res = ast_websocket_read(session->ast_ws_session, &payload,
&payload_len, &opcode, &fragmented);
if (res != 0) {
@@ -200,7 +245,7 @@ static struct ast_json *ari_ws_session_read(
"reason_phrase", "Failed to parse request message JSON",
"uri", ""
);
- ari_websocket_send_event(ari_ws_session, ari_ws_session->app_name,
+ ari_websocket_send_event(session, session->app_name,
error, 0);
ast_json_unref(error);
ast_log(LOG_WARNING,
@@ -241,88 +286,58 @@ void ari_handle_websocket(
* \param message The dispatched message.
* \param debug_app Debug flag for the application.
*/
-void ari_websocket_send_event(struct ari_ws_session *ari_ws_session,
+void ari_websocket_send_event(struct ari_ws_session *session,
const char *app_name, struct ast_json *message, int debug_app)
{
- char *remote_addr = ast_sockaddr_stringify(
- ast_websocket_remote_address(ari_ws_session->ast_ws_session));
- const char *msg_type, *msg_application, *msg_timestamp, *msg_ast_id;
+ char *remote_addr = session->ast_ws_session ? ast_sockaddr_stringify(
+ ast_websocket_remote_address(session->ast_ws_session)) : "";
+ const char *msg_type, *msg_application;
SCOPE_ENTER(4, "%s: Dispatching message from Stasis app '%s'\n", remote_addr, app_name);
- ast_assert(ari_ws_session != NULL);
+ ast_assert(session != NULL);
- ao2_lock(ari_ws_session);
+ ao2_lock(session);
msg_type = S_OR(ast_json_string_get(ast_json_object_get(message, "type")), "");
msg_application = S_OR(
- ast_json_string_get(ast_json_object_get(message, "application")), "");
+ ast_json_string_get(ast_json_object_get(message, "application")), app_name);
/* If we've been replaced, remove the application from our local
websocket_apps container */
- if (strcmp(msg_type, "ApplicationReplaced") == 0 &&
+ if (session->type == ARI_WS_TYPE_INBOUND
+ && strcmp(msg_type, "ApplicationReplaced") == 0 &&
strcmp(msg_application, app_name) == 0) {
- ao2_find(ari_ws_session->websocket_apps, msg_application,
- OBJ_UNLINK | OBJ_NODATA);
- }
-
- msg_timestamp = S_OR(
- ast_json_string_get(ast_json_object_get(message, "timestamp")), "");
- if (ast_strlen_zero(msg_timestamp)) {
- if (ast_json_object_set(message, "timestamp", ast_json_timeval(ast_tvnow(), NULL))) {
- ao2_unlock(ari_ws_session);
- SCOPE_EXIT_LOG_RTN(LOG_WARNING,
- "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n",
- remote_addr, msg_type, msg_application);
- }
- }
-
- msg_ast_id = S_OR(
- ast_json_string_get(ast_json_object_get(message, "asterisk_id")), "");
- if (ast_strlen_zero(msg_ast_id)) {
- char eid[20];
-
- if (ast_json_object_set(message, "asterisk_id",
- ast_json_string_create(ast_eid_to_str(eid, sizeof(eid), &ast_eid_default)))) {
- ao2_unlock(ari_ws_session);
- SCOPE_EXIT_LOG_RTN(LOG_WARNING,
- "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n",
- remote_addr, msg_type, msg_application);
- }
+ AST_VECTOR_REMOVE_CMP_ORDERED(&session->websocket_apps,
+ app_name, ast_strings_equal, ast_free_ptr);
}
/* Now, we need to determine our state to see how we will handle the message */
if (ast_json_object_set(message, "application", ast_json_string_create(app_name))) {
- ao2_unlock(ari_ws_session);
+ ao2_unlock(session);
SCOPE_EXIT_LOG_RTN(LOG_WARNING,
"%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n",
remote_addr, msg_type, msg_application);
}
- if (!ari_ws_session) {
- /* If the websocket is NULL, the message goes to the queue */
- if (!AST_VECTOR_APPEND(&ari_ws_session->message_queue, message)) {
- ast_json_ref(message);
- }
- ast_log(LOG_WARNING,
- "%s: Queued '%s' message for Stasis app '%s'; websocket is not ready\n",
- remote_addr,
- msg_type,
- msg_application);
- } else if (stasis_app_event_allowed(app_name, message)) {
-
- if (TRACE_ATLEAST(4) || debug_app) {
- char *str = ast_json_dump_string_format(message, AST_JSON_PRETTY);
+ if (stasis_app_event_allowed(app_name, message)) {
+ session_send_or_queue(session, message, msg_type,
+ app_name, debug_app);
+ }
- ast_verbose("<--- Sending ARI event to %s --->\n%s\n",
- remote_addr,
- str);
- ast_json_free(str);
+ if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL
+ && !ast_strlen_zero(session->channel_id)
+ && ast_strings_equal(msg_type, "StasisEnd")) {
+ struct ast_json *chan = ast_json_object_get(message, "channel");
+ struct ast_json *id_obj = ast_json_object_get(chan, "id");
+ const char *id = ast_json_string_get(id_obj);
+ if (!ast_strlen_zero(id)
+ && ast_strings_equal(id, session->channel_id)) {
+ ast_debug(3, "%s: StasisEnd message sent for channel '%s'\n",
+ remote_addr, id);
+ session->stasis_end_sent = 1;
}
-
- ari_ws_session_write(ari_ws_session, message);
}
-
- ao2_unlock(ari_ws_session);
+ ao2_unlock(session);
SCOPE_EXIT("%s: Dispatched '%s' message from Stasis app '%s'\n",
remote_addr, msg_type, app_name);
}
@@ -331,69 +346,169 @@ static void stasis_app_message_handler(void *data, const char *app_name,
struct ast_json *message)
{
int debug_app = stasis_app_get_debug_by_name(app_name);
- struct ari_ws_session *ari_ws_session = data;
- ast_assert(ari_ws_session != NULL);
- ari_websocket_send_event(ari_ws_session, app_name, message, debug_app);
+ struct ari_ws_session *session = data;
+
+ if (!session) {
+ ast_debug(3, "Stasis app '%s' message handler called with NULL session. OK for per_call_config websocket.\n",
+ app_name);
+ return;
+ }
+
+ ari_websocket_send_event(session, app_name, message, debug_app);
}
-static int parse_app_args(struct ast_variable *get_params,
- struct ast_ari_response * response,
- struct ast_ari_events_event_websocket_args *args)
+static void session_unref(struct ari_ws_session *session)
{
- struct ast_variable *i;
- RAII_VAR(char *, app_parse, NULL, ast_free);
+ if (!session) {
+ return;
+ }
+ ast_debug(4, "%s: Unreffing ARI websocket session\n", session->session_id);
+ ao2_ref(session, -1);
+}
- for (i = get_params; i; i = i->next) {
- if (strcmp(i->name, "app") == 0) {
- /* Parse comma separated list */
- char *vals[MAX_VALS];
- size_t j;
+static void session_unregister_app_cb(char *app_name, struct ari_ws_session *session)
+{
+ ast_debug(3, "%s: Trying to unregister app '%s'\n",
+ session->session_id, app_name);
+ if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
+ char context_name[AST_MAX_CONTEXT + 1];
+ sprintf(context_name, "%s%s", STASIS_CONTEXT_PREFIX, app_name);
+ ast_debug(3, "%s: Unregistering context '%s' for app '%s'\n",
+ session->session_id, context_name, app_name);
+ ast_context_destroy_by_name(context_name, ARI_CONTEXT_REGISTRAR);
+ } else {
+ ast_debug(3, "%s: Unregistering stasis app '%s' and unsubscribing from all events.\n",
+ session->session_id, app_name);
+ stasis_app_unregister(app_name);
+ }
- app_parse = ast_strdup(i->value);
- if (!app_parse) {
- ast_ari_response_alloc_failed(response);
- return -1;
- }
+ /*
+ * We don't send ApplicationUnregistered events for outbound per-call
+ * configs because there's no websocket to send them via or to
+ * inbound websockets because the websocket is probably closed already.
+ */
+ if (!(session->type
+ & (ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG | ARI_WS_TYPE_INBOUND))) {
+ session_send_app_event(session, "ApplicationUnregistered", app_name);
+ }
+}
+
+static void session_unregister_apps(struct ari_ws_session *session)
+{
+ int app_count = (int)AST_VECTOR_SIZE(&session->websocket_apps);
+
+ if (app_count == 0) {
+ return;
+ }
+ ast_debug(3, "%s: Unregistering stasis apps.\n", session->session_id);
+
+ AST_VECTOR_CALLBACK_VOID(&session->websocket_apps, session_unregister_app_cb,
+ session);
+ AST_VECTOR_RESET(&session->websocket_apps, ast_free_ptr);
+
+ return;
+}
+
+static int session_register_apps(struct ari_ws_session *session,
+ const char *_apps, int subscribe_all)
+{
+ char *apps = ast_strdupa(_apps);
+ char *app_name;
+ int app_counter = 0;
+
+ ast_debug(3, "%s: Registering apps '%s'. Subscribe all: %s\n",
+ session->session_id, apps, subscribe_all ? "yes" : "no");
+
+ while ((app_name = ast_strsep(&apps, ',', AST_STRSEP_STRIP))) {
+
+ if (ast_strlen_zero(app_name)) {
+ ast_log(LOG_WARNING, "%s: Invalid application name\n", session->session_id);
+ return -1;
+ }
- if (strlen(app_parse) == 0) {
- /* ast_app_separate_args can't handle "" */
- args->app_count = 1;
- vals[0] = app_parse;
+ if (strlen(app_name) > ARI_MAX_APP_NAME_LEN) {
+ ast_log(LOG_WARNING, "%s: Websocket app '%s' > %d characters\n",
+ session->session_id, app_name, (int)ARI_MAX_APP_NAME_LEN);
+ return -1;
+ }
+
+ if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
+ /*
+ * Outbound per-call configs only create a dialplan context.
+ * If they registered stasis apps there'd be no way for the
+ * Stasis dialplan app to know that it needs to start a
+ * per-call websocket connection.
+ */
+ char context_name[AST_MAX_CONTEXT + 1];
+
+ sprintf(context_name, "%s%s", STASIS_CONTEXT_PREFIX, app_name);
+ if (!ast_context_find(context_name)) {
+ if (!ast_context_find_or_create(NULL, NULL, context_name,
+ ARI_CONTEXT_REGISTRAR)) {
+ ast_log(LOG_WARNING, "%s: Could not create context '%s'\n",
+ session->session_id, context_name);
+ return -1;
+ } else {
+ ast_add_extension(context_name, 0, "_.", 1, NULL, NULL,
+ "Stasis", ast_strdup(app_name), ast_free_ptr,
+ ARI_CONTEXT_REGISTRAR);
+ ast_add_extension(context_name, 0, "h", 1, NULL, NULL,
+ "NoOp", NULL, NULL, ARI_CONTEXT_REGISTRAR);
+ }
} else {
- args->app_count = ast_app_separate_args(
- app_parse, ',', vals,
- ARRAY_LEN(vals));
+ ast_debug(3, "%s: Context '%s' already exists\n", session->session_id,
+ context_name);
}
+ } else {
+ int already_registered = stasis_app_is_registered(app_name);
+ int res = 0;
- if (args->app_count == 0) {
- ast_ari_response_alloc_failed(response);
- return -1;
+ if (subscribe_all) {
+ res = stasis_app_register_all(app_name, stasis_app_message_handler,
+ session);
+ } else {
+ res = stasis_app_register(app_name, stasis_app_message_handler,
+ session);
}
- if (args->app_count >= MAX_VALS) {
- ast_ari_response_error(response, 400,
- "Bad Request",
- "Too many values for app");
+ if (res != 0) {
return -1;
}
- args->app = ast_malloc(sizeof(*args->app) * args->app_count);
- if (!args->app) {
- ast_ari_response_alloc_failed(response);
- return -1;
+ /*
+ * If there was an existing app by the same name, the register handler
+ * will have sent an ApplicationReplaced event. If it's a new app, we
+ * send an ApplicationRegistered event.
+ *
+ * Except... There's no websocket to send it on for outbound per-call
+ * configs and inbound websockets don't need them because they aready
+ * know what apps they've registered for.
+ */
+ if (!already_registered
+ && !(session->type & (ARI_WS_TYPE_INBOUND | ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG))) {
+ session_send_app_event(session, "ApplicationRegistered",
+ app_name);
}
+ }
- for (j = 0; j < args->app_count; ++j) {
- args->app[j] = (vals[j]);
+ if (AST_VECTOR_ADD_SORTED(&session->websocket_apps, ast_strdup(app_name), strcmp)) {
+ ast_log(LOG_WARNING, "%s: Unable to add app '%s' to apps container\n",
+ session->session_id, app_name);
+ return -1;
+ }
+
+ app_counter++;
+ if (app_counter == 1) {
+ ast_free(session->app_name);
+ session->app_name = ast_strdup(app_name);
+ if (!session->app_name) {
+ ast_log(LOG_WARNING, "%s: Unable to duplicate app name\n",
+ session->session_id);
+ return -1;
}
- } else if (strcmp(i->name, "subscribeAll") == 0) {
- args->subscribe_all = ast_true(i->value);
}
}
- args->app_parse = app_parse;
- app_parse = NULL;
-
return 0;
}
@@ -410,32 +525,25 @@ static int parse_app_args(struct ast_variable *get_params,
* \internal
* \brief Reset the ari_ws_session without destroying it.
* It can't be reused and will be cleaned up by the caller.
+ * This should only be called by session_create()
+ * and session_cleanup().
*/
-static void ari_ws_session_reset(struct ari_ws_session *ari_ws_session)
+static void session_reset(struct ari_ws_session *session)
{
- struct ao2_iterator i;
- char *app;
- int j;
- SCOPED_AO2LOCK(lock, ari_ws_session);
+ SCOPED_AO2LOCK(lock, session);
+
+ ast_debug(3, "%s: Resetting ARI websocket session\n",
+ session->session_id);
/* Clean up the websocket_apps container */
- if (ari_ws_session->websocket_apps) {
- i = ao2_iterator_init(ari_ws_session->websocket_apps, 0);
- while ((app = ao2_iterator_next(&i))) {
- stasis_app_unregister(app);
- ao2_cleanup(app);
- }
- ao2_iterator_destroy(&i);
- ao2_cleanup(ari_ws_session->websocket_apps);
- ari_ws_session->websocket_apps = NULL;
+ if (AST_VECTOR_SIZE(&session->websocket_apps) > 0) {
+ session_unregister_apps(session);
}
+ AST_VECTOR_RESET(&session->websocket_apps, ast_free_ptr);
+ AST_VECTOR_FREE(&session->websocket_apps);
- /* Clean up the message_queue container */
- for (j = 0; j < AST_VECTOR_SIZE(&ari_ws_session->message_queue); j++) {
- struct ast_json *msg = AST_VECTOR_GET(&ari_ws_session->message_queue, j);
- ast_json_unref(msg);
- }
- AST_VECTOR_FREE(&ari_ws_session->message_queue);
+ AST_VECTOR_RESET(&session->message_queue, ast_json_unref);
+ AST_VECTOR_FREE(&session->message_queue);
}
/*!
@@ -444,208 +552,255 @@ static void ari_ws_session_reset(struct ari_ws_session *ari_ws_session)
* This unlinks the ari_ws_session from the registry and cleans up the
* decrements the reference count.
*/
-static void ari_ws_session_cleanup(struct ari_ws_session *ari_ws_session)
+static void session_cleanup(struct ari_ws_session *session)
{
- if (!ari_ws_session) {
+ if (!session) {
return;
}
+ ast_debug(3, "%s: Cleaning up ARI websocket session RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+
+ session_reset(session);
+
+ if (session_registry) {
+ ast_debug(3, "%s: Unlinking websocket session from registry RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+ ao2_unlink(session_registry, session);
+ }
- ari_ws_session_reset(ari_ws_session);
- if (ari_ws_session_registry) {
- ao2_unlink(ari_ws_session_registry, ari_ws_session);
+ /*
+ * If this is a per-call config then its only reference
+ * was held by the registry container so we don't need
+ * to unref it here.
+ */
+ if (session->type != ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
+ session_unref(session);
}
- ao2_ref(ari_ws_session, -1);
}
/*!
* \internal
* \brief The ao2 destructor.
- * This cleans up the reference to the parent ast_websocket.
+ * This cleans up the reference to the parent ast_websocket and the
+ * outbound connection websocket if any.
*/
-static void ari_ws_session_dtor(void *obj)
+static void session_dtor(void *obj)
{
- struct ari_ws_session *ari_ws_session = obj;
-
- ast_free(ari_ws_session->app_name);
- if (!ari_ws_session->ast_ws_session) {
+ struct ari_ws_session *session = obj;
+
+ ast_debug(3, "%s: Destroying ARI websocket session\n",
+ session->session_id);
+
+ ast_free(session->app_name);
+ ast_free(session->remote_addr);
+ ast_free(session->channel_id);
+ ast_free(session->channel_name);
+ ao2_cleanup(session->owc);
+ session->owc = NULL;
+ if (!session->ast_ws_session) {
return;
}
- ast_websocket_unref(ari_ws_session->ast_ws_session);
- ari_ws_session->ast_ws_session = NULL;
+ ast_websocket_unref(session->ast_ws_session);
+ session->ast_ws_session = NULL;
}
-static int ari_ws_session_create(
- int (*validator)(struct ast_json *),
+#define handle_create_error(ser, code, msg, reason) \
+({ \
+ if (ser) { \
+ ast_http_error(ser, code, msg, reason); \
+ } \
+ ast_log(LOG_WARNING, "Failed to create ARI websocket session: %d %s %s\n", \
+ code, msg, reason); \
+})
+
+static struct ari_ws_session *session_create(
struct ast_tcptls_session_instance *ser,
- struct ast_ari_events_event_websocket_args *args,
- const char *session_id)
+ const char *apps,
+ int subscribe_all,
+ const char *session_id,
+ struct ari_conf_outbound_websocket *ows,
+ enum ari_websocket_type ws_type)
{
- RAII_VAR(struct ari_ws_session *, ari_ws_session, NULL, ao2_cleanup);
- int (* register_handler)(const char *, stasis_app_cb handler, void *data);
- size_t size, i;
+ RAII_VAR(struct ari_ws_session *, session, NULL, ao2_cleanup);
+ size_t size;
- if (validator == NULL) {
- validator = null_validator;
- }
+ ast_debug(3, "%s: Creating ARI websocket session for apps '%s'\n",
+ session_id, apps);
- size = sizeof(*ari_ws_session) + strlen(session_id) + 1;
+ size = sizeof(*session) + strlen(session_id) + 1;
- ari_ws_session = ao2_alloc(size, ari_ws_session_dtor);
- if (!ari_ws_session) {
- return -1;
+ session = ao2_alloc(size, session_dtor);
+ if (!session) {
+ return NULL;
}
- ari_ws_session->app_name = ast_strdup(args->app_parse);
- if (!ari_ws_session->app_name) {
- ast_http_error(ser, 500, "Internal Server Error",
- "Allocation failed");
- return -1;
- }
+ session->type = ws_type;
+ session->subscribe_all = subscribe_all;
- strcpy(ari_ws_session->session_id, session_id); /* Safe */
+ strcpy(session->session_id, session_id); /* Safe */
/* Instantiate the hash table for Stasis apps */
- ari_ws_session->websocket_apps =
- ast_str_container_alloc(APPS_NUM_BUCKETS);
- if (!ari_ws_session->websocket_apps) {
- ast_http_error(ser, 500, "Internal Server Error",
+ if (AST_VECTOR_INIT(&session->websocket_apps, APPS_INIT_SIZE)) {
+ handle_create_error(ser, 500, "Internal Server Error",
"Allocation failed");
- return -1;
+ return NULL;
}
/* Instantiate the message queue */
- if (AST_VECTOR_INIT(&ari_ws_session->message_queue, MESSAGES_INIT_SIZE)) {
- ast_http_error(ser, 500, "Internal Server Error",
+ if (AST_VECTOR_INIT(&session->message_queue, MESSAGES_INIT_SIZE)) {
+ handle_create_error(ser, 500, "Internal Server Error",
"Allocation failed");
- ao2_cleanup(ari_ws_session->websocket_apps);
- return -1;
+ AST_VECTOR_FREE(&session->websocket_apps);
+ return NULL;
}
- /* Register the apps with Stasis */
- if (args->subscribe_all) {
- register_handler = &stasis_app_register_all;
- } else {
- register_handler = &stasis_app_register;
+ session->validator = ari_validate_message_fn;
+
+ if (ows) {
+ session->owc = ao2_bump(ows);
}
- for (i = 0; i < args->app_count; ++i) {
- const char *app = args->app[i];
+ if (session_register_apps(session, apps, subscribe_all) < 0) {
+ handle_create_error(ser, 500, "Internal Server Error",
+ "Stasis app registration failed");
+ session_reset(session);
+ return NULL;
+ }
- if (ast_strlen_zero(app)) {
- ast_http_error(ser, 400, "Bad Request",
- "Invalid application provided in param [app].");
- ari_ws_session_reset(ari_ws_session);
- return -1;
- }
+ if (!ao2_link(session_registry, session)) {
+ handle_create_error(ser, 500, "Internal Server Error",
+ "Allocation failed");
+ session_reset(session);
+ return NULL;
+ }
- if (ast_str_container_add(ari_ws_session->websocket_apps, app)) {
- ast_http_error(ser, 500, "Internal Server Error",
- "Allocation failed");
- ari_ws_session_reset(ari_ws_session);
- return -1;
- }
+ return ao2_bump(session);
+}
- if (register_handler(app, stasis_app_message_handler, ari_ws_session)) {
- ast_log(LOG_WARNING, "Stasis registration failed for application: '%s'\n", app);
- ast_http_error(ser, 500, "Internal Server Error",
- "Stasis registration failed");
- ari_ws_session_reset(ari_ws_session);
- return -1;
- }
+/*!
+ * \internal
+ * \brief Updates the websocket session.
+ *
+ * \details If the value of the \c ws_session is not \c NULL and there are messages in the
+ * event session's \c message_queue, the messages are dispatched and removed from
+ * the queue.
+ *
+ * \param ari_ws_session The ARI websocket session
+ * \param ast_ws_session The Asterisk websocket session
+ */
+static int session_update(struct ari_ws_session *ari_ws_session,
+ struct ast_websocket *ast_ws_session, int send_registered_events)
+{
+ RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
+ int i;
+
+ if (ast_ws_session == NULL) {
+ return -1;
}
- ari_ws_session->validator = validator;
+ if (!general) {
+ return -1;
+ }
- /*
- * Add the event session to the session registry.
- * When this functions returns, the registry will have
- * the only reference to the session.
- */
- if (!ao2_link(ari_ws_session_registry, ari_ws_session)) {
- ast_http_error(ser, 500, "Internal Server Error",
- "Allocation failed");
- ari_ws_session_reset(ari_ws_session);
+ ari_ws_session->remote_addr = ast_strdup(ast_sockaddr_stringify(
+ ast_websocket_remote_address(ast_ws_session)));
+ if (!ari_ws_session->remote_addr) {
+ ast_log(LOG_ERROR, "Failed to copy remote address\n");
return -1;
}
+ if (ast_websocket_set_nonblock(ast_ws_session) != 0) {
+ ast_log(LOG_ERROR,
+ "ARI web socket failed to set nonblock; closing: %s\n",
+ strerror(errno));
+ return -1;
+ }
+
+ if (ast_websocket_set_timeout(ast_ws_session, general->write_timeout)) {
+ ast_log(LOG_WARNING, "Failed to set write timeout %d on ARI web socket\n",
+ general->write_timeout);
+ }
+
+ ao2_ref(ast_ws_session, +1);
+ ari_ws_session->ast_ws_session = ast_ws_session;
+ ao2_lock(ari_ws_session);
+ for (i = 0; i < AST_VECTOR_SIZE(&ari_ws_session->message_queue); i++) {
+ struct ast_json *msg = AST_VECTOR_GET(&ari_ws_session->message_queue, i);
+ session_write(ari_ws_session, msg);
+ ast_json_unref(msg);
+ }
+
+ AST_VECTOR_RESET(&ari_ws_session->message_queue, AST_VECTOR_ELEM_CLEANUP_NOOP);
+ ao2_unlock(ari_ws_session);
+
+ if (send_registered_events) {
+ int i;
+ char *app;
+
+ for (i = 0; i < AST_VECTOR_SIZE(&ari_ws_session->websocket_apps); i++) {
+ app = AST_VECTOR_GET(&ari_ws_session->websocket_apps, i);
+ session_send_app_event(ari_ws_session,
+ "ApplicationRegistered", app);
+ }
+ }
+
return 0;
}
/*!
* \internal
- * \brief This function gets called before the upgrade process is completed.
- * HTTP is still in effect.
+ * \brief This function gets called for incoming websocket connections
+ * before the upgrade process is completed.
+ *
+ * The point is to be able to report early errors via HTTP rather
+ * than letting res_http_websocket create an ast_websocket session
+ * then immediately close it if there's an error.
*/
static int websocket_attempted_cb(struct ast_tcptls_session_instance *ser,
struct ast_variable *get_params, struct ast_variable *headers,
const char *session_id)
{
- struct ast_ari_events_event_websocket_args args = {};
- int res = 0;
- RAII_VAR(struct ast_ari_response *, response, NULL, ast_free);
- char *remote_addr = ast_sockaddr_stringify(&ser->remote_address);
+ const char *subscribe_all = NULL;
+ const char *apps = NULL;
+ struct ari_ws_session *session = NULL;
- response = ast_calloc(1, sizeof(*response));
- if (!response) {
- ast_log(LOG_ERROR, "Failed to create response.\n");
- ast_http_error(ser, 500, "Server Error", "Memory allocation error");
+ apps = ast_variable_find_in_list(get_params, "app");
+ if (ast_strlen_zero(apps)) {
+ handle_create_error(ser, 400, "Bad Request",
+ "HTTP request is missing param: [app]");
return -1;
}
- res = parse_app_args(get_params, response, &args);
- if (res != 0) {
- /* Param parsing failure */
- RAII_VAR(char *, msg, NULL, ast_json_free);
- if (response->message) {
- msg = ast_json_dump_string(response->message);
- } else {
- ast_log(LOG_ERROR, "Missing response message\n");
- }
-
- if (msg) {
- ast_http_error(ser, response->response_code, response->response_text, msg);
- return -1;
- }
- }
+ subscribe_all = ast_variable_find_in_list(get_params, "subscribeAll");
- if (args.app_count == 0) {
- ast_http_error(ser, 400, "Bad Request",
- "HTTP request is missing param: [app]");
+ session = session_create(ser, apps, ast_true(subscribe_all),
+ session_id, NULL, ARI_WS_TYPE_INBOUND);
+ if (!session) {
+ handle_create_error(ser, 500, "Server Error",
+ "Failed to create ARI websocket session");
return -1;
}
+ /* It's in the session registry now so we can release our reference */
+ session_unref(session);
-#if defined(AST_DEVMODE)
- res = ari_ws_session_create(ast_ari_validate_message_fn(),
- ser, &args, session_id);
-#else
- res = ari_ws_session_create(NULL, ser, &args, session_id);
-#endif
- if (res != 0) {
- ast_log(LOG_ERROR,
- "%s: Failed to create ARI ari_session\n", remote_addr);
- }
-
- ast_free(args.app_parse);
- ast_free(args.app);
- return res;
+ return 0;
}
/*!
* \internal
- * \brief This function gets called after the upgrade process is completed.
- * The websocket is now in effect.
+ * \brief This function gets called for incoming websocket connections
+ * after the upgrade process is completed.
*/
static void websocket_established_cb(struct ast_websocket *ast_ws_session,
struct ast_variable *get_params, struct ast_variable *upgrade_headers)
{
- RAII_VAR(struct ast_ari_response *, response, NULL, ast_free);
/*
* ast_ws_session is passed in with it's refcount bumped so
* we need to unref it when we're done. The refcount will
* be bumped again when we add it to the ari_ws_session.
*/
RAII_VAR(struct ast_websocket *, s, ast_ws_session, ast_websocket_unref);
- RAII_VAR(struct ari_ws_session *, ari_ws_session, NULL, ari_ws_session_cleanup);
+ RAII_VAR(struct ari_ws_session *, ari_ws_session, NULL, session_cleanup);
struct ast_json *msg;
struct ast_variable *v;
char *remote_addr = ast_sockaddr_stringify(
@@ -661,81 +816,819 @@ static void websocket_established_cb(struct ast_websocket *ast_ws_session,
}
}
- response = ast_calloc(1, sizeof(*response));
- if (!response) {
- SCOPE_EXIT_LOG_RTN(LOG_ERROR,
- "%s: Failed to create response\n", remote_addr);
- }
-
- /* Find the event_session and update its websocket */
- ari_ws_session = ao2_find(ari_ws_session_registry, session_id, OBJ_SEARCH_KEY);
- if (ari_ws_session) {
- ao2_unlink(ari_ws_session_registry, ari_ws_session);
- ari_ws_session_update(ari_ws_session, ast_ws_session);
- } else {
+ /*
+ * Find the ari_ws_session that was created by websocket_attempted_cb
+ * and update its ast_websocket.
+ */
+ ari_ws_session = ao2_find(session_registry, session_id, OBJ_SEARCH_KEY);
+ if (!ari_ws_session) {
SCOPE_EXIT_LOG_RTN(LOG_ERROR,
- "%s: Failed to locate an event session for the websocket session\n",
- remote_addr);
+ "%s: Failed to locate an event session for the websocket session %s\n",
+ remote_addr, session_id);
}
+ /*
+ * Since this is a new inbound websocket session,
+ * session_register_apps() will have already sent "ApplicationRegistered"
+ * events for the apps. We don't want to do it again.
+ */
+ session_update(ari_ws_session, ast_ws_session, 0);
+
+ ari_ws_session->connected = 1;
ast_trace(-1, "%s: Waiting for messages\n", remote_addr);
- while ((msg = ari_ws_session_read(ari_ws_session))) {
+ while ((msg = session_read(ari_ws_session))) {
ari_websocket_process_request(ari_ws_session, remote_addr,
upgrade_headers, ari_ws_session->app_name, msg);
ast_json_unref(msg);
}
+ ari_ws_session->connected = 0;
SCOPE_EXIT("%s: Websocket closed\n", remote_addr);
}
-static int ari_ws_session_shutdown_cb(void *ari_ws_session, void *arg, int flags)
+static int session_shutdown_cb(void *obj, void *arg, int flags)
{
- ari_ws_session_cleanup(ari_ws_session);
+ struct ari_ws_session *session = obj;
+
+ /* Per-call configs have no actual websocket */
+ if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
+ ast_log(LOG_NOTICE, "%s: Shutting down %s ARI websocket session\n",
+ session->session_id,
+ ari_websocket_type_to_str(session->type));
+ session_cleanup(session);
+ return 0;
+ }
+ if (session->type == ARI_WS_TYPE_INBOUND) {
+ ast_log(LOG_NOTICE, "%s: Shutting down inbound ARI websocket session from %s\n",
+ session->session_id, session->remote_addr);
+ } else {
+ ast_log(LOG_NOTICE, "%s: Shutting down %s ARI websocket session to %s\n",
+ session->session_id,
+ ari_websocket_type_to_str(session->type),
+ session->remote_addr);
+ }
+
+ /*
+ * We need to ensure the session is kept around after the cleanup
+ * so we can close the websocket.
+ */
+ ao2_bump(session);
+ session->closing = 1;
+ session_cleanup(session);
+ if (session->ast_ws_session) {
+ ast_websocket_close(session->ast_ws_session, 1000);
+ }
return 0;
}
-static void ari_ws_session_registry_dtor(void)
+
+struct ari_ws_session * ari_websocket_get_session(const char *session_id)
+{
+ return ao2_find(session_registry, session_id, OBJ_SEARCH_KEY);
+}
+
+static struct ari_ws_session *session_find_by_app(const char *app_name,
+ unsigned int ws_type)
+{
+ struct ari_ws_session *session = NULL;
+ struct ao2_iterator i;
+
+ if (ast_strlen_zero(app_name)) {
+ return NULL;
+ }
+
+ i = ao2_iterator_init(session_registry, 0);
+ while ((session = ao2_iterator_next(&i))) {
+ char *app = NULL;
+ if (!(session->type & ws_type)) {
+ session_unref(session);
+ continue;
+ }
+
+ app = AST_VECTOR_GET_CMP(&session->websocket_apps,
+ app_name, ast_strings_equal);
+ if (app) {
+ break;
+ }
+ session_unref(session);
+ }
+ ao2_iterator_destroy(&i);
+ return session;
+}
+
+/*!
+ * \internal
+ * \brief Connect to an external ARI application server with retries.
+ *
+ * Sets up the ast_websocket and tls config (if needed) and
+ * tries to open a connection to the remote server.
+ *
+ */
+static struct ast_websocket *outbound_session_connect(
+ struct ari_ws_session *session,
+ enum ast_websocket_result *result)
+{
+ struct ari_conf_outbound_websocket *owc = session->owc;
+ int reconnect_counter = owc->reconnect_attempts;
+
+ while (1) {
+ struct ast_websocket *astws = NULL;
+ struct ast_websocket_client_options options = {
+ .uri = owc->uri,
+ .protocols = owc->protocols,
+ .username = owc->username,
+ .password = owc->password,
+ .timeout = owc->connect_timeout,
+ .suppress_connection_msgs = 1,
+ .tls_cfg = NULL,
+ };
+ SCOPED_AO2LOCK(lock, session);
+
+ if (owc->tls_enabled) {
+ /*
+ * tls_cfg and its contents are freed automatically
+ * by res_http_websocket when the connection ends.
+ * We create it even if tls is not enabled to we can
+ * suppress connection error messages and print our own.
+ */
+ options.tls_cfg = ast_calloc(1, sizeof(*options.tls_cfg));
+ if (!options.tls_cfg) {
+ return NULL;
+ }
+ /* TLS options */
+ options.tls_cfg->enabled = owc->tls_enabled;
+ options.tls_cfg->cafile = ast_strdup(owc->ca_list_file);
+ options.tls_cfg->capath = ast_strdup(owc->ca_list_path);
+ options.tls_cfg->certfile = ast_strdup(owc->cert_file);
+ options.tls_cfg->pvtfile = ast_strdup(owc->priv_key_file);
+ ast_set2_flag(&options.tls_cfg->flags, !owc->verify_server_cert, AST_SSL_DONT_VERIFY_SERVER);
+ ast_set2_flag(&options.tls_cfg->flags, !owc->verify_server_hostname, AST_SSL_IGNORE_COMMON_NAME);
+ }
+
+ astws = ast_websocket_client_create_with_options(&options, result);
+ if (astws && *result == WS_OK) {
+ return astws;
+ }
+
+ reconnect_counter--;
+ if (reconnect_counter <= 0) {
+ ast_log(LOG_WARNING,
+ "%s: Failed after %d tries: %s%s%s%s\n",
+ session->session_id,
+ owc->reconnect_attempts,
+ ast_websocket_result_to_str(*result),
+ errno ? " (" : "",
+ errno ? strerror(errno) : "",
+ errno ? ")" : ""
+ );
+ break;
+ }
+ usleep(owc->reconnect_interval * 1000);
+ }
+
+ return NULL;
+}
+
+/*!
+ * \internal
+ * \brief Connection and request handler thread for outbound websockets.
+ *
+ * This thread handles the connection and reconnection logic for outbound
+ * websockets. Once connected, it waits for incoming REST over Websocket
+ * requests and dispatches them to ari_websocket_process_request()).
+ */
+static void *outbound_session_handler_thread(void *obj)
+{
+ RAII_VAR(struct ari_ws_session *, session, obj, session_cleanup);
+ int already_sent_registers = 1;
+
+ ast_debug(3, "%s: Starting outbound websocket thread RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+ session->thread = pthread_self();
+ session->connected = 0;
+
+ while(1) {
+ RAII_VAR(struct ast_websocket *, astws, NULL, ast_websocket_unref);
+ RAII_VAR(struct ast_variable *, upgrade_headers, NULL, ast_variables_destroy);
+ enum ast_websocket_result result;
+ struct ast_json *msg;
+
+ ast_debug(3, "%s: Attempting to connect to %s\n", session->session_id,
+ session->owc->uri);
+
+ astws = outbound_session_connect(session, &result);
+ if (!astws || result != WS_OK) {
+ if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL) {
+ struct stasis_app_control *control =
+ stasis_app_control_find_by_channel_id(session->channel_id);
+ if (control) {
+ ast_debug(3, "%s: Connection failed. Returning to dialplan.\n",
+ session->session_id);
+ stasis_app_control_mark_failed(control);
+ stasis_app_control_continue(control, NULL, NULL, -1);
+ ao2_cleanup(control);
+ } else {
+ ast_debug(3, "%s: Connection failed. No control object found.\n",
+ session->session_id);
+ }
+
+ break;
+ }
+ usleep(session->owc->reconnect_interval * 1000);
+ continue;
+ }
+
+ /*
+ * We only want to send "ApplicationRegistered" events in the
+ * case of a reconnect. The initial connection will have already sent
+ * the events when outbound_register_apps() was called.
+ */
+ session_update(session, astws, !already_sent_registers);
+ already_sent_registers = 0;
+
+ /*
+ * This is the Authorization header that would normally be taken
+ * from the incoming HTTP request that is being upgraded to a websocket.
+ * Since this is an outbound websocket, we have to create it ourselves.
+ *
+ * This is NOT the same as the Authorization header that is used for
+ * authentication with the remote websocket server.
+ */
+ upgrade_headers = ast_http_create_basic_auth_header(
+ session->owc->local_ari_user, session->owc->local_ari_password);
+ if (!upgrade_headers) {
+ ast_log(LOG_WARNING, "%s: Failed to create upgrade header\n", session->session_id);
+ session->thread = 0;
+ ast_websocket_close(astws, 1000);
+ return NULL;
+ }
+
+ session->connected = 1;
+ ast_debug(3, "%s: Websocket connected\n", session->session_id);
+ ast_debug(3, "%s: Waiting for messages RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+
+ /*
+ * The websocket is connected. Now we need to wait for messages
+ * from the server.
+ */
+ while ((msg = session_read(session))) {
+ ari_websocket_process_request(session, session->remote_addr,
+ upgrade_headers, session->app_name, msg);
+ ast_json_unref(msg);
+ }
+
+ session->connected = 0;
+ ast_websocket_unref(session->ast_ws_session);
+ session->ast_ws_session = NULL;
+ if (session->closing) {
+ ast_debug(3, "%s: Websocket closing RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+ break;
+ }
+
+ ast_log(LOG_WARNING, "%s: Websocket disconnected. Reconnecting\n",
+ session->session_id);
+ }
+
+ ast_debug(3, "%s: Stopping outbound websocket thread RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+ session->thread = 0;
+
+ return NULL;
+}
+
+enum session_apply_result {
+ SESSION_APPLY_NO_CHANGE,
+ SESSION_APPLY_OK,
+ SESSION_APPLY_RECONNECT_REQUIRED,
+ SESSION_APPLY_FAILED,
+};
+
+static enum session_apply_result outbound_session_apply_config(
+ struct ari_ws_session *session,
+ struct ari_conf_outbound_websocket *new_owc)
+{
+ enum session_apply_result apply_result;
+ enum ari_conf_owc_fields what_changed;
+ const char *new_owc_id = ast_sorcery_object_get_id(new_owc);
+
+ what_changed = ari_conf_owc_detect_changes(session->owc, new_owc);
+
+ if (what_changed == ARI_OWC_FIELD_NONE) {
+ ast_debug(2, "%s: No changes detected\n", new_owc_id);
+ return SESSION_APPLY_NO_CHANGE;
+ }
+ ast_debug(2, "%s: Config change detected. Checking details\n", new_owc_id);
+
+ if (what_changed & ARI_OWC_NEEDS_REREGISTER) {
+ ast_debug(2, "%s: Re-registering apps\n", new_owc_id);
+
+ if (!(what_changed & ARI_OWC_FIELD_SUBSCRIBE_ALL)) {
+ /*
+ * If subscribe_all didn't change, we don't have to
+ * unregister apps that are already registered and
+ * also in the new config. We'll remove them from
+ * the session->websocket_apps container so that
+ * session_unregister_apps will only clean up
+ * the ones that are going away. session_register_apps
+ * will add them back in again and cause ApplicationReplaced
+ * messages to be sent.
+ *
+ * If subscribe_all did change, we have no choice but to
+ * unregister all apps and register all the ones in
+ * the new config even if they already existed.
+ */
+ int i = 0;
+ char *app;
+
+ while(i < (int) AST_VECTOR_SIZE(&session->websocket_apps)) {
+ app = AST_VECTOR_GET(&session->websocket_apps, i);
+ if (ast_in_delimited_string(app, new_owc->apps, ',')) {
+ AST_VECTOR_REMOVE_ORDERED(&session->websocket_apps, i);
+ ast_debug(3, "%s: Unlinked app '%s' to keep it from being unregistered\n",
+ new_owc_id, app);
+ ast_free(app);
+ } else {
+ i++;
+ }
+ }
+ }
+
+ session_unregister_apps(session);
+
+ /*
+ * Register the new apps. This will also replace any
+ * existing apps that are in the new config sending
+ * ApplicationRegistered or ApplicationReplaced events
+ * as necessary.
+ */
+ if (session_register_apps(session, new_owc->apps,
+ new_owc->subscribe_all) < 0) {
+ ast_log(LOG_WARNING, "%s: Failed to register apps '%s'\n",
+ new_owc_id, new_owc->apps);
+ /* Roll back. */
+ session_unregister_apps(session);
+ /* Re-register the original apps. */
+ if (session_register_apps(session, session->owc->apps,
+ session->owc->subscribe_all) < 0) {
+ ast_log(LOG_WARNING, "%s: Failed to re-register apps '%s'\n",
+ new_owc_id, session->owc->apps);
+ }
+ return SESSION_APPLY_FAILED;
+ }
+ }
+ /*
+ * We need to update the session with the new config
+ * but it has to be done after re-registering apps and
+ * before we reconnect.
+ */
+ ao2_replace(session->owc, new_owc);
+ session->type = new_owc->connection_type;
+ session->subscribe_all = new_owc->subscribe_all;
+
+ apply_result = SESSION_APPLY_OK;
+
+ if (what_changed & ARI_OWC_NEEDS_RECONNECT) {
+ ast_debug(2, "%s: Reconnect required\n", new_owc_id);
+ apply_result = SESSION_APPLY_RECONNECT_REQUIRED;
+ if (session->ast_ws_session) {
+ ast_debug(2, "%s: Closing websocket\n", new_owc_id);
+ ast_websocket_close(session->ast_ws_session, 1000);
+ }
+ }
+
+ return apply_result;
+}
+
+/*
+ * This is the fail-safe timeout for the per-call websocket
+ * connection. To prevent a cleanup race condition, we wait
+ * 3 times the timeout the thread will use to connect to the
+ * websocket server. This way we're sure the thread will be
+ * done before we do final cleanup. This timeout is only used
+ * if the thread is cancelled somehow and can't indicate
+ * whether it actually connected or not.
+ */
+#define PER_CALL_FAIL_SAFE_TIMEOUT(owc) \
+ (int64_t)((owc->connect_timeout + owc->reconnect_interval) \
+ * (owc->reconnect_attempts + 3))
+
+/*!
+ * \brief This function gets called by app_stasis when a call arrives
+ * but a Stasis application isn't already registered. We check to see
+ * if a per-call config exists for the application and if so, we create a
+ * per-call websocket connection and return a unique app id which app_stasis
+ * can use to call stasis_app_exec() with.
+ */
+char *ast_ari_create_per_call_websocket(const char *app_name,
+ struct ast_channel *chan)
+{
+ RAII_VAR(struct ari_ws_session *, session, NULL, session_unref);
+ RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup);
+ RAII_VAR(char *, session_id, NULL, ast_free);
+ RAII_VAR(char *, app_id, NULL, ast_free);
+ enum ari_conf_owc_fields invalid_fields;
+ const char *owc_id = NULL;
+ char *app_id_rtn = NULL;
+ struct timeval tv_start;
+ int res = 0;
+
+ owc = ari_conf_get_owc_for_app(app_name, ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG);
+ if (!owc) {
+ ast_log(LOG_WARNING, "%s: Failed to find outbound websocket per-call config for app '%s'\n",
+ ast_channel_name(chan), app_name);
+ return NULL;
+ }
+ owc_id = ast_sorcery_object_get_id(owc);
+ invalid_fields = ari_conf_owc_get_invalid_fields(owc_id);
+
+ if (invalid_fields) {
+ ast_log(LOG_WARNING, "%s: Unable to create per-call websocket. Outbound websocket config is invalid\n",
+ owc_id);
+ return NULL;
+ }
+
+ res = ast_asprintf(&session_id, "%s:%s", owc_id, ast_channel_name(chan));
+ if (res < 0) {
+ return NULL;
+ }
+ res = ast_asprintf(&app_id, "%s:%s", app_name, ast_channel_name(chan));
+ if (res < 0) {
+ ast_free(app_id);
+ return NULL;
+ }
+
+ session = session_create(NULL, app_id, owc->subscribe_all,
+ session_id, owc, ARI_WS_TYPE_OUTBOUND_PER_CALL);
+ if (!session) {
+ ast_log(LOG_WARNING, "%s: Failed to create websocket session\n", session_id);
+ return NULL;
+ }
+
+ session->channel_id = ast_strdup(ast_channel_uniqueid(chan));
+ session->channel_name = ast_strdup(ast_channel_name(chan));
+
+ /*
+ * We have to bump the session reference count here because
+ * we need to check that the session is connected before we return.
+ * If it didn't connect, then the thread will have cleaned up the
+ * session while we're in the loop checking for the connection
+ * which will result in a SEGV or FRACK.
+ * RAII will clean up this bump.
+ */
+ ao2_bump(session);
+ ast_debug(2, "%s: Starting thread RC: %d\n", session->session_id,
+ (int)ao2_ref(session, 0));
+
+ if (ast_pthread_create_detached_background(&session->thread, NULL,
+ outbound_session_handler_thread, session)) {
+ session_cleanup(session);
+ ast_log(LOG_WARNING, "%s: Failed to create thread.\n", session->session_id);
+ return NULL;
+ }
+
+ /*
+ * We need to make sure the session connected and is processing
+ * requests before we return but we don't want to block forever
+ * in case the thread never starts or gets cancelled so we have
+ * a fail-safe timeout.
+ */
+ tv_start = ast_tvnow();
+ while (session->thread > 0 && !session->connected) {
+ struct timeval tv_now = ast_tvnow();
+ if (ast_tvdiff_ms(tv_now, tv_start) > PER_CALL_FAIL_SAFE_TIMEOUT(owc)) {
+ break;
+ }
+ /* Sleep for 500ms before checking again. */
+ usleep(500 * 1000);
+ }
+
+ if (session->thread <= 0 || !session->connected) {
+ ast_log(LOG_WARNING, "%s: Failed to create per call websocket thread\n",
+ session_id);
+ return NULL;
+ }
+
+ ast_debug(3, "%s: Created per call websocket for app '%s'\n",
+ session_id, app_id);
+
+ /*
+ * We now need to prevent RAII from freeing the app_id.
+ */
+ app_id_rtn = app_id;
+ app_id = NULL;
+ return app_id_rtn;
+}
+
+#define STASIS_END_MAX_WAIT_MS 5000
+#define STASIS_END_POST_WAIT_US (3000 * 1000)
+
+/*
+ * This thread is used to close the websocket after the StasisEnd
+ * event has been sent and control has been returned to the dialplan.
+ * We wait a few seconds to allow additional events to be sent
+ * like ChannelVarset and ChannelDestroyed.
+ */
+static void *outbound_session_pc_close_thread(void *data)
{
- if (!ari_ws_session_registry) {
+ /*
+ * We're using RAII because we want to show a debug message
+ * after we run ast_websocket_close().
+ */
+ RAII_VAR(struct ari_ws_session *, session, data, session_unref);
+
+ /*
+ * We're going to wait 3 seconds to allow stasis to send additional
+ * events like ChannelVarset and ChannelDestroyed after the StasisEnd.
+ */
+ ast_debug(3, "%s: Waiting for %dms before closing websocket RC: %d\n",
+ session->session_id, (int)(STASIS_END_POST_WAIT_US / 1000),
+ (int)ao2_ref(session, 0));
+ usleep(STASIS_END_POST_WAIT_US);
+ session->closing = 1;
+ if (session->ast_ws_session) {
+ ast_websocket_close(session->ast_ws_session, 1000);
+ }
+ ast_debug(3, "%s: Websocket closed RC: %d\n", session->session_id,
+ (int)ao2_ref(session, 0));
+ return NULL;
+}
+
+/*!
+ * \brief This function is called by the app_stasis dialplan app
+ * to close a per-call websocket after stasis_app_exec() returns.
+ */
+void ast_ari_close_per_call_websocket(char *app_name)
+{
+ struct ari_ws_session *session = NULL;
+ pthread_t thread;
+ struct timeval tv_start;
+
+ session = session_find_by_app(app_name, ARI_WS_TYPE_OUTBOUND_PER_CALL);
+ if (!session) {
+ ast_debug(3, "%s: Per call websocket not found\n", app_name);
+ ast_free(app_name);
return;
}
+ ast_free(app_name);
+
+ /*
+ * When stasis_app_exec() returns, the StasisEnd event for the
+ * channel has been queued but since actually sending it is done
+ * in a separate thread, it probably won't have been sent yet.
+ * We need to wait for it to go out on the wire before we close the
+ * websocket. ari_websocket_send_event will set a flag on the session
+ * when a StasisEnd event is sent for the channel that originally
+ * triggered the connection. We'll wait for that but we don't want
+ * to wait forever so there's a fail-safe timeout in case a thread
+ * got cancelled or we missed the StasisEnd event somehow.
+ */
+ ast_debug(3, "%s: Waiting for StasisEnd event to be sent RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+
+ tv_start = ast_tvnow();
+ while (session->thread > 0 && !session->stasis_end_sent) {
+ struct timeval tv_now = ast_tvnow();
+ int64_t diff = ast_tvdiff_ms(tv_now, tv_start);
+ ast_debug(3, "%s: Waiting for StasisEnd event %lu %d %ld\n",
+ session->session_id, (unsigned long)session->thread,
+ session->stasis_end_sent, diff);
+ if (diff > STASIS_END_MAX_WAIT_MS) {
+ break;
+ }
+ /* Sleep for 500ms before checking again. */
+ usleep(500 * 1000);
+ }
+ ast_debug(3, "%s: StasisEnd event sent. Scheduling websocket close. RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+
+ /*
+ * We can continue to send events like ChannelVarset and ChannelDestroyed
+ * to the websocket after the StasisEnd event but those events won't be
+ * generated until after the Stasis() dialplan app returns. We don't want
+ * to hold up the dialplan while we wait so we'll create a thread that waits
+ * a few seconds more before closing the websocket.
+ *
+ * We transferring ownership of the session to the thread.
+ */
+ if (ast_pthread_create_detached_background(&thread, NULL,
+ outbound_session_pc_close_thread, session)) {
+ ast_log(LOG_WARNING, "%s: Failed to create websocket close thread\n",
+ session->session_id);
+ session_unref(session);
+ }
+ ast_debug(3, "%s: Scheduled websocket close RC: %d\n",
+ session->session_id, (int)ao2_ref(session, 0));
+
+ return;
+}
+
+struct ao2_container* ari_websocket_get_sessions(void)
+{
+ return ao2_bump(session_registry);
+}
+
+static int outbound_session_create(void *obj, void *args, int flags)
+{
+ struct ari_conf_outbound_websocket *owc = obj;
+ const char *owc_id = ast_sorcery_object_get_id(owc);
+ struct ari_ws_session *session = NULL;
+ enum session_apply_result apply_result;
+ enum ari_conf_owc_fields invalid_fields = ari_conf_owc_get_invalid_fields(owc_id);
+
+ ast_log(LOG_NOTICE, "%s: Creating outbound websocket session\n", owc_id);
+
+ session = ari_websocket_get_session(owc_id);
+ if (session) {
+ ast_debug(2, "%s: Found existing connection\n", owc_id);
+ if (invalid_fields) {
+ session_unref(session);
+ ast_log(LOG_WARNING,
+ "%s: Unable to update websocket session. Outbound websocket config is invalid\n",
+ owc_id);
+ return 0;
+ }
+
+ ao2_lock(session);
+ apply_result = outbound_session_apply_config(session, owc);
+ ao2_unlock(session);
+ session_unref(session);
+ if (apply_result == SESSION_APPLY_FAILED) {
+ ast_log(LOG_WARNING,
+ "%s: Failed to apply new configuration. Existing connection preserved.\n",
+ owc_id);
+ }
+ return 0;
+ }
+
+ if (invalid_fields) {
+ ast_log(LOG_WARNING,
+ "%s: Unable to create websocket session. Outbound websocket config is invalid\n",
+ owc_id);
+ return 0;
+ }
- ao2_callback(ari_ws_session_registry, OBJ_MULTIPLE | OBJ_NODATA,
- ari_ws_session_shutdown_cb, NULL);
+ session = session_create(NULL, owc->apps, owc->subscribe_all, owc_id,
+ owc, owc->connection_type);
+ if (!session) {
+ ast_log(LOG_WARNING, "%s: Failed to create websocket session\n", owc_id);
+ return 0;
+ }
+
+ if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
+ /* There's no thread to transfer the reference to */
+ session_unref(session);
+ return 0;
+ }
- ao2_cleanup(ari_ws_session_registry);
- ari_ws_session_registry = NULL;
+ ast_debug(2, "%s: Starting thread RC: %d\n", session->session_id,
+ (int)ao2_ref(session, 0));
+ /* We're transferring the session reference to the thread. */
+ if (ast_pthread_create_detached_background(&session->thread, NULL,
+ outbound_session_handler_thread, session)) {
+ session_cleanup(session);
+ ast_log(LOG_WARNING, "%s: Failed to create thread.\n", session->session_id);
+ return 0;
+ }
+ ast_debug(2, "%s: launched thread\n", session->session_id);
+
+ return 0;
+}
+
+static void outbound_sessions_load(const char *name)
+{
+ RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
+ struct ao2_iterator i;
+ struct ari_ws_session *session;
+
+ ast_debug(2, "Reloading ARI websockets\n");
+
+ ao2_callback(owcs, OBJ_NODATA, outbound_session_create, NULL);
+
+ i = ao2_iterator_init(session_registry, 0);
+ while ((session = ao2_iterator_next(&i))) {
+ int cleanup = 1;
+ if (session->owc
+ && (session->type &
+ (ARI_WS_TYPE_OUTBOUND_PERSISTENT | ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG))) {
+ struct ari_conf_outbound_websocket *ows =
+ ari_conf_get_owc(session->session_id);
+ if (!ows) {
+ ast_debug(3, "Cleaning up outbound websocket %s\n",
+ session->session_id);
+ session->closing = 1;
+ session_cleanup(session);
+ if (session->ast_ws_session) {
+ ast_websocket_close(session->ast_ws_session, 1000);
+ }
+
+ if (session->type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) {
+ /*
+ * If persistent, session_cleanup will cleanup
+ * this reference so we don't want to double clean it up.
+ * session_cleanup doesn't cleanup the reference
+ * for per-call configs so we need to do that ourselves.
+ */
+ cleanup = 0;
+ }
+ }
+ ao2_cleanup(ows);
+ }
+ /* We don't want to double cleanup if its been closed. */
+ if (cleanup) {
+ ao2_cleanup(session);
+ }
+ }
+ ao2_iterator_destroy(&i);
+
+ return;
+}
+
+int ari_outbound_websocket_start(struct ari_conf_outbound_websocket *owc)
+{
+ if (owc) {
+ return outbound_session_create(owc, NULL, 0);
+ }
+ return -1;
+}
+
+void ari_websocket_shutdown(struct ari_ws_session *session)
+{
+ if (session) {
+ session_shutdown_cb(session, NULL, 0);
+ }
+}
+
+void ari_websocket_shutdown_all(void)
+{
+ if (session_registry) {
+ ao2_callback(session_registry, OBJ_MULTIPLE | OBJ_NODATA,
+ session_shutdown_cb, NULL);
+ }
+}
+
+static void session_registry_dtor(void)
+{
+ if (session_registry) {
+ ao2_callback(session_registry, OBJ_MULTIPLE | OBJ_NODATA,
+ session_shutdown_cb, NULL);
+ ao2_cleanup(session_registry);
+ session_registry = NULL;
+ }
}
int ari_websocket_unload_module(void)
{
- ari_ws_session_registry_dtor();
+ session_registry_dtor();
ao2_cleanup(ast_ws_server);
ast_ws_server = NULL;
return 0;
}
-AO2_STRING_FIELD_CMP_FN(ari_ws_session, session_id);
-AO2_STRING_FIELD_HASH_FN(ari_ws_session, session_id);
+static struct ast_sorcery_observer observer_callbacks = {
+ .loaded = outbound_sessions_load,
+};
+
+AO2_STRING_FIELD_CMP_FN(ari_ws_session, session_id)
+AO2_STRING_FIELD_SORT_FN(ari_ws_session, session_id)
-int ari_websocket_load_module(void)
+int ari_websocket_load_module(int is_enabled)
{
int res = 0;
struct ast_websocket_protocol *protocol;
- ari_ws_session_registry = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0,
- ARI_WS_SESSION_NUM_BUCKETS, ari_ws_session_hash_fn,
- NULL, ari_ws_session_cmp_fn);
- if (!ari_ws_session_registry) {
+ ast_debug(2, "Initializing ARI websockets. Enabled: %s\n", is_enabled ? "yes" : "no");
+
+ session_registry = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_MUTEX,
+ AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE,
+ ari_ws_session_sort_fn, ari_ws_session_cmp_fn);
+ if (!session_registry) {
ast_log(LOG_WARNING,
"Failed to allocate the local registry for websocket applications\n");
return AST_MODULE_LOAD_DECLINE;
}
+ res = ari_sorcery_observer_add("outbound_websocket", &observer_callbacks);
+ if (res < 0) {
+ ast_log(LOG_WARNING, "Failed to register ARI websocket observer\n");
+ session_registry_dtor();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ /*
+ * The global "enabled" flag only controls whether the REST and
+ * inbound websockets are enabled. The outbound websocket
+ * configs are always enabled.
+ if (!is_enabled) {
+ return AST_MODULE_LOAD_SUCCESS;
+ }
+ */
+
ast_ws_server = ast_websocket_server_create();
if (!ast_ws_server) {
- ari_ws_session_registry_dtor();
+ session_registry_dtor();
return AST_MODULE_LOAD_DECLINE;
}
@@ -743,7 +1636,7 @@ int ari_websocket_load_module(void)
if (!protocol) {
ao2_ref(ast_ws_server, -1);
ast_ws_server = NULL;
- ari_ws_session_registry_dtor();
+ session_registry_dtor();
return AST_MODULE_LOAD_DECLINE;
}
protocol->session_attempted = websocket_attempted_cb;
diff --git a/res/ari/ari_websockets.h b/res/ari/ari_websockets.h
index 4ad180acfd..24b4ba3f1f 100644
--- a/res/ari/ari_websockets.h
+++ b/res/ari/ari_websockets.h
@@ -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-" 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_ */
diff --git a/res/ari/cli.c b/res/ari/cli.c
index f9d9cecfb7..306a597b32 100644
--- a/res/ari/cli.c
+++ b/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,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 \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 \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 \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));
}
diff --git a/res/ari/config.c b/res/ari/config.c
index 501487f519..558cf61cc7 100644
--- a/res/ari/config.c
+++ b/res/ari/config.c
@@ -22,38 +22,65 @@
* \author David M. Lee, II
*/
+#include
+
#include "asterisk.h"
+#include "asterisk/sorcery.h"
#include "asterisk/config_options.h"
#include "asterisk/http_websocket.h"
#include "asterisk/app.h"
#include "asterisk/channel.h"
+#include "asterisk/vector.h"
#include "internal.h"
-/*! \brief Locking container for safe configuration access. */
-static AO2_GLOBAL_OBJ_STATIC(confs);
-
-/*! \brief Mapping of the ARI conf struct's globals to the
- * general context in the config file. */
-static struct aco_type general_option = {
- .type = ACO_GLOBAL,
- .name = "general",
- .item_offset = offsetof(struct ast_ari_conf, general),
- .category = "general",
- .category_match = ACO_WHITELIST_EXACT,
+static struct ast_sorcery *sorcery;
+
+struct outbound_websocket_state {
+ enum ari_conf_owc_fields invalid_fields;
+ char id[0];
};
-static struct aco_type *general_options[] = ACO_TYPES(&general_option);
+#define OWC_STATES_BUCKETS 13
+struct ao2_container *owc_states = NULL;
-/*! \brief Encoding format handler converts from boolean to enum. */
-static int encoding_format_handler(const struct aco_option *opt,
+static void outbound_websocket_dtor(void *obj)
+{
+ struct ari_conf_outbound_websocket *owc = obj;
+
+ ast_debug(3, "%s: Disposing of outbound websocket config\n",
+ ast_sorcery_object_get_id(owc));
+ ast_string_field_free_memory(owc);
+}
+
+static void *outbound_websocket_alloc(const char *id)
+{
+ struct ari_conf_outbound_websocket *owc = NULL;
+
+ owc = ast_sorcery_generic_alloc(sizeof(*owc), outbound_websocket_dtor);
+ if (!owc) {
+ return NULL;
+ }
+
+ if (ast_string_field_init(owc, 1024) != 0) {
+ ao2_cleanup(owc);
+ return NULL;
+ }
+
+ ast_debug(2, "%s: Allocated outbound websocket config\n", id);
+ return owc;
+}
+
+/*! \brief Parses the ast_ari_conf_outbound_ws_type enum from a config file */
+static int outbound_websocket_connection_type_handler(const struct aco_option *opt,
struct ast_variable *var, void *obj)
{
- struct ast_ari_conf_general *general = obj;
+ struct ari_conf_outbound_websocket *ows = obj;
- if (!strcasecmp(var->name, "pretty")) {
- general->format = ast_true(var->value) ?
- AST_JSON_PRETTY : AST_JSON_COMPACT;
+ if (strcasecmp(var->value, "persistent") == 0) {
+ ows->connection_type = ARI_WS_TYPE_OUTBOUND_PERSISTENT;
+ } else if (strcasecmp(var->value, "per_call_config") == 0) {
+ ows->connection_type = ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG;
} else {
return -1;
}
@@ -61,16 +88,14 @@ static int encoding_format_handler(const struct aco_option *opt,
return 0;
}
-/*! \brief Parses the ast_ari_password_format enum from a config file */
-static int password_format_handler(const struct aco_option *opt,
- struct ast_variable *var, void *obj)
+static int outbound_websocket_connection_type_to_str(const void *obj, const intptr_t *args, char **buf)
{
- struct ast_ari_conf_user *user = obj;
+ const struct ari_conf_outbound_websocket *owc = obj;
- if (strcasecmp(var->value, "plain") == 0) {
- user->password_format = ARI_PASSWORD_FORMAT_PLAIN;
- } else if (strcasecmp(var->value, "crypt") == 0) {
- user->password_format = ARI_PASSWORD_FORMAT_CRYPT;
+ if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) {
+ *buf = ast_strdup("persistent");
+ } else if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) {
+ *buf = ast_strdup("per_call_config");
} else {
return -1;
}
@@ -78,183 +103,542 @@ static int password_format_handler(const struct aco_option *opt,
return 0;
}
-/*! \brief Destructor for \ref ast_ari_conf_user */
-static void user_dtor(void *obj)
+/*
+ * Can't use INT_MIN because it's an expression
+ * and macro substitutions using stringify can't
+ * handle that.
+ */
+#define DEFAULT_RECONNECT_ATTEMPTS -2147483648
+
+/*!
+ * \brief Callback to initialize an outbound websocket object
+ * \retval 0 on success
+ * \retval CMP_MATCH on error which will cause the object to be removed
+ */
+static int outbound_websocket_apply(const struct ast_sorcery *sorcery, void *obj)
{
- struct ast_ari_conf_user *user = obj;
- ast_debug(3, "Disposing of user %s\n", user->username);
- ast_free(user->username);
+ struct ari_conf_outbound_websocket *owc = obj;
+ const char *id = ast_sorcery_object_get_id(owc);
+ int res = 0;
+
+ ast_debug(3, "%s: Initializing outbound websocket\n", id);
+
+ if (ast_strlen_zero(owc->uri)) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket missing uri\n", id);
+ res = -1;
+ }
+ if (ast_strlen_zero(owc->protocols)) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket missing protocols\n", id);
+ res = -1;
+ }
+ if (ast_strlen_zero(owc->apps)) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket missing apps\n", id);
+ res = -1;
+ } else {
+ char *apps = ast_strdupa(owc->apps);
+ char *app;
+ while ((app = ast_strsep(&apps, ',', AST_STRSEP_STRIP))) {
+
+ if (ast_strlen_zero(app)) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket has empty app\n", id);
+ res = -1;
+ }
+ if (strlen(app) > ARI_MAX_APP_NAME_LEN) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket app '%s' > %d characters\n",
+ id, app, (int)ARI_MAX_APP_NAME_LEN);
+ res = -1;
+ }
+
+ }
+ }
+
+ if (ast_strlen_zero(owc->local_ari_user)) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket missing local_ari_user\n", id);
+ res = -1;
+ }
+
+ if (res != 0) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket configuration failed\n", id);
+ } else {
+ ast_debug(3, "%s: Outbound websocket configuration succeeded\n", id);
+
+ if (owc->reconnect_attempts == DEFAULT_RECONNECT_ATTEMPTS) {
+ if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) {
+ owc->reconnect_attempts = INT_MAX;
+ } else {
+ owc->reconnect_attempts = 4;
+ }
+ }
+ }
+
+ /* Reminder: If res is -1, the config will be discarded. */
+ return res;
}
-/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */
-static void *user_alloc(const char *cat)
+enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id)
{
- RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
+ RAII_VAR(struct outbound_websocket_state *, state, NULL, ao2_cleanup);
- if (!cat) {
- return NULL;
+ state = ao2_find(owc_states, id, OBJ_SEARCH_KEY);
+ return state ? state->invalid_fields : ARI_OWC_FIELD_NONE;
+}
+
+static int outbound_websocket_validate_cb(void *obj, void *args, int flags)
+{
+ struct ari_conf_outbound_websocket *owc = obj;
+ struct ari_conf_outbound_websocket *other_owc = NULL;
+ RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup);
+ struct ao2_iterator it;
+ const char *id = ast_sorcery_object_get_id(owc);
+ struct ast_vector_string apps = { 0, };
+ struct ari_conf_user *user = NULL;
+ struct outbound_websocket_state *state = NULL;
+ int res = 0;
+
+ ast_debug(2, "%s: Validating outbound websocket\n", id);
+
+ owcs = ari_conf_get_owcs();
+ if (!owcs || ao2_container_count(owcs) == 0) {
+ return 0;
+ }
+
+ if (AST_VECTOR_INIT(&apps, 5) != 0) {
+ return 0;
}
- ast_debug(3, "Allocating user %s\n", cat);
+ res = ast_vector_string_split(&apps, owc->apps, ",", 0, NULL);
+ if (res != 0) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket apps '%s' failed to split\n",
+ id, owc->apps);
+ AST_VECTOR_RESET(&apps, ast_free_ptr);
+ AST_VECTOR_FREE(&apps);
+ return 0;
+ }
- user = ao2_alloc_options(sizeof(*user), user_dtor,
- AO2_ALLOC_OPT_LOCK_NOLOCK);
+ state = ao2_find(owc_states, id, OBJ_SEARCH_KEY);
+ if (!state) {
+ state = ao2_alloc(sizeof(*state) + strlen(id) + 1, NULL);
+ if (!state) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket state allocation failed\n", id);
+ AST_VECTOR_RESET(&apps, ast_free_ptr);
+ AST_VECTOR_FREE(&apps);
+ return 0;
+ }
+ strcpy(state->id, id); /* Safe */
+ ast_debug(3, "%s: Created new outbound websocket state\n", id);
+ } else {
+ ast_debug(3, "%s: Outbound websocket state already exists\n", id);
+ }
+ state->invalid_fields = ARI_OWC_FIELD_NONE;
+
+ /*
+ * Check all other owcs to make sure we don't have
+ * duplicate apps.
+ */
+ it = ao2_iterator_init(owcs, 0);
+ while ((other_owc = ao2_iterator_next(&it))) {
+ const char *other_id = ast_sorcery_object_get_id(other_owc);
+ if (!ast_strings_equal(other_id, id)) {
+ int i = 0;
+ for (i = 0; i < AST_VECTOR_SIZE(&apps); i++) {
+ const char *app = AST_VECTOR_GET(&apps, i);
+ if (ast_in_delimited_string(app, other_owc->apps, ',')) {
+ ast_log(LOG_WARNING,
+ "%s: Outbound websocket '%s' is also trying to register app '%s'\n",
+ id, other_id, app);
+ state->invalid_fields |= ARI_OWC_FIELD_APPS;
+ }
+ }
+ }
+ ao2_cleanup(other_owc);
+ if (owc->invalid) {
+ break;
+ }
+ }
+ ao2_iterator_destroy(&it);
+ AST_VECTOR_RESET(&apps, ast_free_ptr);
+ AST_VECTOR_FREE(&apps);
+
+ /*
+ * Check that the local_ari_user is valid and has
+ * a plain text password.
+ */
+ user = ast_sorcery_retrieve_by_id(sorcery, "user", owc->local_ari_user);
if (!user) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' not found\n",
+ id, owc->local_ari_user);
+ state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+ } else {
+ if (user->password_format != ARI_PASSWORD_FORMAT_PLAIN) {
+ ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' password MUST be plain text\n",
+ id, owc->local_ari_user);
+ state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+ }
+ if (ast_string_field_set(owc, local_ari_password, user->password) != 0) {
+ state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+ }
+ }
+ ao2_cleanup(user);
+
+ /*
+ * The container has AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE set so
+ * this is an insert or replace operation.
+ */
+ ao2_link(owc_states, state);
+ ao2_cleanup(state);
+
+ return 0;
+}
+
+static int outbound_websocket_state_cleanup(void *obj, void *arg, int flags)
+{
+ struct outbound_websocket_state *state = obj;
+ struct ari_conf_outbound_websocket *owc = ari_conf_get_owc(state->id);
+ int res = 0;
+
+ if (!owc) {
+ ast_debug(3, "%s: Cleaning up orphaned outbound websocket state\n", state->id);
+ res = CMP_MATCH;
+ }
+ ao2_cleanup(owc);
+
+ return res;
+}
+
+static void outbound_websockets_validate(const char *name)
+{
+ RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
+
+ ao2_callback(owcs, OBJ_NODATA, outbound_websocket_validate_cb, NULL);
+ /* Clean up any states whose configs have disappeared. */
+ ao2_callback(owc_states, OBJ_NODATA | OBJ_UNLINK,
+ outbound_websocket_state_cleanup, NULL);
+}
+
+struct ao2_container *ari_conf_get_owcs(void)
+{
+ if (!sorcery) {
return NULL;
}
- user->username = ast_strdup(cat);
- if (!user->username) {
+ return ast_sorcery_retrieve_by_fields(sorcery, "outbound_websocket",
+ AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+}
+
+struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id)
+{
+ if (!sorcery) {
return NULL;
}
- ao2_ref(user, +1);
- return user;
+ return ast_sorcery_retrieve_by_id(sorcery, "outbound_websocket", id);
}
-/*! \brief Sorting function for use with red/black tree */
-static int user_sort_cmp(const void *obj_left, const void *obj_right, int flags)
+struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app(
+ const char *app_name, unsigned int ws_type)
{
- const struct ast_ari_conf_user *user_left = obj_left;
- const struct ast_ari_conf_user *user_right = obj_right;
- const char *key_right = obj_right;
- int cmp;
+ struct ari_conf_outbound_websocket *owc = NULL;
+ struct ao2_container *owcs = NULL;
+ struct ao2_iterator i;
- switch (flags & OBJ_SEARCH_MASK) {
- case OBJ_SEARCH_OBJECT:
- key_right = user_right->username;
- /* Fall through */
- case OBJ_SEARCH_KEY:
- cmp = strcasecmp(user_left->username, key_right);
- break;
- case OBJ_SEARCH_PARTIAL_KEY:
- /*
- * We could also use a partial key struct containing a length
- * so strlen() does not get called for every comparison instead.
- */
- cmp = strncasecmp(user_left->username, key_right, strlen(key_right));
- break;
+ if (ast_strlen_zero(app_name)) {
+ return NULL;
+ }
+
+ ast_debug(3, "Checking outbound websockets for app '%s'\n", app_name);
+
+ owcs = ari_conf_get_owcs();
+ if (!owcs || ao2_container_count(owcs) == 0) {
+ ast_debug(3, "No outbound websockets found\n");
+ return NULL;
+ }
+
+ i = ao2_iterator_init(owcs, 0);
+ while ((owc = ao2_iterator_next(&i))) {
+ const char *id = ast_sorcery_object_get_id(owc);
+
+ ast_debug(3, "%s: Checking outbound websocket apps '%s' for app '%s'\n",
+ id, owc->apps, app_name);
+ if (owc->connection_type & ws_type
+ && ast_in_delimited_string(app_name, owc->apps, ',')) {
+ ast_debug(3, "%s: Found correct websocket type for apps '%s' for app '%s'\n",
+ id, owc->apps, app_name);
+ break;
+ }
+ ao2_cleanup(owc);
+ }
+ ao2_iterator_destroy(&i);
+ ao2_cleanup(owcs);
+ if (!owc) {
+ ast_debug(3, "No outbound websocket found for app '%s'\n", app_name);
+ }
+
+ return owc;
+}
+
+const char *ari_websocket_type_to_str(enum ari_websocket_type type)
+{
+ switch (type) {
+ case ARI_WS_TYPE_OUTBOUND_PERSISTENT:
+ return "persistent";
+ case ARI_WS_TYPE_OUTBOUND_PER_CALL:
+ return "per_call";
+ case ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG:
+ return "per_call_config";
+ case ARI_WS_TYPE_INBOUND:
+ return "inbound";
+ case ARI_WS_TYPE_ANY:
+ return "any";
default:
- /* Sort can only work on something with a full or partial key. */
- ast_assert(0);
- cmp = 0;
- break;
+ return "unknown";
}
- return cmp;
}
-/*! \brief \ref aco_type item_find function */
-static void *user_find(struct ao2_container *tmp_container, const char *cat)
+enum ari_conf_owc_fields ari_conf_owc_detect_changes(
+ struct ari_conf_outbound_websocket *old_owc,
+ struct ari_conf_outbound_websocket *new_owc)
{
- if (!cat) {
- return NULL;
+ enum ari_conf_owc_fields changed = ARI_OWC_FIELD_NONE;
+ const char *new_id = ast_sorcery_object_get_id(new_owc);
+ RAII_VAR(struct ast_variable *, changes, NULL, ast_variables_destroy);
+ struct ast_variable *v = NULL;
+ int res = 0;
+
+ res = ast_sorcery_diff(sorcery, old_owc, new_owc, &changes);
+ if (res != 0) {
+ ast_log(LOG_WARNING, "%s: Failed to create changeset\n", new_id);
+ return ARI_OWC_FIELD_NONE;
+ }
+
+ for (v = changes; v; v = v->next) {
+ ast_debug(2, "%s: %s changed to %s\n", new_id, v->name, v->value);
+ if (ast_strings_equal(v->name, "apps")) {
+ changed |= ARI_OWC_FIELD_APPS;
+ } else if (ast_strings_equal(v->name, "subscribe_all")) {
+ changed |= ARI_OWC_FIELD_SUBSCRIBE_ALL;
+ } else if (ast_strings_equal(v->name, "connection_type")) {
+ changed |= ARI_OWC_FIELD_CONNECTION_TYPE;
+ } else if (ast_strings_equal(v->name, "uri")) {
+ changed |= ARI_OWC_FIELD_URI;
+ } else if (ast_strings_equal(v->name, "protocols")) {
+ changed |= ARI_OWC_FIELD_PROTOCOLS;
+ } else if (ast_strings_equal(v->name, "username")) {
+ changed |= ARI_OWC_FIELD_USERNAME;
+ } else if (ast_strings_equal(v->name, "password")) {
+ changed |= ARI_OWC_FIELD_PASSWORD;
+ } else if (ast_strings_equal(v->name, "local_ari_user")) {
+ changed |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+ } else if (ast_strings_equal(v->name, "local_ari_password")) {
+ changed |= ARI_OWC_FIELD_LOCAL_ARI_PASSWORD;
+ } else if (ast_strings_equal(v->name, "tls_enabled")) {
+ changed |= ARI_OWC_FIELD_TLS_ENABLED;
+ } else if (ast_strings_equal(v->name, "ca_list_file")) {
+ changed |= ARI_OWC_FIELD_CA_LIST_FILE;
+ } else if (ast_strings_equal(v->name, "ca_list_path")) {
+ changed |= ARI_OWC_FIELD_CA_LIST_PATH;
+ } else if (ast_strings_equal(v->name, "cert_file")) {
+ changed |= ARI_OWC_FIELD_CERT_FILE;
+ } else if (ast_strings_equal(v->name, "priv_key_file")) {
+ changed |= ARI_OWC_FIELD_PRIV_KEY_FILE;
+ } else if (ast_strings_equal(v->name, "reconnect_interval")) {
+ changed |= ARI_OWC_FIELD_RECONNECT_INTERVAL;
+ } else if (ast_strings_equal(v->name, "reconnect_attempts")) {
+ changed |= ARI_OWC_FIELD_RECONNECT_ATTEMPTS;
+ } else if (ast_strings_equal(v->name, "connection_timeout")) {
+ changed |= ARI_OWC_FIELD_CONNECTION_TIMEOUT;
+ } else if (ast_strings_equal(v->name, "verify_server_cert")) {
+ changed |= ARI_OWC_FIELD_VERIFY_SERVER_CERT;
+ } else if (ast_strings_equal(v->name, "verify_server_hostname")) {
+ changed |= ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME;
+ } else {
+ ast_debug(2, "%s: Unknown change %s\n", new_id, v->name);
+ }
}
- return ao2_find(tmp_container, cat, OBJ_SEARCH_KEY);
+ return changed;
+
}
-static struct aco_type user_option = {
- .type = ACO_ITEM,
- .name = "user",
- .category_match = ACO_BLACKLIST_EXACT,
- .category = "general",
- .matchfield = "type",
- .matchvalue = "user",
- .item_alloc = user_alloc,
- .item_find = user_find,
- .item_offset = offsetof(struct ast_ari_conf, users),
-};
+/*! \brief \ref ast_ari_conf destructor. */
+static void general_dtor(void *obj)
+{
+ struct ari_conf_general *cfg = obj;
-static struct aco_type *global_user[] = ACO_TYPES(&user_option);
+ ast_string_field_free_memory(cfg);
+}
-static void conf_general_dtor(void *obj)
+static void *general_alloc(const char *name)
{
- struct ast_ari_conf_general *general = obj;
+ struct ari_conf_general *general = ast_sorcery_generic_alloc(
+ sizeof(*general), general_dtor);
+
+ if (!general) {
+ return NULL;
+ }
+
+ if (ast_string_field_init(general, 64) != 0) {
+ return NULL;
+ }
- ast_string_field_free_memory(general);
+ return general;
}
-/*! \brief \ref ast_ari_conf destructor. */
-static void conf_destructor(void *obj)
+#define MAX_VARS 128
+
+static int general_apply(const struct ast_sorcery *sorcery, void *obj)
{
- struct ast_ari_conf *cfg = obj;
+ struct ari_conf_general *general = obj;
+ char *parse = NULL;
+ AST_DECLARE_APP_ARGS(args,
+ AST_APP_ARG(vars)[MAX_VARS];
+ );
- ao2_cleanup(cfg->general);
- ao2_cleanup(cfg->users);
+ ast_debug(2, "Initializing general config\n");
+
+ parse = ast_strdupa(general->channelvars);
+ AST_STANDARD_APP_ARGS(args, parse);
+
+ ast_channel_set_ari_vars(args.argc, args.vars);
+ return 0;
}
-/*! \brief Allocate an \ref ast_ari_conf for config parsing */
-static void *conf_alloc(void)
+/*! \brief Encoding format handler converts from boolean to enum. */
+static int general_pretty_handler(const struct aco_option *opt,
+ struct ast_variable *var, void *obj)
{
- struct ast_ari_conf *cfg;
+ struct ari_conf_general *general = obj;
- cfg = ao2_alloc_options(sizeof(*cfg), conf_destructor,
- AO2_ALLOC_OPT_LOCK_NOLOCK);
- if (!cfg) {
+ general->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT;
+
+ return 0;
+}
+
+struct ari_conf_general* ari_conf_get_general(void)
+{
+ if (!sorcery) {
return NULL;
}
- cfg->general = ao2_alloc_options(sizeof(*cfg->general), conf_general_dtor,
- AO2_ALLOC_OPT_LOCK_NOLOCK);
+ return ast_sorcery_retrieve_by_id(sorcery, "general", "general");
+}
- cfg->users = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
- AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, user_sort_cmp, NULL);
+static int general_pretty_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+ const struct ari_conf_general *general = obj;
+
+ if (general->format == AST_JSON_PRETTY) {
+ *buf = ast_strdup("yes");
+ } else {
+ *buf = ast_strdup("no");
+ }
+ return 0;
+}
- if (!cfg->users
- || !cfg->general
- || ast_string_field_init(cfg->general, 64)
- || aco_set_defaults(&general_option, "general", cfg->general)) {
- ao2_ref(cfg, -1);
+/*! \brief Destructor for \ref ast_ari_conf_user */
+static void user_dtor(void *obj)
+{
+ struct ari_conf_user *user = obj;
+ ast_string_field_free_memory(user);
+ ast_debug(3, "%s: Disposing of user\n", ast_sorcery_object_get_id(user));
+}
+
+/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */
+static void *user_alloc(const char *cat)
+{
+ struct ari_conf_user *user = ast_sorcery_generic_alloc(
+ sizeof(*user), user_dtor);
+
+ if (!user) {
return NULL;
}
- return cfg;
+ if (ast_string_field_init(user, 64) != 0) {
+ ao2_cleanup(user);
+ user = NULL;
+ }
+
+ return user;
}
-#define CONF_FILENAME "ari.conf"
+static int user_apply(const struct ast_sorcery *sorcery, void *obj)
+{
+ struct ari_conf_user *user = obj;
+ const char *id = ast_sorcery_object_get_id(user);
-/*! \brief The conf file that's processed for the module. */
-static struct aco_file conf_file = {
- /*! The config file name. */
- .filename = CONF_FILENAME,
- /*! The mapping object types to be processed. */
- .types = ACO_TYPES(&general_option, &user_option),
-};
+ ast_debug(2, "%s: Initializing user\n", id);
-CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc,
- .files = ACO_FILES(&conf_file));
+ if (ast_strlen_zero(user->password)) {
+ ast_log(LOG_WARNING, "%s: User missing password\n", id);
+ return -1;
+ }
-struct ast_ari_conf *ast_ari_config_get(void)
+ return 0;
+}
+
+/*! \brief Parses the ast_ari_password_format enum from a config file */
+static int user_password_format_handler(const struct aco_option *opt,
+ struct ast_variable *var, void *obj)
{
- struct ast_ari_conf *res = ao2_global_obj_ref(confs);
- if (!res) {
- ast_log(LOG_ERROR,
- "Error obtaining config from " CONF_FILENAME "\n");
+ struct ari_conf_user *user = obj;
+
+ if (strcasecmp(var->value, "plain") == 0) {
+ user->password_format = ARI_PASSWORD_FORMAT_PLAIN;
+ } else if (strcasecmp(var->value, "crypt") == 0) {
+ user->password_format = ARI_PASSWORD_FORMAT_CRYPT;
+ } else {
+ return -1;
}
- return res;
+
+ return 0;
}
-struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
- const char *password)
+static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf)
{
- RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
- RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
- int is_valid = 0;
+ const struct ari_conf_user *user = obj;
+
+ if (user->password_format == ARI_PASSWORD_FORMAT_CRYPT) {
+ *buf = ast_strdup("crypt");
+ } else {
+ *buf = ast_strdup("plain");
+ }
+ return 0;
+}
- conf = ast_ari_config_get();
- if (!conf) {
+struct ao2_container *ari_conf_get_users(void)
+{
+ if (!sorcery) {
return NULL;
}
- user = ao2_find(conf->users, username, OBJ_SEARCH_KEY);
- if (!user) {
+ return ast_sorcery_retrieve_by_fields(sorcery, "user",
+ AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+}
+
+struct ari_conf_user *ari_conf_get_user(const char *username)
+{
+ if (!sorcery) {
return NULL;
}
- if (ast_strlen_zero(user->password)) {
- ast_log(LOG_WARNING,
- "User '%s' missing password; authentication failed\n",
- user->username);
+ return ast_sorcery_retrieve_by_id(sorcery, "user", username);
+}
+
+/*
+ * This is called by res_ari.c to validate the user and password
+ * for the websocket connection.
+ */
+struct ari_conf_user *ari_conf_validate_user(const char *username,
+ const char *password)
+{
+ struct ari_conf_user *user = NULL;
+ int is_valid = 0;
+
+ if (ast_strlen_zero(username) || ast_strlen_zero(password)) {
+ return NULL;
+ }
+
+ user = ast_sorcery_retrieve_by_id(sorcery, "user", username);
+ if (!user) {
return NULL;
}
@@ -268,120 +652,211 @@ struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
}
if (!is_valid) {
- return NULL;
+ ao2_cleanup(user);
+ user = NULL;
}
- ao2_ref(user, +1);
return user;
}
-/*! \brief Callback to validate a user object */
-static int validate_user_cb(void *obj, void *arg, int flags)
+int ari_sorcery_observer_add(const char *object_type,
+ const struct ast_sorcery_observer *callbacks)
{
- struct ast_ari_conf_user *user = obj;
-
- if (ast_strlen_zero(user->password)) {
- ast_log(LOG_WARNING, "User '%s' missing password\n",
- user->username);
+ if (!sorcery) {
+ return -1;
}
+ return ast_sorcery_observer_add(sorcery, object_type, callbacks);
+}
+int ari_sorcery_observer_remove(const char *object_type,
+ const struct ast_sorcery_observer *callbacks)
+{
+ if (!sorcery) {
+ return -1;
+ }
+ ast_sorcery_observer_remove(sorcery, object_type, callbacks);
return 0;
}
-/*! \brief Load (or reload) configuration. */
-static int process_config(int reload)
+static void outbound_websocket_created_cb(const void *obj)
{
- RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
+ const struct ari_conf_outbound_websocket *owc = obj;
- switch (aco_process_config(&cfg_info, reload)) {
- case ACO_PROCESS_ERROR:
+ ast_debug(3, "%s: Outbound XXXXXXXXXXXX websocket created\n", ast_sorcery_object_get_id(owc));
+}
+
+static struct ast_sorcery_observer observer_callbacks = {
+ .created = outbound_websocket_created_cb,
+ .loaded = outbound_websockets_validate,
+};
+
+#define ari_conf_bool(object, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ def_value, OPT_YESNO_T, 1, \
+ FLDSET(struct ari_conf_ ## object, field))
+
+#define _stringify(val) #val
+#define ari_conf_int(object, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ _stringify(def_value), OPT_INT_T, PARSE_IN_RANGE, \
+ FLDSET(struct ari_conf_ ## object, field), INT_MIN, INT_MAX)
+
+#define ari_conf_uint(object, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ _stringify(def_value), OPT_UINT_T, PARSE_IN_RANGE, \
+ FLDSET(struct ari_conf_ ## object, field), 0, UINT_MAX)
+
+#define ari_conf_sf(object, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ def_value, OPT_STRINGFIELD_T, 0, \
+ STRFLDSET(struct ari_conf_ ##object, field))
+
+#define ari_conf_cust(object, option, def_value) \
+ ast_sorcery_object_field_register_custom(sorcery, #object, #option, \
+ def_value, object ## _ ## option ## _handler, \
+ object ## _ ## option ## _to_str, NULL, 0, 0)
+
+AO2_STRING_FIELD_HASH_FN(outbound_websocket_state, id)
+AO2_STRING_FIELD_CMP_FN(outbound_websocket_state, id)
+
+static int ari_conf_init(void)
+{
+ ast_debug(2, "Initializing ARI configuration\n");
+
+ owc_states = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX,
+ AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, OWC_STATES_BUCKETS,
+ outbound_websocket_state_hash_fn, NULL,
+ outbound_websocket_state_cmp_fn);
+ if (!owc_states) {
+ ast_log(LOG_ERROR, "Failed to allocate outbound websocket states\n");
return -1;
- case ACO_PROCESS_OK:
- case ACO_PROCESS_UNCHANGED:
- break;
}
- conf = ast_ari_config_get();
- if (!conf) {
- ast_assert(0); /* We just configured; it should be there */
+ if (!(sorcery = ast_sorcery_open())) {
+ ast_log(LOG_ERROR, "Failed to open sorcery\n");
return -1;
}
- if (conf->general->enabled) {
- if (ao2_container_count(conf->users) == 0) {
- ast_log(LOG_ERROR, "No configured users for ARI\n");
- } else {
- ao2_callback(conf->users, OBJ_NODATA, validate_user_cb, NULL);
- }
+ ast_sorcery_apply_default(sorcery, "general", "config",
+ "ari.conf,criteria=type=general,single_object=yes,explicit_name=general");
+ ast_sorcery_apply_default(sorcery, "user", "config",
+ "ari.conf,criteria=type=user");
+ ast_sorcery_apply_default(sorcery, "outbound_websocket", "config",
+ "ari.conf,criteria=type=outbound_websocket");
+
+ if (ast_sorcery_object_register(sorcery, "general", general_alloc, NULL, general_apply)) {
+ ast_log(LOG_ERROR, "Failed to register ARI general object with sorcery\n");
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ return -1;
}
- return 0;
-}
+ if (ast_sorcery_object_register(sorcery, "user", user_alloc, NULL, user_apply)) {
+ ast_log(LOG_ERROR, "Failed to register ARI user object with sorcery\n");
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ return -1;
+ }
-#define MAX_VARS 128
+ if (ast_sorcery_object_register(sorcery, "outbound_websocket", outbound_websocket_alloc,
+ NULL, outbound_websocket_apply)) {
+ ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket object with sorcery\n");
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ return -1;
+ }
-static int channelvars_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
-{
- char *parse = NULL;
- AST_DECLARE_APP_ARGS(args,
- AST_APP_ARG(vars)[MAX_VARS];
- );
+ if (ast_sorcery_observer_add(sorcery, "outbound_websocket", &observer_callbacks)) {
+ ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket observer with sorcery\n");
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ return -1;
+ }
- parse = ast_strdupa(var->value);
- AST_STANDARD_APP_ARGS(args, parse);
+ ast_sorcery_object_field_register_nodoc(sorcery, "general", "type", "", OPT_NOOP_T, 0, 0);
+ ari_conf_sf(general, auth_realm, auth_realm, "Asterisk REST Interface");
+ ari_conf_sf(general, allowed_origins, allowed_origins, "");
+ ari_conf_sf(general, channelvars, channelvars, "");
+ ari_conf_bool(general, enabled, enabled, "yes");
+ ari_conf_cust(general, pretty, "no");
+ ari_conf_int(general, websocket_write_timeout, write_timeout,
+ AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT);
+
+
+ ast_sorcery_object_field_register(sorcery, "user", "type", "", OPT_NOOP_T, 0, 0);
+ ari_conf_sf(user, password, password, "");
+ ari_conf_bool(user, read_only, read_only, "no");
+ ari_conf_cust(user, password_format, "plain");
+
+ ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0);
+ ari_conf_sf(outbound_websocket, uri, uri, "");
+ ari_conf_sf(outbound_websocket, protocols, protocols, "");
+ ari_conf_sf(outbound_websocket, apps, apps, "");
+ ari_conf_sf(outbound_websocket, username, username, "");
+ ari_conf_sf(outbound_websocket, password, password, "");
+ ari_conf_sf(outbound_websocket, local_ari_user, local_ari_user, "");
+ ari_conf_sf(outbound_websocket, ca_list_file, ca_list_file, "");
+ ari_conf_sf(outbound_websocket, ca_list_path, ca_list_path, "");
+ ari_conf_sf(outbound_websocket, cert_file, cert_file, "");
+ ari_conf_sf(outbound_websocket, priv_key_file, priv_key_file, "");
+ ari_conf_bool(outbound_websocket, subscribe_all, subscribe_all, "no");
+ ari_conf_bool(outbound_websocket, tls_enabled, tls_enabled, "no");
+ ari_conf_bool(outbound_websocket, verify_server_cert, verify_server_cert, "yes");
+ ari_conf_bool(outbound_websocket, verify_server_hostname, verify_server_hostname, "yes");
+ ari_conf_cust(outbound_websocket, connection_type, "");
+ ari_conf_int(outbound_websocket, connection_timeout, connect_timeout, 500);
+ ari_conf_int(outbound_websocket, reconnect_attempts, reconnect_attempts, DEFAULT_RECONNECT_ATTEMPTS);
+ ari_conf_int(outbound_websocket, reconnect_interval, reconnect_interval, 500);
- ast_channel_set_ari_vars(args.argc, args.vars);
return 0;
}
-int ast_ari_config_init(void)
+int ari_conf_load(enum ari_conf_load_flags flags)
{
- if (aco_info_init(&cfg_info)) {
- aco_info_destroy(&cfg_info);
+ void (*loader)(const struct ast_sorcery *sorcery, const char *type);
+ const char *msg_prefix;
+
+ if (flags & ARI_CONF_RELOAD) {
+ loader = ast_sorcery_reload_object;
+ msg_prefix= "Reloading";
+ } else {
+ loader = ast_sorcery_load_object;
+ msg_prefix= "Loading";
+ }
+
+ if (flags & ARI_CONF_INIT) {
+ if (ari_conf_init() != 0) {
+ ast_log(LOG_ERROR, "Failed to initialize ARI configuration\n");
+ return -1;
+ }
+ }
+
+ if (!sorcery) {
+ ast_log(LOG_ERROR, "ARI configuration not initialized\n");
return -1;
}
- /* ARI general category options */
- aco_option_register(&cfg_info, "enabled", ACO_EXACT, general_options,
- "yes", OPT_BOOL_T, 1,
- FLDSET(struct ast_ari_conf_general, enabled));
- aco_option_register_custom(&cfg_info, "pretty", ACO_EXACT,
- general_options, "no", encoding_format_handler, 0);
- aco_option_register(&cfg_info, "auth_realm", ACO_EXACT, general_options,
- "Asterisk REST Interface", OPT_CHAR_ARRAY_T, 0,
- FLDSET(struct ast_ari_conf_general, auth_realm),
- ARI_AUTH_REALM_LEN);
- aco_option_register(&cfg_info, "allowed_origins", ACO_EXACT, general_options,
- "", OPT_STRINGFIELD_T, 0,
- STRFLDSET(struct ast_ari_conf_general, allowed_origins));
- aco_option_register(&cfg_info, "websocket_write_timeout", ACO_EXACT, general_options,
- AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT_STR, OPT_INT_T, PARSE_IN_RANGE,
- FLDSET(struct ast_ari_conf_general, write_timeout), 1, INT_MAX);
- aco_option_register_custom(&cfg_info, "channelvars", ACO_EXACT, general_options,
- "", channelvars_handler, 0);
-
- /* ARI type=user category options */
- aco_option_register(&cfg_info, "type", ACO_EXACT, global_user, NULL,
- OPT_NOOP_T, 0, 0);
- aco_option_register(&cfg_info, "read_only", ACO_EXACT, global_user,
- "no", OPT_BOOL_T, 1,
- FLDSET(struct ast_ari_conf_user, read_only));
- aco_option_register(&cfg_info, "password", ACO_EXACT, global_user,
- "", OPT_CHAR_ARRAY_T, 0,
- FLDSET(struct ast_ari_conf_user, password), ARI_PASSWORD_LEN);
- aco_option_register_custom(&cfg_info, "password_format", ACO_EXACT,
- global_user, "plain", password_format_handler, 0);
-
- return process_config(0);
-}
-
-int ast_ari_config_reload(void)
-{
- return process_config(1);
-}
-
-void ast_ari_config_destroy(void)
-{
- aco_info_destroy(&cfg_info);
- ao2_global_obj_release(confs);
+ if (flags & ARI_CONF_LOAD_GENERAL) {
+ ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "general");
+ loader(sorcery, "general");
+ }
+
+ if (flags & ARI_CONF_LOAD_USER) {
+ ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "user");
+ loader(sorcery, "user");
+ }
+
+ if (flags & ARI_CONF_LOAD_OWC) {
+ ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "outbound_websocket");
+ loader(sorcery, "outbound_websocket");
+ }
+
+ return 0;
+}
+
+void ari_conf_destroy(void)
+{
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ ao2_cleanup(owc_states);
}
diff --git a/res/ari/internal.h b/res/ari/internal.h
index 08a633894a..4829bad2bb 100644
--- a/res/ari/internal.h
+++ b/res/ari/internal.h
@@ -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);
/*! @} */
diff --git a/res/res_ari.c b/res/res_ari.c
index 56d8c28154..50d0f1730b 100644
--- a/res/res_ari.c
+++ b/res/res_ari.c
@@ -77,114 +77,6 @@
core
***/
-/*** DOCUMENTATION
-
- HTTP binding for the Stasis API
-
-
-
- 12.0.0
-
- General configuration settings
-
-
- 12.0.0
-
- Enable/disable the ARI module
-
- This option enables or disables the ARI module.
-
- ARI uses Asterisk's HTTP server, which must also be enabled in http.conf.
-
-
-
- [http.conf]
- [https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/]
-
-
-
-
- 11.11.0
- 12.4.0
-
- The timeout (in milliseconds) to set on WebSocket connections.
-
- 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.
-
-
-
-
- 12.0.0
-
- Responses from ARI are formatted to be human readable
-
-
-
- 12.0.0
-
- Realm to use for authentication. Defaults to Asterisk REST Interface.
-
-
-
- 12.0.0
-
- Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins.
-
-
-
- 14.2.0
-
- Comma separated list of channel variables to display in channel json.
-
-
-
-
-
- 12.0.0
-
- Per-user configuration settings
-
-
- 13.30.0
- 16.7.0
- 17.1.0
-
- Define this configuration section as a user.
-
-
- Configure this section as a user
-
-
-
-
-
- 13.30.0
- 16.7.0
- 17.1.0
-
- When set to yes, user is only authorized for read-only requests
-
-
-
- 13.30.0
- 16.7.0
- 17.1.0
-
- Crypted or plaintext password (see password_format)
-
-
-
- 12.0.0
-
- 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
-
-
-
-
-***/
-
#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);
}
diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json
index ef0849fc63..fd300981b6 100644
--- a/rest-api/api-docs/events.json
+++ b/rest-api/api-docs/events.json
@@ -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.",