*/
const char *ast_sorcery_get_module(const struct ast_sorcery *sorcery);
+/*!
+ * \section AstSorceryConvenienceMacros Simple Sorcery Convenience Macros
+ *
+ * For simple scenarios, the following macros can be used to register
+ * common object fields. The only requirement is that your source code's
+ * definition of it's sorcery handle be named "sorcery".
+ *
+ * Example structure:
+ * \code
+ * struct my_sorcery_object {
+ * SORCERY_OBJECT(details);
+ * AST_DECLARE_STRING_FIELDS(
+ * AST_STRING_FIELD(mystring);
+ * );
+ * enum some_enum_type myenum;
+ * int myint;
+ * unsigned int myuint;
+ * int mybool;
+ * };
+ * \endcode
+ *
+ * Example object type registration:
+ * \code
+ * ast_sorcery_object_register(sorcery, "myobject", ...);
+ * \endcode
+ */
+
+/*!
+ * \brief Register a boolean field as type OPT_YESNO_T within an object.
+ * \param object The unquoted object type.
+ * \param structure The unquoted name of the structure that contains the field
+ * without the "struct" prefix.
+ * \param option The unquoted name of the option as it appears in the config file.
+ * \param field The unquoted name of the field in the structure.
+ * \param def_value The quoted default value of the field. Should be "yes" or "no"
+ *
+ * \code
+ * ast_sorcery_register_bool(myobject, my_sorcery_object, mybool, mybool, "yes");
+ * \endcode
+ */
+#define ast_sorcery_register_bool(object, structure, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ def_value, OPT_YESNO_T, 1, \
+ FLDSET(struct structure, field))
+
+/*!
+ * \internal
+ * \brief Stringify a value.
+ *
+ * Needed because the preprocessor doesn't evaluate macros before it stringifies them.
+ */
+#define _sorcery_stringify(val) #val
+
+/*!
+ * \brief Register an int field as type OPT_INT_T within an object.
+ * \param object The unquoted object type.
+ * \param structure The unquoted name of the structure that contains the field
+ * without the "struct" prefix.
+ * \param option The unquoted name of the option as it appears in the config file.
+ * \param field The unquoted name of the field in the structure.
+ * \param def_value The unquoted default value of the field.
+ *
+ * \code
+ * ast_sorcery_register_int(myobject, my_sorcery_object, myint, myint, -32);
+ * \endcode
+ */
+#define ast_sorcery_register_int(object, structure, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ _sorcery_stringify(def_value), OPT_INT_T, PARSE_IN_RANGE, \
+ FLDSET(struct structure, field), INT_MIN, INT_MAX)
+
+/*!
+ * \brief Register an unsigned int field as type OPT_UINT_T within an object.
+ * \param object The unquoted object type.
+ * \param structure The unquoted name of the structure that contains the field
+ * without the "struct" prefix.
+ * \param option The unquoted name of the option as it appears in the config file.
+ * \param field The unquoted name of the field in the structure.
+ * \param def_value The unquoted default value of the field.
+ *
+ * \code
+ * ast_sorcery_register_uint(myobject, my_sorcery_object, myint, myint, 32);
+ * \endcode
+ */
+#define ast_sorcery_register_uint(object, structure, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ _stringify(def_value), OPT_UINT_T, PARSE_IN_RANGE, \
+ FLDSET(struct structure, field), 0, UINT_MAX)
+
+/*!
+ * \brief Register a stringfield field as type OPT_STRINGFIELD_T within an object.
+ * \param object The unquoted object type.
+ * \param structure The unquoted name of the structure that contains the field
+ * without the "struct" prefix.
+ * \param option The unquoted name of the option as it appears in the config file.
+ * \param field The unquoted name of the field in the structure.
+ * \param def_value The quoted default value of the field.
+ *
+ * \code
+ * ast_sorcery_register_sf(myobject, my_sorcery_object, mystring, mystring, "");
+ * \endcode
+ */
+#define ast_sorcery_register_sf(object, structure, option, field, def_value) \
+ ast_sorcery_object_field_register(sorcery, #object, #option, \
+ def_value, OPT_STRINGFIELD_T, 0, \
+ STRFLDSET(struct structure, field))
+
+/*!
+ * \brief Register a custom field within an object.
+ * \param object The unquoted object type.
+ * \param structure The unquoted name of the structure that contains the field
+ * without the "struct" prefix.
+ * \param option The unquoted name of the option as it appears in the config file.
+ *
+ * \code
+ * ast_sorcery_register_cust(myobject, my_sorcery_object, mystring);
+ * \endcode
+ *
+ * \note
+ * You must have defined the following standard sorcery custom handler functions:
+ * \li myobject_mystring_from_str(const struct aco_option *opt, struct ast_variable *var, void *obj)
+ * \li myobject_mystring_to_str(const void *obj, const intptr_t *args, char **buf)
+ */
+#define ast_sorcery_register_cust(object, option, def_value) \
+ ast_sorcery_object_field_register_custom(sorcery, #object, #option, \
+ def_value, object ## _ ## option ## _from_str, \
+ object ## _ ## option ## _to_str, NULL, 0, 0)
+
+
#if defined(__cplusplus) || defined(c_plusplus)
}
#endif
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2025, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#ifndef _RES_WEBSOCKET_CLIENT_H
+#define _RES_WEBSOCKET_CLIENT_H
+
+#include "asterisk/http_websocket.h"
+#include "asterisk/sorcery.h"
+
+enum ast_ws_client_fields {
+ AST_WS_CLIENT_FIELD_NONE = 0,
+ AST_WS_CLIENT_FIELD_URI = (1 << 0),
+ AST_WS_CLIENT_FIELD_PROTOCOLS = (1 << 1),
+ AST_WS_CLIENT_FIELD_USERNAME = (1 << 3),
+ AST_WS_CLIENT_FIELD_PASSWORD = (1 << 4),
+ AST_WS_CLIENT_FIELD_TLS_ENABLED = (1 << 7),
+ AST_WS_CLIENT_FIELD_CA_LIST_FILE = (1 << 8),
+ AST_WS_CLIENT_FIELD_CA_LIST_PATH = (1 << 9),
+ AST_WS_CLIENT_FIELD_CERT_FILE = (1 << 10),
+ AST_WS_CLIENT_FIELD_PRIV_KEY_FILE = (1 << 11),
+ AST_WS_CLIENT_FIELD_CONNECTION_TYPE = (1 << 13),
+ AST_WS_CLIENT_FIELD_RECONNECT_INTERVAL = (1 << 14),
+ AST_WS_CLIENT_FIELD_RECONNECT_ATTEMPTS = (1 << 15),
+ AST_WS_CLIENT_FIELD_CONNECTION_TIMEOUT = (1 << 16),
+ AST_WS_CLIENT_FIELD_VERIFY_SERVER_CERT = (1 << 17),
+ AST_WS_CLIENT_FIELD_VERIFY_SERVER_HOSTNAME = (1 << 18),
+ AST_WS_CLIENT_NEEDS_RECONNECT = AST_WS_CLIENT_FIELD_URI | AST_WS_CLIENT_FIELD_PROTOCOLS
+ | AST_WS_CLIENT_FIELD_CONNECTION_TYPE
+ | AST_WS_CLIENT_FIELD_USERNAME | AST_WS_CLIENT_FIELD_PASSWORD
+ | AST_WS_CLIENT_FIELD_TLS_ENABLED | AST_WS_CLIENT_FIELD_CA_LIST_FILE
+ | AST_WS_CLIENT_FIELD_CA_LIST_PATH | AST_WS_CLIENT_FIELD_CERT_FILE
+ | AST_WS_CLIENT_FIELD_PRIV_KEY_FILE | AST_WS_CLIENT_FIELD_VERIFY_SERVER_CERT
+ | AST_WS_CLIENT_FIELD_VERIFY_SERVER_HOSTNAME,
+};
+
+/*
+ * The first 23 fields are reserved for the websocket client core.
+ */
+#define AST_WS_CLIENT_FIELD_USER_START 24
+
+struct ast_websocket_client {
+ SORCERY_OBJECT(details);
+ AST_DECLARE_STRING_FIELDS(
+ AST_STRING_FIELD(uri); /*!< Server URI */
+ AST_STRING_FIELD(protocols); /*!< Websocket protocols to use with server */
+ AST_STRING_FIELD(username); /*!< Auth user name */
+ AST_STRING_FIELD(password); /*!< Auth password */
+ AST_STRING_FIELD(ca_list_file); /*!< CA file */
+ AST_STRING_FIELD(ca_list_path); /*!< CA path */
+ AST_STRING_FIELD(cert_file); /*!< Certificate file */
+ AST_STRING_FIELD(priv_key_file); /*!< Private key file */
+ );
+ int invalid; /*!< Invalid configuration */
+ enum ast_ws_client_fields invalid_fields; /*!< Invalid fields */
+ enum ast_websocket_type connection_type; /*!< Connection type */
+ int connect_timeout; /*!< Connection timeout (ms) */
+ unsigned int reconnect_attempts; /*!< How many attempts before returning an error */
+ unsigned int reconnect_interval; /*!< How often to attempt a reconnect (ms) */
+ int tls_enabled; /*!< TLS enabled */
+ int verify_server_cert; /*!< Verify server certificate */
+ int verify_server_hostname; /*!< Verify server hostname */
+};
+
+/*!
+ * \brief Retrieve a container of all websocket client objects.
+ *
+ * \return The container. It may be empty but must always be cleaned up by the caller.
+ */
+struct ao2_container *ast_websocket_client_retrieve_all(void);
+
+/*!
+ * \brief Retrieve a websocket client object by ID.
+ *
+ * \param id The ID of the websocket client object.
+ * \return The websocket client ao2 object or NULL if not found. The reference
+ * must be cleaned up by the caller.
+ */
+struct ast_websocket_client *ast_websocket_client_retrieve_by_id(const char *id);
+
+/*!
+ * \brief Detect changes between two websocket client configurations.
+ *
+ * \param old_ow The old websocket configuration.
+ * \param new_ow The new websocket configuration.
+ * \return A bitmask of changed fields.
+ */
+enum ast_ws_client_fields ast_websocket_client_get_field_diff(
+ struct ast_websocket_client *old_wc,
+ struct ast_websocket_client *new_wc);
+
+/*!
+ * \brief Add sorcery observers for websocket client events.
+ *
+ * \param callbacks The observer callbacks to add.
+ * \return 0 on success, -1 on failure.
+ */
+int ast_websocket_client_observer_add(
+ const struct ast_sorcery_observer *callbacks);
+
+/*!
+ * \brief Remove sorcery observers for websocket client events.
+ *
+ * \param callbacks The observer callbacks to remove.
+ */
+void ast_websocket_client_observer_remove(
+ const struct ast_sorcery_observer *callbacks);
+
+/*!
+ * \brief Connect to a websocket server using the configured authentication,
+ * retry and TLS options.
+ *
+ * \param wc A pointer to the ast_websocket_structure
+ * \param lock_obj A pointer to an ao2 object to lock while the
+ * connection is being attempted or NULL if no locking is needed.
+ * \param display_name An id string to use for logging messages.
+ * If NULL or empty the connection's ID will be used.
+ * \param result A pointer to an enum ast_websocket_result to store the
+ * result of the connection attempt.
+ *
+ * \return A pointer to the ast_websocket structure on success, or NULL on failure.
+ */
+struct ast_websocket *ast_websocket_client_connect(struct ast_websocket_client *wc,
+ void *lock_obj, const char *display_name, enum ast_websocket_result *result);
+
+/*!
+ * \brief Force res_websocket_client to reload its configuration.
+ * \return 0 on success, -1 on failure.
+ */
+int ast_websocket_client_reload(void);
+
+#endif /* _RES_WEBSOCKET_CLIENT_H */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2025, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*** MODULEINFO
+ <support_level>core</support_level>
+ ***/
+
+/*** DOCUMENTATION
+ <configInfo name="res_websocket_client" language="en_US">
+ <synopsis>Websocket Client Configuration</synopsis>
+ <configFile name="websocket_client.conf">
+ <configObject name="websocket_client">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Websocket Client 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 "websocket_client".</synopsis>
+ </configOption>
+ <configOption name="uri">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Full URI to remote server.</synopsis>
+ </configOption>
+ <configOption name="protocols">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Comma separated list of protocols acceptable to the server.</synopsis>
+ </configOption>
+ <configOption name="username">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Server authentication username if required.</synopsis>
+ </configOption>
+ <configOption name="password">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Server authentication password if required.</synopsis>
+ </configOption>
+ <configOption name="connection_type">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Single persistent connection or per-call configuration.</synopsis>
+ <description>
+ <enumlist>
+ <enum name="persistent"><para>Single persistent connection for all calls.</para></enum>
+ <enum name="per_call_config"><para>New connection for each call to the Stasis() dialplan app.</para></enum>
+ </enumlist>
+ </description>
+ </configOption>
+ <configOption name="connection_timeout">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Connection timeout (ms).</synopsis>
+ </configOption>
+ <configOption name="reconnect_attempts">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>On failure, how many times should reconnection be attempted?</synopsis>
+ <description>
+ <para>
+ For per_call connections, this is the number of
+ (re)connection attempts to make before returning an
+ and terminating the call. Persistent connections
+ always retry forever but this setting will control
+ how often failure messages are logged.
+ </para>
+ </description>
+ </configOption>
+ <configOption name="reconnect_interval">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>How often should reconnection be attempted (ms)?</synopsis>
+ </configOption>
+ <configOption name="tls_enabled">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Enable TLS</synopsis>
+ </configOption>
+ <configOption name="ca_list_file">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>File containing the server's CA certificate. (optional)</synopsis>
+ </configOption>
+ <configOption name="ca_list_path">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>Path to a directory containing one or more hashed CA certificates. (optional)</synopsis>
+ </configOption>
+ <configOption name="cert_file">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>File containing a client certificate. (optional)</synopsis>
+ </configOption>
+ <configOption name="priv_key_file">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>File containing the client's private key. (optional)</synopsis>
+ </configOption>
+ <configOption name="verify_server_cert">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>If set to true, verify the server's certificate. (optional)</synopsis>
+ </configOption>
+ <configOption name="verify_server_hostname">
+ <since>
+ <version>20.15.0</version>
+ <version>21.10.0</version>
+ <version>22.5.0</version>
+ </since>
+ <synopsis>If set to true, verify that the server's hostname matches the common name in it's certificate. (optional)</synopsis>
+ </configOption>
+ </configObject>
+ </configFile>
+ </configInfo>
+***/
+
+
+#include "asterisk.h"
+
+#include "asterisk/module.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/strings.h"
+#include "asterisk/vector.h"
+#include "asterisk/websocket_client.h"
+
+static struct ast_sorcery *sorcery = NULL;
+
+struct ast_websocket *ast_websocket_client_connect(struct ast_websocket_client *wc,
+ void *lock_obj, const char *display_name, enum ast_websocket_result *result)
+{
+ int reconnect_counter = wc->reconnect_attempts;
+
+ if (ast_strlen_zero(display_name)) {
+ display_name = ast_sorcery_object_get_id(wc);
+ }
+
+ while (1) {
+ struct ast_websocket *astws = NULL;
+ struct ast_websocket_client_options options = {
+ .uri = wc->uri,
+ .protocols = wc->protocols,
+ .username = wc->username,
+ .password = wc->password,
+ .timeout = wc->connect_timeout,
+ .suppress_connection_msgs = 1,
+ .tls_cfg = NULL,
+ };
+
+ if (lock_obj) {
+ ao2_lock(lock_obj);
+ }
+
+ if (wc->tls_enabled) {
+ /*
+ * tls_cfg and its contents are freed automatically
+ * by res_http_websocket when the connection ends.
+ * We create it even if tls is not enabled to we can
+ * suppress connection error messages and print our own.
+ */
+ options.tls_cfg = ast_calloc(1, sizeof(*options.tls_cfg));
+ if (!options.tls_cfg) {
+ if (lock_obj) {
+ ao2_unlock(lock_obj);
+ }
+ return NULL;
+ }
+ /* TLS options */
+ options.tls_cfg->enabled = wc->tls_enabled;
+ options.tls_cfg->cafile = ast_strdup(wc->ca_list_file);
+ options.tls_cfg->capath = ast_strdup(wc->ca_list_path);
+ options.tls_cfg->certfile = ast_strdup(wc->cert_file);
+ options.tls_cfg->pvtfile = ast_strdup(wc->priv_key_file);
+ ast_set2_flag(&options.tls_cfg->flags, !wc->verify_server_cert, AST_SSL_DONT_VERIFY_SERVER);
+ ast_set2_flag(&options.tls_cfg->flags, !wc->verify_server_hostname, AST_SSL_IGNORE_COMMON_NAME);
+ }
+
+ astws = ast_websocket_client_create_with_options(&options, result);
+ if (astws && *result == WS_OK) {
+ if (lock_obj) {
+ ao2_unlock(lock_obj);
+ }
+ return astws;
+ }
+
+ reconnect_counter--;
+ if (reconnect_counter <= 0) {
+ if (wc->connection_type == AST_WS_TYPE_CLIENT_PERSISTENT) {
+ ast_log(LOG_WARNING,
+ "%s: Websocket connection to %s failed after %d tries: %s%s%s%s. Retrying in %d ms.\n",
+ display_name,
+ wc->uri,
+ wc->reconnect_attempts,
+ ast_websocket_result_to_str(*result),
+ errno ? " (" : "",
+ errno ? strerror(errno) : "",
+ errno ? ")" : "",
+ wc->reconnect_interval
+ );
+ } else {
+ ast_log(LOG_WARNING,
+ "%s: Websocket connection to %s failed after %d tries: %s%s%s%s. Hanging up after exhausting retries.\n",
+ display_name,
+ wc->uri,
+ wc->reconnect_attempts,
+ ast_websocket_result_to_str(*result),
+ errno ? " (" : "",
+ errno ? strerror(errno) : "",
+ errno ? ")" : ""
+ );
+ }
+ break;
+ }
+
+ if (lock_obj) {
+ ao2_lock(lock_obj);
+ }
+ usleep(wc->reconnect_interval * 1000);
+ }
+
+ return NULL;
+}
+
+
+
+static void wc_dtor(void *obj)
+{
+ struct ast_websocket_client *wc = obj;
+
+ ast_debug(3, "%s: Disposing of websocket client config\n",
+ ast_sorcery_object_get_id(wc));
+ ast_string_field_free_memory(wc);
+}
+
+static void *wc_alloc(const char *id)
+{
+ struct ast_websocket_client *wc = NULL;
+
+ wc = ast_sorcery_generic_alloc(sizeof(*wc), wc_dtor);
+ if (!wc) {
+ return NULL;
+ }
+
+ if (ast_string_field_init(wc, 1024) != 0) {
+ ao2_cleanup(wc);
+ return NULL;
+ }
+
+ ast_debug(2, "%s: Allocated websocket client config\n", id);
+ return wc;
+}
+
+static int websocket_client_connection_type_from_str(const struct aco_option *opt,
+ struct ast_variable *var, void *obj)
+{
+ struct ast_websocket_client *ws = obj;
+
+ if (strcasecmp(var->value, "persistent") == 0) {
+ ws->connection_type = AST_WS_TYPE_CLIENT_PERSISTENT;
+ } else if (strcasecmp(var->value, "per_call_config") == 0) {
+ ws->connection_type = AST_WS_TYPE_CLIENT_PER_CALL_CONFIG;
+ } else {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int websocket_client_connection_type_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+ const struct ast_websocket_client *wc = obj;
+
+ if (wc->connection_type == AST_WS_TYPE_CLIENT_PERSISTENT) {
+ *buf = ast_strdup("persistent");
+ } else if (wc->connection_type == AST_WS_TYPE_CLIENT_PER_CALL_CONFIG) {
+ *buf = ast_strdup("per_call_config");
+ } else {
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * Can't use INT_MIN because it's an expression
+ * and macro substitutions using stringify can't
+ * handle that.
+ */
+#define DEFAULT_RECONNECT_ATTEMPTS -2147483648
+
+static int wc_apply(const struct ast_sorcery *sorcery, void *obj)
+{
+ struct ast_websocket_client *wc = obj;
+ const char *id = ast_sorcery_object_get_id(wc);
+ int res = 0;
+
+ ast_debug(3, "%s: Applying config\n", id);
+
+ if (ast_strlen_zero(wc->uri)) {
+ ast_log(LOG_WARNING, "%s: Websocket client missing uri\n", id);
+ res = -1;
+ }
+
+ if (res != 0) {
+ ast_log(LOG_WARNING, "%s: Websocket client configuration failed\n", id);
+ } else {
+ ast_debug(3, "%s: Websocket client configuration succeeded\n", id);
+
+ if (wc->reconnect_attempts == DEFAULT_RECONNECT_ATTEMPTS) {
+ if (wc->connection_type == AST_WS_TYPE_CLIENT_PERSISTENT) {
+ wc->reconnect_attempts = INT_MAX;
+ } else {
+ wc->reconnect_attempts = 4;
+ }
+ }
+ }
+
+ return res;
+}
+
+struct ao2_container *ast_websocket_client_retrieve_all(void)
+{
+ if (!sorcery) {
+ return NULL;
+ }
+
+ return ast_sorcery_retrieve_by_fields(sorcery, "websocket_client",
+ AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+}
+
+struct ast_websocket_client *ast_websocket_client_retrieve_by_id(const char *id)
+{
+ if (!sorcery) {
+ return NULL;
+ }
+
+ return ast_sorcery_retrieve_by_id(sorcery, "websocket_client", id);
+}
+
+enum ast_ws_client_fields ast_websocket_client_get_field_diff(
+ struct ast_websocket_client *old_wc,
+ struct ast_websocket_client *new_wc)
+{
+ enum ast_ws_client_fields changed = AST_WS_CLIENT_FIELD_NONE;
+ const char *new_id = ast_sorcery_object_get_id(new_wc);
+ 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_wc, new_wc, &changes);
+ if (res != 0) {
+ ast_log(LOG_WARNING, "%s: Failed to create changeset\n", new_id);
+ return AST_WS_CLIENT_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, "connection_type")) {
+ changed |= AST_WS_CLIENT_FIELD_CONNECTION_TYPE;
+ } else if (ast_strings_equal(v->name, "uri")) {
+ changed |= AST_WS_CLIENT_FIELD_URI;
+ } else if (ast_strings_equal(v->name, "protocols")) {
+ changed |= AST_WS_CLIENT_FIELD_PROTOCOLS;
+ } else if (ast_strings_equal(v->name, "username")) {
+ changed |= AST_WS_CLIENT_FIELD_USERNAME;
+ } else if (ast_strings_equal(v->name, "password")) {
+ changed |= AST_WS_CLIENT_FIELD_PASSWORD;
+ } else if (ast_strings_equal(v->name, "tls_enabled")) {
+ changed |= AST_WS_CLIENT_FIELD_TLS_ENABLED;
+ } else if (ast_strings_equal(v->name, "ca_list_file")) {
+ changed |= AST_WS_CLIENT_FIELD_CA_LIST_FILE;
+ } else if (ast_strings_equal(v->name, "ca_list_path")) {
+ changed |= AST_WS_CLIENT_FIELD_CA_LIST_PATH;
+ } else if (ast_strings_equal(v->name, "cert_file")) {
+ changed |= AST_WS_CLIENT_FIELD_CERT_FILE;
+ } else if (ast_strings_equal(v->name, "priv_key_file")) {
+ changed |= AST_WS_CLIENT_FIELD_PRIV_KEY_FILE;
+ } else if (ast_strings_equal(v->name, "reconnect_interval")) {
+ changed |= AST_WS_CLIENT_FIELD_RECONNECT_INTERVAL;
+ } else if (ast_strings_equal(v->name, "reconnect_attempts")) {
+ changed |= AST_WS_CLIENT_FIELD_RECONNECT_ATTEMPTS;
+ } else if (ast_strings_equal(v->name, "connection_timeout")) {
+ changed |= AST_WS_CLIENT_FIELD_CONNECTION_TIMEOUT;
+ } else if (ast_strings_equal(v->name, "verify_server_cert")) {
+ changed |= AST_WS_CLIENT_FIELD_VERIFY_SERVER_CERT;
+ } else if (ast_strings_equal(v->name, "verify_server_hostname")) {
+ changed |= AST_WS_CLIENT_FIELD_VERIFY_SERVER_HOSTNAME;
+ } 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_wc,new_wc);
+ }
+ return changed;
+
+}
+
+int ast_websocket_client_observer_add(const struct ast_sorcery_observer *callbacks)
+{
+ if (!sorcery || !callbacks) {
+ return -1;
+ }
+
+ if (ast_sorcery_observer_add(sorcery, "websocket_client", callbacks)) {
+ ast_log(LOG_ERROR, "Failed to register websocket client observers\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+void ast_websocket_client_observer_remove(const struct ast_sorcery_observer *callbacks)
+{
+ if (!sorcery || !callbacks) {
+ return;
+ }
+
+ ast_sorcery_observer_remove(sorcery, "websocket_client", callbacks);
+}
+
+
+static int load_module(void)
+{
+ ast_debug(2, "Initializing Websocket Client Configuration\n");
+ sorcery = ast_sorcery_open();
+ if (!sorcery) {
+ ast_log(LOG_ERROR, "Failed to open sorcery\n");
+ return -1;
+ }
+
+ ast_sorcery_apply_default(sorcery, "websocket_client", "config",
+ "websocket_client.conf,criteria=type=websocket_client");
+
+ if (ast_sorcery_object_register(sorcery, "websocket_client", wc_alloc,
+ NULL, wc_apply)) {
+ ast_log(LOG_ERROR, "Failed to register websocket_client object with sorcery\n");
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ return -1;
+ }
+
+ ast_sorcery_object_field_register(sorcery, "websocket_client", "type", "", OPT_NOOP_T, 0, 0);
+ ast_sorcery_register_cust(websocket_client, connection_type, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, uri, uri, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, protocols, protocols, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, username, username, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, password, password, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, ca_list_file, ca_list_file, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, ca_list_path, ca_list_path, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, cert_file, cert_file, "");
+ ast_sorcery_register_sf(websocket_client, ast_websocket_client, priv_key_file, priv_key_file, "");
+ ast_sorcery_register_bool(websocket_client, ast_websocket_client, tls_enabled, tls_enabled, "no");
+ ast_sorcery_register_bool(websocket_client, ast_websocket_client, verify_server_cert, verify_server_cert, "yes");
+ ast_sorcery_register_bool(websocket_client, ast_websocket_client, verify_server_hostname, verify_server_hostname, "yes");
+ ast_sorcery_register_int(websocket_client, ast_websocket_client, connection_timeout, connect_timeout, 500);
+ ast_sorcery_register_int(websocket_client, ast_websocket_client, reconnect_attempts, reconnect_attempts, 4);
+ ast_sorcery_register_int(websocket_client, ast_websocket_client, reconnect_interval, reconnect_interval, 500);
+
+ ast_sorcery_load(sorcery);
+
+ return 0;
+}
+
+static int reload_module(void)
+{
+ ast_debug(2, "Reloading Websocket Client Configuration\n");
+ ast_sorcery_reload(sorcery);
+
+ return 0;
+}
+
+int ast_websocket_client_reload(void)
+{
+ ast_debug(2, "Reloading Websocket Client Configuration\n");
+ if (sorcery) {
+ ast_sorcery_reload(sorcery);
+ }
+
+ return 0;
+}
+
+static int unload_module(void)
+{
+ ast_debug(2, "Unloading Websocket Client Configuration\n");
+ if (sorcery) {
+ ast_sorcery_unref(sorcery);
+ sorcery = NULL;
+ }
+ return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "WebSocket Client Support",
+ .support_level = AST_MODULE_SUPPORT_CORE,
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload_module,
+ .load_pri = AST_MODPRI_CHANNEL_DEPEND,
+ .requires = "res_http_websocket",
+);