]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
ARI Outbound Websockets
authorGeorge Joseph <gjoseph@sangoma.com>
Fri, 28 Mar 2025 12:54:21 +0000 (06:54 -0600)
committergithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Mon, 2 Jun 2025 16:35:34 +0000 (16:35 +0000)
Asterisk can now establish websocket sessions _to_ your ARI applications
as well as accepting websocket sessions _from_ them.
Full details: http://s.asterisk.net/ari-outbound-ws

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

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

15 files changed:
apps/app_stasis.c
configs/samples/ari.conf.sample
include/asterisk/ari.h
include/asterisk/vector.h
main/strings.c
res/ari/ari_doc.xml [new file with mode: 0644]
res/ari/ari_model_validators.c
res/ari/ari_model_validators.h
res/ari/ari_websockets.c
res/ari/ari_websockets.h
res/ari/cli.c
res/ari/config.c
res/ari/internal.h
res/res_ari.c
rest-api/api-docs/events.json

index bbe7718cc86296343245b1b0e6eb7af9d291ae74..5809b06fd3c18430c7b27d1eb7bc74778d22c713 100644 (file)
 
 /*** MODULEINFO
        <depend>res_stasis</depend>
+       <depend>res_ari</depend>
        <support_level>core</support_level>
  ***/
 
 #include "asterisk.h"
 
 #include "asterisk/app.h"
+#include "asterisk/ari.h"
 #include "asterisk/module.h"
 #include "asterisk/pbx.h"
 #include "asterisk/stasis.h"
@@ -86,6 +88,7 @@ static const char *stasis = "Stasis";
 static int app_exec(struct ast_channel *chan, const char *data)
 {
        char *parse = NULL;
+       char *connection_id;
        int ret = -1;
 
        AST_DECLARE_APP_ARGS(args,
@@ -104,13 +107,35 @@ static int app_exec(struct ast_channel *chan, const char *data)
 
        if (args.argc < 1) {
                ast_log(LOG_WARNING, "Stasis app_name argument missing\n");
-       } else {
-               ret = stasis_app_exec(chan,
-                                     args.app_name,
-                                     args.argc - 1,
-                                     args.app_argv);
+               goto done;
+       }
+
+       if (stasis_app_is_registered(args.app_name)) {
+               ast_debug(3, "%s: App '%s' is already registered\n",
+                       ast_channel_name(chan), args.app_name);
+               ret = stasis_app_exec(chan, args.app_name, args.argc - 1, args.app_argv);
+               goto done;
        }
+       ast_debug(3, "%s: App '%s' is NOT already registered\n",
+               ast_channel_name(chan), args.app_name);
+
+       /*
+        * The app isn't registered so we need to see if we have a
+        * per-call outbound websocket config we can use.
+        * connection_id will be freed by ast_ari_close_per_call_websocket().
+        */
+       connection_id = ast_ari_create_per_call_websocket(args.app_name, chan);
+       if (ast_strlen_zero(connection_id)) {
+               ast_log(LOG_WARNING,
+                       "%s: Stasis app '%s' doesn't exist\n",
+                       ast_channel_name(chan), args.app_name);
+               goto done;
+       }
+
+       ret = stasis_app_exec(chan, connection_id, args.argc - 1, args.app_argv);
+       ast_ari_close_per_call_websocket(connection_id);
 
+done:
        if (ret) {
                /* set ret to 0 so pbx_core doesnt hangup the channel */
                if (!ast_check_hangup(chan)) {
@@ -140,5 +165,5 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Stasis dialplan applicat
        .support_level = AST_MODULE_SUPPORT_CORE,
        .load = load_module,
        .unload = unload_module,
-       .requires = "res_stasis",
+       .requires = "res_stasis,res_ari",
 );
index 5ce3166bfa77ffdf7c8aa8786b8902e72047578b..e50eb39fe5c54563042d9fe46a13905caaa62a73 100644 (file)
@@ -35,3 +35,24 @@ enabled = yes       ; When set to no, ARI support is disabled.
 ; When set to plain, the password is in plaintext.
 ;
 ;password_format = plain
+
+; Outbound Websocket Connections
+;
+;[connection1]                 ; The connection name
+;type = outbound_websocket     ; Must be "outbound_websocket"
+;websocket_client_id = myid    ; The id of a websocket client defined in
+                               ; websocket_client.conf.
+                               ; Default: none
+;apps = app1, app2             ; A comma-separated list of Stasis applications
+                               ; that will be served by this connection.
+                               ; No other connection may serve these apps.
+                               ; Default: none
+;subscribe_all = no            ; If set to "yes", the server will receive all
+                               ; events just as though "subscribeAll=true" was
+                               ; specified on an incoming websocket connection.
+                               ; Default: no
+;local_ari_user = local_user   ; The name of a local ARI user defined above.
+                               ; This controls whether this connection can make
+                               ; read/write requests or is read-only.
+                               ; Default: none
+
index 165b6a867d59824d7a9157873fa6eb23d6cccfda..a3285c13f91170e53e41d4b3a5bb2adee1b69318 100644 (file)
@@ -244,4 +244,51 @@ void ast_ari_response_created(struct ast_ari_response *response,
  */
 void ast_ari_response_alloc_failed(struct ast_ari_response *response);
 
+/*!
+ * \brief Create a per-call outbound websocket connection.
+ *
+ * \param app_name The app name.
+ * \param channel The channel to create the websocket for.
+ *
+ * This function should really only be called by app_stasis.
+ *
+ * A "per_call" websocket configuration must already exist in
+ * ari.conf that has 'app_name' in its 'apps' parameter.
+ *
+ * The channel uniqueid is used to create a unique app_id
+ * composed of "<app_name>-<channel_uniqueid>" which will be
+ * returned from this call.  This ID will be used to register
+ * an ephemeral Stasis application and should be used as the
+ * app_name for the call to stasis_app_exec().  When
+ * stasis_app_exec() returns, ast_ari_close_per_call_websocket()
+ * must be called with the app_id to close the websocket.
+ *
+ * The channel unique id is also used to detect when the
+ * StasisEnd event is sent for the channel.  It's how
+ * ast_ari_close_per_call_websocket() knows that all
+ * messages for the channel have been sent and it's safe
+ * to close the websocket.
+ *
+ * \retval The ephemeral application id or NULL if one could
+ *         not be created. This pointer will be freed by
+ *         ast_ari_close_per_call_websocket().  Do not free
+ *         it yourself.
+ */
+char *ast_ari_create_per_call_websocket(const char *app_name,
+       struct ast_channel *channel);
+
+/*!
+ * \brief Close a per-call outbound websocket connection.
+ *
+ * \param app_id The ephemeral application id returned by
+ *               ast_ari_create_per_call_websocket().
+ *
+ * This function should really only be called by app_stasis.
+ *
+ * \note This call will block until all messages for the
+ *       channel have been sent or 5 seconds has elapsed.
+ *       After that, the websocket will be closed.
+ */
+void ast_ari_close_per_call_websocket(char *app_id);
+
 #endif /* _ASTERISK_ARI_H */
index 8276a7c4970ae73675a58f79a19a06f46383ac32..e71db648b77305b450d5b93843fd587965dd2190 100644 (file)
@@ -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
  *
index f370466776b67bedf5a555e226ce83bc3b53ece2..a524d55025d828065d8041a947a5154f192335bc 100644 (file)
@@ -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 (file)
index 0000000..f897ecb
--- /dev/null
@@ -0,0 +1,157 @@
+<!DOCTYPE docs SYSTEM "appdocsxml.dtd">
+<?xml-stylesheet type="text/xsl" href="appdocsxml.xslt"?>
+<docs xmlns:xi="http://www.w3.org/2001/XInclude">
+       <configInfo name="res_ari" language="en_US">
+               <synopsis>HTTP binding for the Stasis API</synopsis>
+               <configFile name="ari.conf">
+                       <configObject name="general">
+                               <since>
+                                       <version>12.0.0</version>
+                               </since>
+                               <synopsis>General configuration settings</synopsis>
+                               <configOption name="enabled">
+                                       <since>
+                                               <version>12.0.0</version>
+                                       </since>
+                                       <synopsis>Enable/disable the ARI module</synopsis>
+                                       <description>
+                                               <para>This option enables or disables the ARI module.</para>
+                                               <note>
+                                                       <para>ARI uses Asterisk's HTTP server, which must also be enabled in <filename>http.conf</filename>.</para>
+                                               </note>
+                                       </description>
+                                       <see-also>
+                                               <ref type="filename">http.conf</ref>
+                                               <ref type="link">https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/</ref>
+                                       </see-also>
+                               </configOption>
+                               <configOption name="websocket_write_timeout" default="100">
+                                       <since>
+                                               <version>11.11.0</version>
+                                               <version>12.4.0</version>
+                                       </since>
+                                       <synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis>
+                                       <description>
+                                               <para>If a websocket connection accepts input slowly, the timeout
+                                               for writes to it can be increased to keep it from being disconnected.
+                                               Value is in milliseconds.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="pretty">
+                                       <since>
+                                               <version>12.0.0</version>
+                                       </since>
+                                       <synopsis>Responses from ARI are formatted to be human readable</synopsis>
+                               </configOption>
+                               <configOption name="auth_realm">
+                                       <since>
+                                               <version>12.0.0</version>
+                                       </since>
+                                       <synopsis>Realm to use for authentication. Defaults to Asterisk REST Interface.</synopsis>
+                               </configOption>
+                               <configOption name="allowed_origins">
+                                       <since>
+                                               <version>12.0.0</version>
+                                       </since>
+                                       <synopsis>Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins.</synopsis>
+                               </configOption>
+                               <configOption name="channelvars">
+                                       <since>
+                                               <version>14.2.0</version>
+                                       </since>
+                                       <synopsis>Comma separated list of channel variables to display in channel json.</synopsis>
+                               </configOption>
+                       </configObject>
+
+                       <configObject name="user">
+                               <since>
+                                       <version>12.0.0</version>
+                               </since>
+                               <synopsis>Per-user configuration settings</synopsis>
+                               <configOption name="type">
+                                       <since>
+                                               <version>13.30.0</version>
+                                               <version>16.7.0</version>
+                                               <version>17.1.0</version>
+                                       </since>
+                                       <synopsis>Define this configuration section as a user.</synopsis>
+                                       <description>
+                                               <enumlist>
+                                                       <enum name="user"><para>Configure this section as a <replaceable>user</replaceable></para></enum>
+                                               </enumlist>
+                                       </description>
+                               </configOption>
+                               <configOption name="read_only">
+                                       <since>
+                                               <version>13.30.0</version>
+                                               <version>16.7.0</version>
+                                               <version>17.1.0</version>
+                                       </since>
+                                       <synopsis>When set to yes, user is only authorized for read-only requests</synopsis>
+                               </configOption>
+                               <configOption name="password">
+                                       <since>
+                                               <version>13.30.0</version>
+                                               <version>16.7.0</version>
+                                               <version>17.1.0</version>
+                                       </since>
+                                       <synopsis>Crypted or plaintext password (see password_format)</synopsis>
+                               </configOption>
+                               <configOption name="password_format">
+                                       <since>
+                                               <version>12.0.0</version>
+                                       </since>
+                                       <synopsis>password_format may be set to plain (the default) or crypt. When set to crypt, crypt(3) is used to validate the password. A crypted password can be generated using mkpasswd -m sha-512. When set to plain, the password is in plaintext</synopsis>
+                               </configOption>
+                       </configObject>
+                       <configObject name="outbound_websocket">
+                               <since>
+                                       <version>20.15.0</version>
+                                       <version>21.10.0</version>
+                                       <version>22.5.0</version>
+                               </since>
+                               <synopsis>Outbound websocket configuration</synopsis>
+                               <configOption name="type">
+                                       <since>
+                                               <version>20.15.0</version>
+                                               <version>21.10.0</version>
+                                               <version>22.5.0</version>
+                                       </since>
+                                       <synopsis>Must be "outbound_websocket".</synopsis>
+                               </configOption>
+                               <configOption name="websocket_client_id">
+                                       <since>
+                                               <version>20.15.0</version>
+                                               <version>21.10.0</version>
+                                               <version>22.5.0</version>
+                                       </since>
+                                       <synopsis>The ID of a connection defined in websocket_client.conf.</synopsis>
+                               </configOption>
+                               <configOption name="apps">
+                                       <since>
+                                               <version>20.15.0</version>
+                                               <version>21.10.0</version>
+                                               <version>22.5.0</version>
+                                       </since>
+                                       <synopsis>Comma separated list of stasis applications that will use this websocket.</synopsis>
+                               </configOption>
+                               <configOption name="local_ari_user">
+                                       <since>
+                                               <version>20.15.0</version>
+                                               <version>21.10.0</version>
+                                               <version>22.5.0</version>
+                                       </since>
+                                       <synopsis>The local ARI user to act as.</synopsis>
+                               </configOption>
+                               <configOption name="subscribe_all" default="no">
+                                       <since>
+                                               <version>20.15.0</version>
+                                               <version>21.10.0</version>
+                                               <version>22.5.0</version>
+                                       </since>
+                                       <synopsis>Subscribe applications to all event</synopsis>
+                               </configOption>
+                       </configObject>
+               </configFile>
+       </configInfo>
+</docs>
\ No newline at end of file
index a1dda282b8e98d2069def7b4d0869b960060c844..5ef382111239c7a16d771daf2ba4aa70dd0285f0 100644 (file)
@@ -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
index a351b443040b7ea900f41cff81964e99db4330fc..58701c2af1a1130f478f86608812da59f1d6de05 100644 (file)
@@ -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)
index 3712dd7d108969fc44670c69ea6aff5c506be67d..cc5bd9e9508804418064d4c0dcb37a4e9eae54f7 100644 (file)
 #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"
+#include "asterisk/websocket_client.h"
 
 
 /*! \file
  * \author David M. Lee, II <dlee@digium.com>
  */
 
-/*! 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 +78,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 +100,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 +213,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 +221,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 +246,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 +287,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 == AST_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 == AST_WS_TYPE_CLIENT_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 +347,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 == AST_WS_TYPE_CLIENT_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
+               & (AST_WS_TYPE_CLIENT_PER_CALL_CONFIG | AST_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;
 
-                       if (strlen(app_parse) == 0) {
-                               /* ast_app_separate_args can't handle "" */
-                               args->app_count = 1;
-                               vals[0] = app_parse;
+       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_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 == AST_WS_TYPE_CLIENT_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 & (AST_WS_TYPE_INBOUND | AST_WS_TYPE_CLIENT_PER_CALL_CONFIG))) {
+                               session_send_app_event(session, "ApplicationRegistered",
+                                       app_name);
                        }
+               }
+
+               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;
+               }
 
-                       for (j = 0; j < args->app_count; ++j) {
-                               args->app[j] = (vals[j]);
+               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 +526,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 +553,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);
 
-       ari_ws_session_reset(ari_ws_session);
-       if (ari_ws_session_registry) {
-               ao2_unlink(ari_ws_session_registry, ari_ws_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);
+       }
+
+       /*
+        * 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 != AST_WS_TYPE_CLIENT_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 ast_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");
-               }
+       subscribe_all = ast_variable_find_in_list(get_params, "subscribeAll");
 
-               if (msg) {
-                       ast_http_error(ser, response->response_code, response->response_text, msg);
-                       return -1;
-               }
-       }
-
-       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, AST_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,89 +817,755 @@ 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 == AST_WS_TYPE_CLIENT_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 == AST_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 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)
 {
-       if (!ari_ws_session_registry) {
+       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->websocket_client->uri);
+
+               astws = ast_websocket_client_connect(session->owc->websocket_client,
+                       NULL, session->session_id, &result);
+               if (!astws || result != WS_OK) {
+                       if (session->type == AST_WS_TYPE_CLIENT_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->websocket_client->reconnect_interval * 1000);
+                       continue;
+               }
+               ast_log(LOG_NOTICE, "%s: Outbound websocket connected to %s\n",
+                       session->type == AST_WS_TYPE_CLIENT_PERSISTENT ? session->session_id : session->channel_name,
+                               session->owc->websocket_client->uri);
+
+               /*
+                * 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->websocket_client->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->websocket_client->connect_timeout + owc->websocket_client->reconnect_interval) \
+       * (owc->websocket_client->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, AST_WS_TYPE_CLIENT_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, AST_WS_TYPE_CLIENT_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)
+{
+       /*
+        * 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, AST_WS_TYPE_CLIENT_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);
 
-       ao2_callback(ari_ws_session_registry, OBJ_MULTIPLE | OBJ_NODATA,
-               ari_ws_session_shutdown_cb, NULL);
+       /*
+        * 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));
 
-       ao2_cleanup(ari_ws_session_registry);
-       ari_ws_session_registry = NULL;
+       /*
+        * 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);
+
+       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;
+       }
+
+       session = session_create(NULL, owc->apps, owc->subscribe_all, owc_id,
+               owc, owc->websocket_client->connection_type);
+       if (!session) {
+               ast_log(LOG_WARNING, "%s: Failed to create websocket session\n", owc_id);
+               return 0;
+       }
+
+       if (owc->websocket_client->connection_type == AST_WS_TYPE_CLIENT_PER_CALL_CONFIG) {
+               /* There's no thread to transfer the reference to */
+               session_unref(session);
+               return 0;
+       }
+
+       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 &
+                       (AST_WS_TYPE_CLIENT_PERSISTENT | AST_WS_TYPE_CLIENT_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 == AST_WS_TYPE_CLIENT_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;
+       }
+}
+
+static struct ast_sorcery_observer observer_callbacks = {
+       .loaded = outbound_sessions_load,
+};
+
 int ari_websocket_unload_module(void)
 {
-       ari_ws_session_registry_dtor();
+       ari_sorcery_observer_remove("outbound_websocket", &observer_callbacks);
+       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);
+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");
+               ari_websocket_unload_module();
+               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();
+               ari_websocket_unload_module();
                return AST_MODULE_LOAD_DECLINE;
        }
 
        protocol = ast_websocket_sub_protocol_alloc("ari");
        if (!protocol) {
-               ao2_ref(ast_ws_server, -1);
-               ast_ws_server = NULL;
-               ari_ws_session_registry_dtor();
+               ari_websocket_unload_module();
                return AST_MODULE_LOAD_DECLINE;
        }
        protocol->session_attempted = websocket_attempted_cb;
index 4ad180acfdb22e01440f9d89a826f91446909dda..a08277b50a93bd4a51a176a91bececb2807ae63b 100644 (file)
@@ -28,6 +28,7 @@
 #include "asterisk/http.h"
 #include "asterisk/json.h"
 #include "asterisk/vector.h"
+#include "asterisk/websocket_client.h"
 
 struct ast_ari_events_event_websocket_args;
 
@@ -35,19 +36,45 @@ struct ast_ari_events_event_websocket_args;
  * which causes optional_api stuff to happen, which makes optional_api more
  * difficult to debug. */
 
-//struct ast_websocket_server;
 struct ast_websocket;
 
+/*
+ * Since we create a "stasis-<appname>" dialplan context for each
+ * stasis app, we need to make sure that the total length will be
+ * <= AST_MAX_CONTEXT
+ */
+#define STASIS_CONTEXT_PREFIX "stasis-"
+#define STASIS_CONTEXT_PREFIX_LEN (sizeof(STASIS_CONTEXT_PREFIX) - 1)
+#define ARI_MAX_APP_NAME_LEN (AST_MAX_CONTEXT - STASIS_CONTEXT_PREFIX_LEN)
+
 struct ari_ws_session {
+       enum ast_websocket_type type;                   /*!< The type of websocket session. */
        struct ast_websocket *ast_ws_session;           /*!< The parent websocket session. */
        int (*validator)(struct ast_json *);            /*!< The message validator. */
-       struct ao2_container *websocket_apps;           /*!< List of Stasis apps registered to
+       struct ast_vector_string websocket_apps;        /*!< List of Stasis apps registered to
                                                             the websocket session. */
+       int subscribe_all;                              /*!< Flag indicating if all events are subscribed to. */
        AST_VECTOR(, struct ast_json *) message_queue;  /*!< Container for holding delayed messages. */
        char *app_name;                                 /*!< The name of the Stasis application. */
+       char *remote_addr;                              /*!< The remote address. */
+       struct ari_conf_outbound_websocket *owc;           /*!< The outbound websocket configuration. */
+       pthread_t thread;                               /*!< The thread that handles the websocket. */
+       char *channel_id;                               /*!< The channel id for per-call websocket. */
+       char *channel_name;                             /*!< The channel name for per-call websocket. */
+       int stasis_end_sent;                            /*!< Flag indicating if the StasisEnd message was sent. */
+       int connected;                                  /*!< Flag indicating if the websocket is connected. */
+       int closing;                                    /*!< Flag indicating if the session is closing. */
        char session_id[];                              /*!< The id for the websocket session. */
 };
 
+struct ao2_container* ari_websocket_get_sessions(void);
+struct ari_ws_session *ari_websocket_get_session(const char *session_id);
+struct ari_ws_session *ari_websocket_get_session_by_app(const char *app_name);
+const char *ari_websocket_type_to_str(enum ast_websocket_type type);
+void ari_websocket_shutdown(struct ari_ws_session *session);
+void ari_websocket_shutdown_all(void);
+int ari_outbound_websocket_start(struct ari_conf_outbound_websocket *owc);
+
 /*!
  * \internal
  * \brief Send a JSON event to a websocket.
@@ -91,6 +118,6 @@ void ari_handle_websocket(struct ast_tcptls_session_instance *ser,
        struct ast_variable *headers);
 
 int ari_websocket_unload_module(void);
-int ari_websocket_load_module(void);
+int ari_websocket_load_module(int is_enabled);
 
 #endif /* ARI_WEBSOCKETS_H_ */
index f9d9cecfb78d216c6d16e3bfc482847df3134ce6..30c5f45c4af65e702d7bd864e448c4a52924a719 100644 (file)
 #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->websocket_client->connection_type),
+               owc->apps,
+               invalid_fields == ARI_OWC_FIELD_NONE ? "valid" : "INVALID",
+               owc->websocket_client->uri);
+       return 0;
+}
+
+#define DASHES "----------------------------------------------------------------------"
+static char *ari_show_owcs(struct ast_cli_entry *e, int cmd,
+       struct ast_cli_args *a)
+{
+       RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "ari show outbound-websockets";
+               e->usage =
+                       "Usage: ari show outbound-websockets\n"
+                       "       Shows all ARI outbound-websockets\n";
+               return NULL;
+       case CLI_GENERATE:
+               return NULL;
+       default:
+               break;
+       }
+
+       if (a->argc != 3) {
+               return CLI_SHOWUSAGE;
+       }
+
+       owcs = ari_conf_get_owcs();
+       if (!owcs) {
+               ast_cli(a->fd, "Error getting ARI configuration\n");
+               return CLI_FAILURE;
+       }
+
+       ast_cli(a->fd, "%-32s %-15s %-32s %-7s %s\n", "Name", "Type", "Apps", "Status", "URI");
+       ast_cli(a->fd, "%.*s %.*s %.*s %.*s %.*s\n", 32, DASHES, 15, DASHES, 32, DASHES, 7, DASHES, 64, DASHES);
+
+       ao2_callback(owcs, OBJ_NODATA, show_owc_cb, a);
+
+       return CLI_SUCCESS;
+}
+
+static char *ari_show_owc(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup);
+       RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
+       const char *id = NULL;
+       enum ari_conf_owc_fields invalid_fields;
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "ari show outbound-websocket";
+               e->usage =
+                       "Usage: ari show outbound-websocket <connection id>\n"
+                       "       Shows a specific ARI outbound websocket\n";
+               return NULL;
+       case CLI_GENERATE:
+               complete_sorcery_object(owcs, a->word);
+               return NULL;
+       default:
+               break;
+       }
+
+       if (a->argc != 4) {
+               return CLI_SHOWUSAGE;
+       }
+
+       owc = ari_conf_get_owc(a->argv[3]);
+       if (!owc) {
+               ast_cli(a->fd, "Error getting ARI configuration\n");
+               return CLI_FAILURE;
+       }
+       id = ast_sorcery_object_get_id(owc);
+       invalid_fields = ari_conf_owc_get_invalid_fields(id);
+
+       ast_cli(a->fd, "[%s] %s\n", id,
+               invalid_fields == ARI_OWC_FIELD_NONE ? "" : "**INVALID**");
+       ast_cli(a->fd, "uri =                    %s\n", owc->websocket_client->uri);
+       ast_cli(a->fd, "protocols =              %s\n", owc->websocket_client->protocols);
+       ast_cli(a->fd, "apps =                   %s%s\n", owc->apps,
+               invalid_fields & ARI_OWC_FIELD_APPS ? " (invalid)" : "");
+       ast_cli(a->fd, "username =               %s\n", owc->websocket_client->username);
+       ast_cli(a->fd, "password =               %s\n", S_COR(owc->websocket_client->password, "********", ""));
+       ast_cli(a->fd, "local_ari_user =         %s%s\n", owc->local_ari_user,
+               invalid_fields & ARI_OWC_FIELD_LOCAL_ARI_USER ? " (invalid)" : "");
+       ast_cli(a->fd, "connection_type =        %s\n", ari_websocket_type_to_str(owc->websocket_client->connection_type));
+       ast_cli(a->fd, "subscribe_all =          %s\n", AST_CLI_YESNO(owc->subscribe_all));
+       ast_cli(a->fd, "connec_timeout =         %d\n", owc->websocket_client->connect_timeout);
+       ast_cli(a->fd, "reconnect_attempts =     %d\n", owc->websocket_client->reconnect_attempts);
+       ast_cli(a->fd, "reconnect_interval =     %d\n", owc->websocket_client->reconnect_interval);
+       ast_cli(a->fd, "tls_enabled =            %s\n", AST_CLI_YESNO(owc->websocket_client->tls_enabled));
+       ast_cli(a->fd, "ca_list_file =           %s\n", owc->websocket_client->ca_list_file);
+       ast_cli(a->fd, "ca_list_path =           %s\n", owc->websocket_client->ca_list_path);
+       ast_cli(a->fd, "cert_file =              %s\n", owc->websocket_client->cert_file);
+       ast_cli(a->fd, "priv_key_file =          %s\n", owc->websocket_client->priv_key_file);
+       ast_cli(a->fd, "verify_server =          %s\n", AST_CLI_YESNO(owc->websocket_client->verify_server_cert));
+       ast_cli(a->fd, "verify_server_hostname = %s\n", AST_CLI_YESNO(owc->websocket_client->verify_server_hostname));
+       ast_cli(a->fd, "\n");
+
+       return CLI_SUCCESS;
+}
+
+static char *ari_start_owc(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup);
+       RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
+
+       if (!owcs) {
+               ast_cli(a->fd, "Error getting ARI configuration\n");
+               return CLI_FAILURE ;
+       }
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "ari start outbound-websocket";
+               e->usage =
+                       "Usage: ari start outbound-websocket <connection id>\n"
+                       "       Starts a specific ARI outbound websocket\n";
+               return NULL;
+       case CLI_GENERATE:
+               complete_sorcery_object(owcs, a->word);
+               return NULL;
+       default:
+               break;
+       }
+
+       if (a->argc != 4) {
+               return CLI_SHOWUSAGE;
+       }
+
+       owc = ari_conf_get_owc(a->argv[3]);
+       if (!owc) {
+               ast_cli(a->fd, "Error getting ARI configuration\n");
+               return CLI_FAILURE;
+       }
+       ast_cli(a->fd, "Starting websocket session for outbound-websocket '%s'\n", a->argv[3]);
+
+       if (ari_outbound_websocket_start(owc) != 0) {
+               ast_cli(a->fd, "Error starting outbound websocket\n");
+               return CLI_FAILURE ;
+       }
+
+       return CLI_SUCCESS;
+}
+
+static int show_sessions_cb(void *obj, void *arg, int flags)
+{
+       struct ari_ws_session *session = obj;
+       struct ast_cli_args *a = arg;
+       char *apps = ast_vector_string_join(&session->websocket_apps, ",");
+
+       ast_cli(a->fd, "%-*s %-15s %-32s %-5s %s\n",
+               AST_UUID_STR_LEN,
+               session->session_id,
+               ari_websocket_type_to_str(session->type),
+               S_OR(session->remote_addr, "N/A"),
+               session->type == AST_WS_TYPE_CLIENT_PER_CALL_CONFIG
+                       ? "N/A" : (session->connected ? "Up" : "Down"),
+               S_OR(apps, ""));
+
+       ast_free(apps);
+       return 0;
+}
+
+#define DASHES "----------------------------------------------------------------------"
+static char *ari_show_sessions(struct ast_cli_entry *e, int cmd,
+       struct ast_cli_args *a)
+{
+       RAII_VAR(struct ao2_container *, sessions, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "ari show websocket sessions";
+               e->usage =
+                       "Usage: ari show websocket sessions\n"
+                       "       Shows all ARI websocket sessions\n";
+               return NULL;
+       case CLI_GENERATE:
+               return NULL;
+       default:
+               break;
+       }
+
+       if (a->argc != 4) {
+               return CLI_SHOWUSAGE;
+       }
+
+       sessions = ari_websocket_get_sessions();
+       if (!sessions) {
+               ast_cli(a->fd, "Error getting websocket sessions\n");
+               return CLI_FAILURE;
+       }
+
+       ast_cli(a->fd, "%-*.*s %-15.15s %-32.32s %-5.5s %-16.16s\n",
+               AST_UUID_STR_LEN, AST_UUID_STR_LEN,
+               "Connection ID",
+               "Type",
+               "RemoteAddr",
+               "State",
+               "Apps"
+               );
+       ast_cli(a->fd, "%-*.*s %-15.15s %-32.32s %-5.5s %-16.16s\n",
+               AST_UUID_STR_LEN, AST_UUID_STR_LEN, DASHES, DASHES, DASHES, DASHES, DASHES);
+
+       ao2_callback(sessions, OBJ_NODATA, show_sessions_cb, a);
+
+       return CLI_SUCCESS;
+}
+
+static char *ari_shut_sessions(struct ast_cli_entry *e, int cmd,
+       struct ast_cli_args *a)
+{
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "ari shutdown websocket sessions";
+               e->usage =
+                       "Usage: ari shutdown websocket sessions\n"
+                       "       Shuts down all ARI websocket sessions\n";
+               return NULL;
+       case CLI_GENERATE:
+               return NULL;
+       default:
+               break;
+       }
+
+       if (a->argc != 4) {
+               return CLI_SHOWUSAGE;
+       }
+
+       ast_cli(a->fd, "Shutting down all websocket sessions\n");
+       ari_websocket_shutdown_all();
+
+       return CLI_SUCCESS;
+}
+
+static void complete_session(struct ao2_container *container,
+       const char *word)
+{
+       size_t wordlen = strlen(word);
+       struct ari_ws_session *session;
+       struct ao2_iterator i = ao2_iterator_init(container, 0);
+
+       while ((session = ao2_iterator_next(&i))) {
+               if (!strncasecmp(word, session->session_id, wordlen)) {
+                       ast_cli_completion_add(ast_strdup(session->session_id));
+               }
+               ao2_ref(session, -1);
+       }
+       ao2_iterator_destroy(&i);
+}
+
+static char *ari_shut_session(struct ast_cli_entry *e, int cmd,
+       struct ast_cli_args *a)
+{
+       RAII_VAR(struct ari_ws_session *, session, NULL, ao2_cleanup);
+       RAII_VAR(struct ao2_container *, sessions, ari_websocket_get_sessions(), ao2_cleanup);
+
+       if (!sessions) {
+               ast_cli(a->fd, "Error getting ARI configuration\n");
+               return CLI_FAILURE ;
+       }
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "ari shutdown websocket session";
+               e->usage =
+                       "Usage: ari shutdown websocket session <id>\n"
+                       "       Shuts down ARI websocket session\n";
+               return NULL;
+       case CLI_GENERATE:
+               complete_session(sessions, a->word);
+               return NULL;
+       default:
+               break;
+       }
+
+       if (a->argc != 5) {
+               return CLI_SHOWUSAGE;
+       }
+
+       session = ari_websocket_get_session(a->argv[4]);
+       if (!session) {
+               ast_cli(a->fd, "Websocket session '%s' not found\n", a->argv[4]);
+               return CLI_FAILURE ;
+       }
+       ast_cli(a->fd, "Shutting down websocket session '%s'\n", a->argv[4]);
+       ari_websocket_shutdown(session);
+
+       return CLI_SUCCESS;
+}
+
 static struct ast_cli_entry cli_ari[] = {
        AST_CLI_DEFINE(ari_show, "Show ARI settings"),
        AST_CLI_DEFINE(ari_show_users, "List ARI users"),
@@ -426,12 +688,18 @@ static struct ast_cli_entry cli_ari[] = {
        AST_CLI_DEFINE(ari_show_apps, "List registered ARI applications"),
        AST_CLI_DEFINE(ari_show_app, "Display details of a registered ARI application"),
        AST_CLI_DEFINE(ari_set_debug, "Enable/disable debugging of an ARI application"),
+       AST_CLI_DEFINE(ari_show_owcs, "List outbound websocket connections"),
+       AST_CLI_DEFINE(ari_show_owc, "Show outbound websocket connection"),
+       AST_CLI_DEFINE(ari_start_owc, "Start outbound websocket connection"),
+       AST_CLI_DEFINE(ari_show_sessions, "Show websocket sessions"),
+       AST_CLI_DEFINE(ari_shut_session, "Shutdown websocket session"),
+       AST_CLI_DEFINE(ari_shut_sessions, "Shutdown websocket sessions"),
 };
 
-int ast_ari_cli_register(void) {
+int ari_cli_register(void) {
        return ast_cli_register_multiple(cli_ari, ARRAY_LEN(cli_ari));
 }
 
-void ast_ari_cli_unregister(void) {
+void ari_cli_unregister(void) {
        ast_cli_unregister_multiple(cli_ari, ARRAY_LEN(cli_ari));
 }
index 501487f519c75cca7092e384aace8d974d008283..af89ff9e363df857bae515e5ed5183d5bf3823e2 100644 (file)
  * \author David M. Lee, II <dlee@digium.com>
  */
 
+#include <limits.h>
+
 #include "asterisk.h"
 
+#include "asterisk/sorcery.h"
 #include "asterisk/config_options.h"
 #include "asterisk/http_websocket.h"
+#include "asterisk/websocket_client.h"
 #include "asterisk/app.h"
 #include "asterisk/channel.h"
+#include "asterisk/vector.h"
 #include "internal.h"
 
-/*! \brief Locking container for safe configuration access. */
-static AO2_GLOBAL_OBJ_STATIC(confs);
-
-/*! \brief Mapping of the ARI conf struct's globals to the
- *         general context in the config file. */
-static struct aco_type general_option = {
-       .type = ACO_GLOBAL,
-       .name = "general",
-       .item_offset = offsetof(struct ast_ari_conf, general),
-       .category = "general",
-       .category_match = ACO_WHITELIST_EXACT,
+static struct ast_sorcery *sorcery;
+
+struct outbound_websocket_state {
+       enum ari_conf_owc_fields invalid_fields;
+       char id[0];
 };
 
-static struct aco_type *general_options[] = ACO_TYPES(&general_option);
+#define OWC_STATES_BUCKETS 13
+struct ao2_container *owc_states = NULL;
 
-/*! \brief Encoding format handler converts from boolean to enum. */
-static int encoding_format_handler(const struct aco_option *opt,
+static void outbound_websocket_dtor(void *obj)
+{
+       struct ari_conf_outbound_websocket *owc = obj;
+
+       ast_debug(3, "%s: Disposing of outbound websocket config\n",
+               ast_sorcery_object_get_id(owc));
+       ast_string_field_free_memory(owc);
+       ao2_cleanup(owc->websocket_client);
+       owc->websocket_client = NULL;
+}
+
+static void *outbound_websocket_alloc(const char *id)
+{
+       struct ari_conf_outbound_websocket *owc = NULL;
+
+       owc = ast_sorcery_generic_alloc(sizeof(*owc), outbound_websocket_dtor);
+       if (!owc) {
+               return NULL;
+       }
+
+       if (ast_string_field_init(owc, 1024) != 0) {
+               ao2_cleanup(owc);
+               return NULL;
+       }
+
+       ast_debug(2, "%s: Allocated outbound websocket config\n", id);
+       return owc;
+}
+
+static int outbound_websocket_websocket_client_id_from_str(const struct aco_option *opt,
        struct ast_variable *var, void *obj)
 {
-       struct ast_ari_conf_general *general = obj;
+       struct ari_conf_outbound_websocket *owc = obj;
 
-       if (!strcasecmp(var->name, "pretty")) {
-               general->format = ast_true(var->value) ?
-                       AST_JSON_PRETTY : AST_JSON_COMPACT;
-       } else {
+       if (ast_strlen_zero(var->value)) {
+               ast_log(LOG_ERROR, "%s: Outbound websocket missing websocket client id\n",
+                       ast_sorcery_object_get_id(owc));
+               return -1;
+       }
+       owc->websocket_client = ast_websocket_client_retrieve_by_id(var->value);
+       if (!owc->websocket_client) {
+               ast_log(LOG_ERROR, "%s: Outbound websocket invalid websocket client id '%s'\n",
+                       ast_sorcery_object_get_id(owc), var->value);
+               return -1;
+       }
+       if (ast_string_field_set(owc, websocket_client_id, var->value) != 0) {
                return -1;
        }
+       return 0;
+}
 
+static int outbound_websocket_websocket_client_id_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+       const struct ari_conf_outbound_websocket *owc = obj;
+       if (!owc->websocket_client) {
+               return -1;
+       }
+       *buf = ast_strdup(owc->websocket_client_id);
        return 0;
 }
 
-/*! \brief Parses the ast_ari_password_format enum from a config file */
-static int password_format_handler(const struct aco_option *opt,
-       struct ast_variable *var, void *obj)
+/*!
+ * \brief Callback to initialize an outbound websocket object
+ * \retval 0 on success
+ * \retval CMP_MATCH on error which will cause the object to be removed
+ */
+static int outbound_websocket_apply(const struct ast_sorcery *sorcery, void *obj)
 {
-       struct ast_ari_conf_user *user = obj;
+       struct ari_conf_outbound_websocket *owc = obj;
+       const char *id = ast_sorcery_object_get_id(owc);
+       int res = 0;
 
-       if (strcasecmp(var->value, "plain") == 0) {
-               user->password_format = ARI_PASSWORD_FORMAT_PLAIN;
-       } else if (strcasecmp(var->value, "crypt") == 0) {
-               user->password_format = ARI_PASSWORD_FORMAT_CRYPT;
+       ast_debug(3, "%s: Initializing outbound websocket\n", id);
+
+       if (ast_strlen_zero(owc->apps)) {
+               ast_log(LOG_WARNING, "%s: Outbound websocket missing apps\n", id);
+               res = -1;
        } else {
-               return -1;
+               char *apps = ast_strdupa(owc->apps);
+               char *app;
+               while ((app = ast_strsep(&apps, ',', AST_STRSEP_STRIP))) {
+
+                       if (ast_strlen_zero(app)) {
+                               ast_log(LOG_WARNING, "%s: Outbound websocket has empty app\n", id);
+                               res = -1;
+                       }
+                       if (strlen(app) > ARI_MAX_APP_NAME_LEN) {
+                               ast_log(LOG_WARNING, "%s: Outbound websocket app '%s' > %d characters\n",
+                                       id, app, (int)ARI_MAX_APP_NAME_LEN);
+                               res = -1;
+                       }
+
+               }
        }
 
-       return 0;
+       if (ast_strlen_zero(owc->local_ari_user)) {
+               ast_log(LOG_WARNING, "%s: Outbound websocket missing local_ari_user\n", id);
+               res = -1;
+       }
+
+       if (res != 0) {
+               ast_log(LOG_WARNING, "%s: Outbound websocket configuration failed\n", id);
+       } else {
+               ast_debug(3, "%s: Outbound websocket configuration succeeded\n", id);
+       }
+
+       /* Reminder: If res is -1, the config will be discarded. */
+       return res;
 }
 
-/*! \brief Destructor for \ref ast_ari_conf_user */
-static void user_dtor(void *obj)
+enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id)
 {
-       struct ast_ari_conf_user *user = obj;
-       ast_debug(3, "Disposing of user %s\n", user->username);
-       ast_free(user->username);
+       RAII_VAR(struct outbound_websocket_state *, state, NULL, ao2_cleanup);
+
+       state = ao2_find(owc_states, id, OBJ_SEARCH_KEY);
+       return state ? state->invalid_fields : ARI_OWC_FIELD_NONE;
 }
 
-/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */
-static void *user_alloc(const char *cat)
+static int outbound_websocket_validate_cb(void *obj, void *args, int flags)
 {
-       RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
+       struct ari_conf_outbound_websocket *owc = obj;
+       struct ari_conf_outbound_websocket *other_owc = NULL;
+       RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup);
+       struct ao2_iterator it;
+       const char *id = ast_sorcery_object_get_id(owc);
+       struct ast_vector_string apps = { 0, };
+       struct ari_conf_user *user = NULL;
+       struct outbound_websocket_state *state = NULL;
+       int res = 0;
+
+       ast_debug(2, "%s: Validating outbound websocket\n", id);
+
+       owcs = ari_conf_get_owcs();
+       if (!owcs || ao2_container_count(owcs) == 0) {
+               return 0;
+       }
 
-       if (!cat) {
-               return NULL;
+       if (AST_VECTOR_INIT(&apps, 5) != 0) {
+               return 0;
        }
 
-       ast_debug(3, "Allocating user %s\n", cat);
+       res = ast_vector_string_split(&apps, owc->apps, ",", 0, NULL);
+       if (res != 0) {
+               ast_log(LOG_WARNING, "%s: Outbound websocket apps '%s' failed to split\n",
+                       id, owc->apps);
+               AST_VECTOR_RESET(&apps, ast_free_ptr);
+               AST_VECTOR_FREE(&apps);
+               return 0;
+       }
 
-       user = ao2_alloc_options(sizeof(*user), user_dtor,
-               AO2_ALLOC_OPT_LOCK_NOLOCK);
+       state = ao2_find(owc_states, id, OBJ_SEARCH_KEY);
+       if (!state) {
+               state = ao2_alloc(sizeof(*state) + strlen(id) + 1, NULL);
+               if (!state) {
+                       ast_log(LOG_WARNING, "%s: Outbound websocket state allocation failed\n", id);
+                       AST_VECTOR_RESET(&apps, ast_free_ptr);
+                       AST_VECTOR_FREE(&apps);
+                       return 0;
+               }
+               strcpy(state->id, id); /* Safe */
+               ast_debug(3, "%s: Created new outbound websocket state\n", id);
+       } else {
+               ast_debug(3, "%s: Outbound websocket state already exists\n", id);
+       }
+       state->invalid_fields = ARI_OWC_FIELD_NONE;
+
+       /*
+        * Check all other owcs to make sure we don't have
+        * duplicate apps.
+        */
+       it = ao2_iterator_init(owcs, 0);
+       while ((other_owc = ao2_iterator_next(&it))) {
+               const char *other_id = ast_sorcery_object_get_id(other_owc);
+               if (!ast_strings_equal(other_id, id)) {
+                       int i = 0;
+                       for (i = 0; i < AST_VECTOR_SIZE(&apps); i++) {
+                               const char *app = AST_VECTOR_GET(&apps, i);
+                               if (ast_in_delimited_string(app, other_owc->apps, ',')) {
+                                       ast_log(LOG_WARNING,
+                                               "%s: Outbound websocket '%s' is also trying to register app '%s'\n",
+                                               id, other_id, app);
+                                       state->invalid_fields |= ARI_OWC_FIELD_APPS;
+                               }
+                       }
+               }
+               ao2_cleanup(other_owc);
+               if (owc->invalid) {
+                       break;
+               }
+       }
+       ao2_iterator_destroy(&it);
+       AST_VECTOR_RESET(&apps, ast_free_ptr);
+       AST_VECTOR_FREE(&apps);
+
+       /*
+        * Check that the local_ari_user is valid and has
+        * a plain text password.
+        */
+       user = ast_sorcery_retrieve_by_id(sorcery, "user", owc->local_ari_user);
        if (!user) {
+               ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' not found\n",
+                       id, owc->local_ari_user);
+               state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+       } else {
+               if (user->password_format != ARI_PASSWORD_FORMAT_PLAIN) {
+                       ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' password MUST be plain text\n",
+                               id, owc->local_ari_user);
+                       state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+               }
+               if (ast_string_field_set(owc, local_ari_password, user->password) != 0) {
+                       state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+               }
+       }
+       ao2_cleanup(user);
+
+       /*
+        * The container has AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE set so
+        * this is an insert or replace operation.
+        */
+       ao2_link(owc_states, state);
+       ao2_cleanup(state);
+
+       return 0;
+}
+
+static int outbound_websocket_state_cleanup(void *obj, void *arg, int flags)
+{
+       struct outbound_websocket_state *state = obj;
+       struct ari_conf_outbound_websocket *owc = ari_conf_get_owc(state->id);
+       int res = 0;
+
+       if (!owc) {
+               ast_debug(3, "%s: Cleaning up orphaned outbound websocket state\n", state->id);
+               res = CMP_MATCH;
+       }
+       ao2_cleanup(owc);
+
+       return res;
+}
+
+static void outbound_websockets_validate(const char *name)
+{
+       RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup);
+
+       ao2_callback(owcs, OBJ_NODATA, outbound_websocket_validate_cb, NULL);
+       /* Clean up any states whose configs have disappeared. */
+       ao2_callback(owc_states, OBJ_NODATA | OBJ_UNLINK,
+               outbound_websocket_state_cleanup, NULL);
+}
+
+struct ao2_container *ari_conf_get_owcs(void)
+{
+       if (!sorcery) {
                return NULL;
        }
 
-       user->username = ast_strdup(cat);
-       if (!user->username) {
+       return ast_sorcery_retrieve_by_fields(sorcery, "outbound_websocket",
+               AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+}
+
+struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id)
+{
+       if (!sorcery) {
                return NULL;
        }
 
-       ao2_ref(user, +1);
-       return user;
+       return ast_sorcery_retrieve_by_id(sorcery, "outbound_websocket", id);
 }
 
-/*! \brief Sorting function for use with red/black tree */
-static int user_sort_cmp(const void *obj_left, const void *obj_right, int flags)
+struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app(
+       const char *app_name, unsigned int ws_type)
 {
-       const struct ast_ari_conf_user *user_left = obj_left;
-       const struct ast_ari_conf_user *user_right = obj_right;
-       const char *key_right = obj_right;
-       int cmp;
+       struct ari_conf_outbound_websocket *owc = NULL;
+       struct ao2_container *owcs = NULL;
+       struct ao2_iterator i;
 
-       switch (flags & OBJ_SEARCH_MASK) {
-       case OBJ_SEARCH_OBJECT:
-               key_right = user_right->username;
-               /* Fall through */
-       case OBJ_SEARCH_KEY:
-               cmp = strcasecmp(user_left->username, key_right);
-               break;
-       case OBJ_SEARCH_PARTIAL_KEY:
-               /*
-                * We could also use a partial key struct containing a length
-                * so strlen() does not get called for every comparison instead.
-                */
-               cmp = strncasecmp(user_left->username, key_right, strlen(key_right));
-               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->websocket_client->connection_type & ws_type
+                       && ast_in_delimited_string(app_name, owc->apps, ',')) {
+                       ast_debug(3, "%s: Found correct websocket type for apps '%s' for app '%s'\n",
+                               id, owc->apps, app_name);
+                       break;
+               }
+               ao2_cleanup(owc);
+       }
+       ao2_iterator_destroy(&i);
+       ao2_cleanup(owcs);
+       if (!owc) {
+               ast_debug(3, "No outbound websocket found for app '%s'\n", app_name);
+       }
+
+       return owc;
+}
+
+const char *ari_websocket_type_to_str(enum ast_websocket_type type)
+{
+       switch (type) {
+       case AST_WS_TYPE_CLIENT_PERSISTENT:
+               return "persistent";
+       case AST_WS_TYPE_CLIENT_PER_CALL:
+               return "per_call";
+       case AST_WS_TYPE_CLIENT_PER_CALL_CONFIG:
+               return "per_call_config";
+       case AST_WS_TYPE_INBOUND:
+               return "inbound";
+       case AST_WS_TYPE_ANY:
+               return "any";
        default:
-               /* Sort can only work on something with a full or partial key. */
-               ast_assert(0);
-               cmp = 0;
-               break;
+               return "unknown";
        }
-       return cmp;
 }
 
-/*! \brief \ref aco_type item_find function */
-static void *user_find(struct ao2_container *tmp_container, const char *cat)
+enum ari_conf_owc_fields ari_conf_owc_detect_changes(
+       struct ari_conf_outbound_websocket *old_owc,
+       struct ari_conf_outbound_websocket *new_owc)
 {
-       if (!cat) {
-               return NULL;
+       enum ari_conf_owc_fields changed = ARI_OWC_FIELD_NONE;
+       const char *new_id = ast_sorcery_object_get_id(new_owc);
+       RAII_VAR(struct ast_variable *, changes, NULL, ast_variables_destroy);
+       struct ast_variable *v = NULL;
+       int res = 0;
+       int changes_found = 0;
+
+       ast_debug(2, "%s: Detecting changes\n", new_id);
+
+       res = ast_sorcery_diff(sorcery, old_owc, new_owc, &changes);
+       if (res != 0) {
+               ast_log(LOG_WARNING, "%s: Failed to create changeset\n", new_id);
+               return ARI_OWC_FIELD_NONE;
+       }
+
+       for (v = changes; v; v = v->next) {
+               changes_found = 1;
+               ast_debug(2, "%s: %s changed to %s\n", new_id, v->name, v->value);
+               if (ast_strings_equal(v->name, "apps")) {
+                       changed |= ARI_OWC_FIELD_APPS;
+               } else if (ast_strings_equal(v->name, "subscribe_all")) {
+                       changed |= ARI_OWC_FIELD_SUBSCRIBE_ALL;
+               } else if (ast_strings_equal(v->name, "local_ari_user")) {
+                       changed |= ARI_OWC_FIELD_LOCAL_ARI_USER;
+               } else if (ast_strings_equal(v->name, "local_ari_password")) {
+                       changed |= ARI_OWC_FIELD_LOCAL_ARI_PASSWORD;
+               } else {
+                       ast_debug(2, "%s: Unknown change %s\n", new_id, v->name);
+               }
+       }
+       if (!changes_found) {
+               ast_debug(2, "%s: No changes found %p %p\n", new_id,
+                       old_owc->websocket_client,new_owc->websocket_client);
        }
 
-       return ao2_find(tmp_container, cat, OBJ_SEARCH_KEY);
+       changed |= ast_websocket_client_get_field_diff(
+               old_owc->websocket_client, new_owc->websocket_client);
+
+       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);
 
-       ast_string_field_free_memory(general);
+       if (!general) {
+               return NULL;
+       }
+
+       if (ast_string_field_init(general, 64) != 0) {
+               return NULL;
+       }
+
+       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];
+       );
+
+       ast_debug(2, "Initializing general config\n");
+
+       parse = ast_strdupa(general->channelvars);
+       AST_STANDARD_APP_ARGS(args, parse);
 
-       ao2_cleanup(cfg->general);
-       ao2_cleanup(cfg->users);
+       ast_channel_set_ari_vars(args.argc, args.vars);
+       return 0;
 }
 
-/*! \brief Allocate an \ref ast_ari_conf for config parsing */
-static void *conf_alloc(void)
+/*! \brief Encoding format handler converts from boolean to enum. */
+static int general_pretty_from_str(const struct aco_option *opt,
+       struct ast_variable *var, void *obj)
 {
-       struct ast_ari_conf *cfg;
+       struct ari_conf_general *general = obj;
+
+       general->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT;
 
-       cfg = ao2_alloc_options(sizeof(*cfg), conf_destructor,
-               AO2_ALLOC_OPT_LOCK_NOLOCK);
-       if (!cfg) {
+       return 0;
+}
+
+struct ari_conf_general* ari_conf_get_general(void)
+{
+       if (!sorcery) {
                return NULL;
        }
 
-       cfg->general = ao2_alloc_options(sizeof(*cfg->general), conf_general_dtor,
-               AO2_ALLOC_OPT_LOCK_NOLOCK);
+       return ast_sorcery_retrieve_by_id(sorcery, "general", "general");
+}
+
+static int general_pretty_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+       const struct ari_conf_general *general = obj;
 
-       cfg->users = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
-               AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, user_sort_cmp, NULL);
+       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;
+       }
+
+       return 0;
+}
 
-struct ast_ari_conf *ast_ari_config_get(void)
+/*! \brief Parses the ast_ari_password_format enum from a config file */
+static int user_password_format_from_str(const struct aco_option *opt,
+       struct ast_variable *var, void *obj)
 {
-       struct ast_ari_conf *res = ao2_global_obj_ref(confs);
-       if (!res) {
-               ast_log(LOG_ERROR,
-                       "Error obtaining config from " CONF_FILENAME "\n");
+       struct ari_conf_user *user = obj;
+
+       if (strcasecmp(var->value, "plain") == 0) {
+               user->password_format = ARI_PASSWORD_FORMAT_PLAIN;
+       } else if (strcasecmp(var->value, "crypt") == 0) {
+               user->password_format = ARI_PASSWORD_FORMAT_CRYPT;
+       } else {
+               return -1;
        }
-       return res;
+
+       return 0;
 }
 
-struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
-       const char *password)
+static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf)
 {
-       RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
-       RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
-       int is_valid = 0;
+       const struct ari_conf_user *user = obj;
+
+       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 +613,184 @@ struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
        }
 
        if (!is_valid) {
-               return NULL;
+               ao2_cleanup(user);
+               user = NULL;
        }
 
-       ao2_ref(user, +1);
        return user;
 }
 
-/*! \brief Callback to validate a user object */
-static int validate_user_cb(void *obj, void *arg, int flags)
+int ari_sorcery_observer_add(const char *object_type,
+       const struct ast_sorcery_observer *callbacks)
 {
-       struct ast_ari_conf_user *user = obj;
-
-       if (ast_strlen_zero(user->password)) {
-               ast_log(LOG_WARNING, "User '%s' missing password\n",
-                       user->username);
+       if (!sorcery) {
+               return -1;
        }
+       return ast_sorcery_observer_add(sorcery, object_type, callbacks);
+}
 
+int ari_sorcery_observer_remove(const char *object_type,
+       const struct ast_sorcery_observer *callbacks)
+{
+       if (!sorcery) {
+               return -1;
+       }
+       ast_sorcery_observer_remove(sorcery, object_type, callbacks);
        return 0;
 }
 
-/*! \brief Load (or reload) configuration. */
-static int process_config(int reload)
+static struct ast_sorcery_observer observer_callbacks = {
+       .loaded = outbound_websockets_validate,
+};
+
+static void ws_client_load(const char *name)
 {
-       RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup);
+       ast_sorcery_force_reload_object(sorcery, "outbound_websocket");
+}
 
-       switch (aco_process_config(&cfg_info, reload)) {
-       case ACO_PROCESS_ERROR:
+static struct ast_sorcery_observer ws_client_observer_callbacks = {
+       .loaded = ws_client_load,
+};
+
+AO2_STRING_FIELD_HASH_FN(outbound_websocket_state, id)
+AO2_STRING_FIELD_CMP_FN(outbound_websocket_state, id)
+
+static int ari_conf_init(void)
+{
+       int res = 0;
+       ast_debug(2, "Initializing ARI configuration\n");
+
+       owc_states = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX,
+               AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, OWC_STATES_BUCKETS,
+               outbound_websocket_state_hash_fn, NULL,
+               outbound_websocket_state_cmp_fn);
+       if (!owc_states) {
+               ast_log(LOG_ERROR, "Failed to allocate outbound websocket states\n");
                return -1;
-       case ACO_PROCESS_OK:
-       case ACO_PROCESS_UNCHANGED:
-               break;
        }
 
-       conf = ast_ari_config_get();
-       if (!conf) {
-               ast_assert(0); /* We just configured; it should be there */
+       if (!(sorcery = ast_sorcery_open())) {
+               ast_log(LOG_ERROR, "Failed to open sorcery\n");
                return -1;
        }
 
-       if (conf->general->enabled) {
-               if (ao2_container_count(conf->users) == 0) {
-                       ast_log(LOG_ERROR, "No configured users for ARI\n");
-               } else {
-                       ao2_callback(conf->users, OBJ_NODATA, validate_user_cb, NULL);
-               }
+       ast_sorcery_apply_default(sorcery, "general", "config",
+               "ari.conf,criteria=type=general,single_object=yes,explicit_name=general");
+       ast_sorcery_apply_default(sorcery, "user", "config",
+               "ari.conf,criteria=type=user");
+       ast_sorcery_apply_default(sorcery, "outbound_websocket", "config",
+               "ari.conf,criteria=type=outbound_websocket");
+
+       if (ast_sorcery_object_register(sorcery, "general", general_alloc, NULL, general_apply)) {
+               ast_log(LOG_ERROR, "Failed to register ARI general object with sorcery\n");
+               ast_sorcery_unref(sorcery);
+               sorcery = NULL;
+               return -1;
        }
 
-       return 0;
-}
+       if (ast_sorcery_object_register(sorcery, "user", user_alloc, NULL, user_apply)) {
+               ast_log(LOG_ERROR, "Failed to register ARI user object with sorcery\n");
+               ast_sorcery_unref(sorcery);
+               sorcery = NULL;
+               return -1;
+       }
 
-#define MAX_VARS 128
+       if (ast_sorcery_object_register(sorcery, "outbound_websocket", outbound_websocket_alloc,
+               NULL, outbound_websocket_apply)) {
+               ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket object with sorcery\n");
+               ast_sorcery_unref(sorcery);
+               sorcery = NULL;
+               return -1;
+       }
 
-static int channelvars_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
-{
-       char *parse = NULL;
-       AST_DECLARE_APP_ARGS(args,
-               AST_APP_ARG(vars)[MAX_VARS];
-       );
+       if (ast_sorcery_observer_add(sorcery, "outbound_websocket", &observer_callbacks)) {
+               ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket observer with sorcery\n");
+               ast_sorcery_unref(sorcery);
+               sorcery = NULL;
+               return -1;
+       }
 
-       parse = ast_strdupa(var->value);
-       AST_STANDARD_APP_ARGS(args, parse);
+       ast_sorcery_object_field_register_nodoc(sorcery, "general", "type", "", OPT_NOOP_T, 0, 0);
+       ast_sorcery_register_sf(general, ari_conf_general, auth_realm, auth_realm, "Asterisk REST Interface");
+       ast_sorcery_register_sf(general, ari_conf_general, allowed_origins, allowed_origins, "");
+       ast_sorcery_register_sf(general, ari_conf_general, channelvars, channelvars, "");
+       ast_sorcery_register_bool(general, ari_conf_general, enabled, enabled, "yes");
+       ast_sorcery_register_cust(general, pretty, "no");
+       ast_sorcery_register_int(general, ari_conf_general, websocket_write_timeout, write_timeout,
+               AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT);
+
+
+       ast_sorcery_object_field_register(sorcery, "user", "type", "", OPT_NOOP_T, 0, 0);
+       ast_sorcery_register_sf(user, ari_conf_user, password, password, "");
+       ast_sorcery_register_bool(user, ari_conf_user, read_only, read_only, "no");
+       ast_sorcery_register_cust(user, password_format, "plain");
+
+       ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0);
+       ast_sorcery_register_cust(outbound_websocket, websocket_client_id, "");
+       ast_sorcery_register_sf(outbound_websocket, ari_conf_outbound_websocket, apps, apps, "");
+       ast_sorcery_register_sf(outbound_websocket, ari_conf_outbound_websocket, local_ari_user, local_ari_user, "");
+       ast_sorcery_register_bool(outbound_websocket, ari_conf_outbound_websocket, subscribe_all, subscribe_all, "no");
+
+       res = ast_websocket_client_observer_add(&ws_client_observer_callbacks);
+       if (res < 0) {
+               ast_log(LOG_WARNING, "Failed to register websocket client observer\n");
+               ast_sorcery_unref(sorcery);
+               sorcery = NULL;
+               return -1;
+       }
 
-       ast_channel_set_ari_vars(args.argc, args.vars);
        return 0;
 }
 
-int ast_ari_config_init(void)
+int ari_conf_load(enum ari_conf_load_flags flags)
 {
-       if (aco_info_init(&cfg_info)) {
-               aco_info_destroy(&cfg_info);
+       void (*loader)(const struct ast_sorcery *sorcery, const char *type);
+       const char *msg_prefix;
+
+       if (flags & ARI_CONF_RELOAD) {
+               loader = ast_sorcery_reload_object;
+               msg_prefix= "Reloading";
+               ast_websocket_client_reload();
+       } else {
+               loader = ast_sorcery_load_object;
+               msg_prefix= "Loading";
+       }
+
+       if (flags & ARI_CONF_INIT) {
+               if (ari_conf_init() != 0) {
+                       ast_log(LOG_ERROR, "Failed to initialize ARI configuration\n");
+                       return -1;
+               }
+       }
+
+       if (!sorcery) {
+               ast_log(LOG_ERROR, "ARI configuration not initialized\n");
                return -1;
        }
 
-       /* 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_websocket_client_observer_remove(&ws_client_observer_callbacks);
+
+       ast_sorcery_unref(sorcery);
+       sorcery = NULL;
+       ao2_cleanup(owc_states);
 }
index 08a633894ab863024ea8995c39af356b137afd02..2a5850f468547a3071fb16223336e1ee85fd8d8d 100644 (file)
 
 #include "asterisk/http.h"
 #include "asterisk/json.h"
+#include "asterisk/md5.h"
+#include "asterisk/sorcery.h"
 #include "asterisk/stringfields.h"
+#include "asterisk/websocket_client.h"
+#include "ari_websockets.h"
+
 
 /*! @{ */
 
  * \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_WEBSOCKET_CONNECTION_ID = (1 << AST_WS_CLIENT_FIELD_USER_START),
+       ARI_OWC_FIELD_APPS =                    (1 << (AST_WS_CLIENT_FIELD_USER_START + 1)),
+       ARI_OWC_FIELD_LOCAL_ARI_USER =          (1 << (AST_WS_CLIENT_FIELD_USER_START + 2)),
+       ARI_OWC_FIELD_LOCAL_ARI_PASSWORD =      (1 << (AST_WS_CLIENT_FIELD_USER_START + 3)),
+       ARI_OWC_FIELD_SUBSCRIBE_ALL =           (1 << (AST_WS_CLIENT_FIELD_USER_START + 4)),
+       ARI_OWC_NEEDS_RECONNECT = AST_WS_CLIENT_NEEDS_RECONNECT
+       | ARI_OWC_FIELD_WEBSOCKET_CONNECTION_ID | ARI_OWC_FIELD_LOCAL_ARI_USER
+       | ARI_OWC_FIELD_LOCAL_ARI_PASSWORD,
+       ARI_OWC_NEEDS_REREGISTER = ARI_OWC_FIELD_APPS | ARI_OWC_FIELD_SUBSCRIBE_ALL,
+};
+
+struct ari_conf_outbound_websocket {
+       SORCERY_OBJECT(details);
+       AST_DECLARE_STRING_FIELDS(
+               AST_STRING_FIELD(websocket_client_id);  /*!< The ID of the websocket client to use */
+               AST_STRING_FIELD(apps);          /*!< Stasis apps using this connection */
+               AST_STRING_FIELD(local_ari_user);/*!< The ARI user to act as */
+               AST_STRING_FIELD(local_ari_password);   /*!< The password for the ARI user */
+       );
+       int invalid;                         /*!< Invalid configuration */
+       int subscribe_all;                   /*!< Subscribe to all events */
+       struct ast_websocket_client *websocket_client; /*!< The websocket client */
+};
+
 /*!
- * \brief Initialize the ARI configuration
+ * \brief Detect changes between two outbound websocket configurations.
+ *
+ * \param old_owc The old outbound websocket configuration.
+ * \param new_owc The new outbound websocket configuration.
+ * \return A bitmask of changed fields.
  */
-int ast_ari_config_init(void);
+enum ari_conf_owc_fields ari_conf_owc_detect_changes(
+       struct ari_conf_outbound_websocket *old_owc,
+       struct ari_conf_outbound_websocket *new_owc);
 
 /*!
- * \brief Reload the ARI configuration
+ * \brief Get the outbound websocket configuration for a Stasis app.
+ *
+ * \param app_name The application name to search for.
+ * \param ws_type An OR'd list of ari_websocket_types or ARI_WS_TYPE_ANY.
+ *
+ * \retval ARI outbound websocket configuration object.
+ * \retval NULL if not found.
  */
-int ast_ari_config_reload(void);
+struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app(
+       const char *app_name, unsigned int ws_type);
+
+enum ari_conf_load_flags {
+       ARI_CONF_INIT =              (1 << 0), /*!< Initialize sorcery */
+       ARI_CONF_RELOAD =            (1 << 1), /*!< Reload sorcery */
+       ARI_CONF_LOAD_GENERAL = (1 << 2), /*!< Load general config */
+       ARI_CONF_LOAD_USER =    (1 << 3), /*!< Load user config */
+       ARI_CONF_LOAD_OWC =     (1 << 4), /*!< Load outbound websocket config */
+       ARI_CONF_LOAD_ALL =     (         /*!< Load all configs */
+               ARI_CONF_LOAD_GENERAL
+               | ARI_CONF_LOAD_USER
+               | ARI_CONF_LOAD_OWC),
+};
 
 /*!
- * \brief Destroy the ARI configuration
+ * \brief (Re)load the ARI configuration
  */
-void ast_ari_config_destroy(void);
+int ari_conf_load(enum ari_conf_load_flags flags);
 
 /*!
- * \brief Get the current ARI configuration.
- *
- * This is an immutable object, so don't modify it. It is AO2 managed, so
- * ao2_cleanup() when you're done with it.
- *
- * \return ARI configuration object.
- * \retval NULL on error.
+ * \brief Destroy the ARI configuration
  */
-struct ast_ari_conf *ast_ari_config_get(void);
+void ari_conf_destroy(void);
+
+struct ari_conf_general* ari_conf_get_general(void);
+struct ao2_container *ari_conf_get_users(void);
+struct ari_conf_user *ari_conf_get_user(const char *username);
+struct ao2_container *ari_conf_get_owcs(void);
+struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id);
+enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id);
+const char *ari_websocket_type_to_str(enum ast_websocket_type type);
+int ari_sorcery_observer_add(const char *object_type,
+       const struct ast_sorcery_observer *callbacks);
+int ari_sorcery_observer_remove(const char *object_type,
+       const struct ast_sorcery_observer *callbacks);
 
 /*!
  * \brief Validated a user's credentials.
@@ -138,7 +184,7 @@ struct ast_ari_conf *ast_ari_config_get(void);
  * \return User object.
  * \retval NULL if username or password is invalid.
  */
-struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username,
+struct ari_conf_user *ari_conf_validate_user(const char *username,
        const char *password);
 
 /*! @} */
index 56d8c2815478e1202d00846edf8a67f33b422e52..cb0a7248df7f98b26cab046b0e87538298a6cf40 100644 (file)
 /*** MODULEINFO
        <depend type="module">res_http_websocket</depend>
        <depend type="module">res_stasis</depend>
+       <depend type="module">res_websocket_client</depend>
        <support_level>core</support_level>
  ***/
 
-/*** DOCUMENTATION
-       <configInfo name="res_ari" language="en_US">
-               <synopsis>HTTP binding for the Stasis API</synopsis>
-               <configFile name="ari.conf">
-                       <configObject name="general">
-                               <since>
-                                       <version>12.0.0</version>
-                               </since>
-                               <synopsis>General configuration settings</synopsis>
-                               <configOption name="enabled">
-                                       <since>
-                                               <version>12.0.0</version>
-                                       </since>
-                                       <synopsis>Enable/disable the ARI module</synopsis>
-                                       <description>
-                                               <para>This option enables or disables the ARI module.</para>
-                                               <note>
-                                                       <para>ARI uses Asterisk's HTTP server, which must also be enabled in <filename>http.conf</filename>.</para>
-                                               </note>
-                                       </description>
-                                       <see-also>
-                                               <ref type="filename">http.conf</ref>
-                                               <ref type="link">https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/</ref>
-                                       </see-also>
-                               </configOption>
-                               <configOption name="websocket_write_timeout" default="100">
-                                       <since>
-                                               <version>11.11.0</version>
-                                               <version>12.4.0</version>
-                                       </since>
-                                       <synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis>
-                                       <description>
-                                               <para>If a websocket connection accepts input slowly, the timeout
-                                               for writes to it can be increased to keep it from being disconnected.
-                                               Value is in milliseconds.</para>
-                                       </description>
-                               </configOption>
-                               <configOption name="pretty">
-                                       <since>
-                                               <version>12.0.0</version>
-                                       </since>
-                                       <synopsis>Responses from ARI are formatted to be human readable</synopsis>
-                               </configOption>
-                               <configOption name="auth_realm">
-                                       <since>
-                                               <version>12.0.0</version>
-                                       </since>
-                                       <synopsis>Realm to use for authentication. Defaults to Asterisk REST Interface.</synopsis>
-                               </configOption>
-                               <configOption name="allowed_origins">
-                                       <since>
-                                               <version>12.0.0</version>
-                                       </since>
-                                       <synopsis>Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins.</synopsis>
-                               </configOption>
-                               <configOption name="channelvars">
-                                       <since>
-                                               <version>14.2.0</version>
-                                       </since>
-                                       <synopsis>Comma separated list of channel variables to display in channel json.</synopsis>
-                               </configOption>
-                       </configObject>
-
-                       <configObject name="user">
-                               <since>
-                                       <version>12.0.0</version>
-                               </since>
-                               <synopsis>Per-user configuration settings</synopsis>
-                               <configOption name="type">
-                                       <since>
-                                               <version>13.30.0</version>
-                                               <version>16.7.0</version>
-                                               <version>17.1.0</version>
-                                       </since>
-                                       <synopsis>Define this configuration section as a user.</synopsis>
-                                       <description>
-                                               <enumlist>
-                                                       <enum name="user"><para>Configure this section as a <replaceable>user</replaceable></para></enum>
-                                               </enumlist>
-                                       </description>
-                               </configOption>
-                               <configOption name="read_only">
-                                       <since>
-                                               <version>13.30.0</version>
-                                               <version>16.7.0</version>
-                                               <version>17.1.0</version>
-                                       </since>
-                                       <synopsis>When set to yes, user is only authorized for read-only requests</synopsis>
-                               </configOption>
-                               <configOption name="password">
-                                       <since>
-                                               <version>13.30.0</version>
-                                               <version>16.7.0</version>
-                                               <version>17.1.0</version>
-                                       </since>
-                                       <synopsis>Crypted or plaintext password (see password_format)</synopsis>
-                               </configOption>
-                               <configOption name="password_format">
-                                       <since>
-                                               <version>12.0.0</version>
-                                       </since>
-                                       <synopsis>password_format may be set to plain (the default) or crypt. When set to crypt, crypt(3) is used to validate the password. A crypted password can be generated using mkpasswd -m sha-512. When set to plain, the password is in plaintext</synopsis>
-                               </configOption>
-                       </configObject>
-               </configFile>
-       </configInfo>
-***/
-
 #include "asterisk.h"
 
 #include "ari/internal.h"
 /*! \brief Helper function to check if module is enabled. */
 static int is_enabled(void)
 {
-       RAII_VAR(struct ast_ari_conf *, cfg, ast_ari_config_get(), ao2_cleanup);
-       return cfg && cfg->general && cfg->general->enabled;
+       RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
+       return general && general->enabled;
 }
 
 /*! Lock for \ref root_handler */
@@ -389,9 +282,9 @@ static void add_allow_header(struct stasis_rest_handlers *handler,
 
 static int origin_allowed(const char *origin)
 {
-       RAII_VAR(struct ast_ari_conf *, cfg, ast_ari_config_get(), ao2_cleanup);
+       RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
 
-       char *allowed = ast_strdupa(cfg->general->allowed_origins);
+       char *allowed = ast_strdupa(general ? general->allowed_origins : "");
        char *current;
 
        while ((current = strsep(&allowed, ","))) {
@@ -555,7 +448,7 @@ static void handle_options(struct stasis_rest_handlers *handler,
  * \return User object for the authenticated user.
  * \retval NULL if authentication failed.
  */
-static struct ast_ari_conf_user *authenticate_api_key(const char *api_key)
+static struct ari_conf_user *authenticate_api_key(const char *api_key)
 {
        RAII_VAR(char *, copy, NULL, ast_free);
        char *username;
@@ -572,7 +465,7 @@ static struct ast_ari_conf_user *authenticate_api_key(const char *api_key)
                return NULL;
        }
 
-       return ast_ari_config_validate_user(username, password);
+       return ari_conf_validate_user(username, password);
 }
 
 /*!
@@ -583,7 +476,7 @@ static struct ast_ari_conf_user *authenticate_api_key(const char *api_key)
  * \return User object for the authenticated user.
  * \retval NULL if authentication failed.
  */
-static struct ast_ari_conf_user *authenticate_user(struct ast_variable *get_params,
+static struct ari_conf_user *authenticate_user(struct ast_variable *get_params,
        struct ast_variable *headers)
 {
        RAII_VAR(struct ast_http_auth *, http_auth, NULL, ao2_cleanup);
@@ -592,7 +485,7 @@ static struct ast_ari_conf_user *authenticate_user(struct ast_variable *get_para
        /* HTTP Basic authentication */
        http_auth = ast_http_get_auth(headers);
        if (http_auth) {
-               return ast_ari_config_validate_user(http_auth->userid,
+               return ari_conf_validate_user(http_auth->userid,
                        http_auth->password);
        }
 
@@ -642,8 +535,8 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
        struct stasis_rest_handlers *handler = NULL;
        struct stasis_rest_handlers *wildcard_handler = NULL;
        RAII_VAR(struct ast_variable *, path_vars, NULL, ast_variables_destroy);
-       RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup);
-       RAII_VAR(struct ast_ari_conf *, conf, ast_ari_config_get(), ao2_cleanup);
+       RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
+       RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
 
        char *path = ast_strdupa(uri);
        char *path_segment = NULL;
@@ -651,7 +544,7 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
        SCOPE_ENTER(3, "Request: %s %s, path:%s\n", ast_get_http_method(method), uri, path);
 
 
-       if (!conf || !conf->general) {
+       if (!general) {
                if (ser && source == ARI_INVOKE_SOURCE_REST) {
                        ast_http_request_close_on_completion(ser);
                }
@@ -679,7 +572,7 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
                 */
                ast_str_append(&response->headers, 0,
                        "WWW-Authenticate: Basic realm=\"%s\"\r\n",
-                       conf->general->auth_realm);
+                       general->auth_realm);
                SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n",
                        response->response_code, response->response_text);
        } else if (!ast_fully_booted) {
@@ -1014,9 +907,8 @@ static void process_cors_request(struct ast_variable *headers,
 
 enum ast_json_encoding_format ast_ari_json_format(void)
 {
-       RAII_VAR(struct ast_ari_conf *, cfg, NULL, ao2_cleanup);
-       cfg = ast_ari_config_get();
-       return cfg->general->format;
+       RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
+       return general ? general->format : AST_JSON_COMPACT;
 }
 
 /*!
@@ -1230,14 +1122,14 @@ static int unload_module(void)
 {
        ari_websocket_unload_module();
 
-       ast_ari_cli_unregister();
+       ari_cli_unregister();
 
        if (is_enabled()) {
                ast_debug(3, "Disabling ARI\n");
                ast_http_uri_unlink(&http_uri);
        }
 
-       ast_ari_config_destroy();
+       ari_conf_destroy();
 
        ao2_cleanup(root_handler);
        root_handler = NULL;
@@ -1272,12 +1164,33 @@ static int load_module(void)
                return AST_MODULE_LOAD_DECLINE;
        }
 
-       if (ast_ari_config_init() != 0) {
+       /*
+        * ari_websocket_load_module() needs to know if ARI is enabled
+        * globally so it needs the "general" config to be loaded but it
+        * also needs to register a sorcery object observer for
+        * "outbound_websocket" BEFORE the outbound_websocket configs are loaded.
+        * outbound_websocket in turn needs the users to be loaded so we'll
+        * initialize sorcery and load "general" and "user" configs first, then
+        * load the websocket module, then load the "outbound_websocket" configs
+        * which will fire the observers.
+        */
+       if (ari_conf_load(ARI_CONF_INIT | ARI_CONF_LOAD_GENERAL | ARI_CONF_LOAD_USER) != 0) {
+               unload_module();
+               return AST_MODULE_LOAD_DECLINE;
+       }
+
+       if (ari_websocket_load_module(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 +1202,22 @@ static int load_module(void)
                ast_debug(3, "ARI disabled\n");
        }
 
-       if (ast_ari_cli_register() != 0) {
-               unload_module();
-               return AST_MODULE_LOAD_DECLINE;
-       }
-
        return AST_MODULE_LOAD_SUCCESS;
 }
 
 static int reload_module(void)
 {
        char was_enabled = is_enabled();
+       int is_now_enabled = 0;
 
-       if (ast_ari_config_reload() != 0) {
-               return AST_MODULE_LOAD_DECLINE;
-       }
+       ari_conf_load(ARI_CONF_RELOAD | ARI_CONF_LOAD_ALL);
+
+       is_now_enabled = is_enabled();
 
-       if (was_enabled && !is_enabled()) {
+       if (was_enabled && !is_now_enabled) {
                ast_debug(3, "Disabling ARI\n");
                ast_http_uri_unlink(&http_uri);
-       } else if (!was_enabled && is_enabled()) {
+       } else if (!was_enabled && is_now_enabled) {
                ast_debug(3, "Enabling ARI\n");
                ast_http_uri_link(&http_uri);
        }
@@ -1321,6 +1230,6 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_
        .load = load_module,
        .unload = unload_module,
        .reload = reload_module,
-       .requires = "http,res_stasis,res_http_websocket",
+       .requires = "http,res_stasis,res_http_websocket,res_websocket_client",
        .load_pri = AST_MODPRI_APP_DEPEND,
 );
index ef0849fc6337e3d09046b8775219c176966fe358..fd300981b6148e0cbbb01dd24c184f73eed3b0a0 100644 (file)
                                "RecordingFinished",
                                "RecordingFailed",
                                "ApplicationMoveFailed",
+                               "ApplicationRegistered",
+                               "ApplicationUnregistered",
                                "ApplicationReplaced",
                                "BridgeCreated",
                                "BridgeDestroyed",
                                }
                        }
                },
+               "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.",