diff --git a/configs/samples/websocket_client.conf.sample b/configs/samples/websocket_client.conf.sample new file mode 100644 index 0000000000..92d8f34641 --- /dev/null +++ b/configs/samples/websocket_client.conf.sample @@ -0,0 +1,51 @@ +; Common WebSocket Client Configuration for res_websocket_client +; +;[connection1] ; The connection name +;type = websocket_client ; Must be "websocket_client" +;connection_type = persistent : "persistent" or "per_call_config" + ; Default: none +;uri = ws://localhost:8765 ; The URI needed to contact the remote server. + ; If you've enabled tls, use "wss" for the scheme. + ; Default: none +;protocols = ari ; The websocket protocol expected by the server. + ; Default: none +;username = username ; An authentication username if required by the server. + ; Default: none +;password = password ; The authentication password for the username. + ; Default: none +;connection_timeout = 500 ; Connection timeout in milliseconds. + ; Default: 500 +;reconnect_interval = 1000 ; Number of milliseconds between (re)connection + ; attempts. + ; Default: 500 +;reconnect_attempts = 4 ; 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. + ; Default: 4 for both connection types. +;tls_enabled = no ; Set to "yes" to enable TLS connections. + ; Default: no +;ca_list_file = /etc/pki/tls/cert.pem + ; A file containing all CA certificates needed + ; for the connection. Not needed if your server + ; has a certificate from a recognized CA. + ; Default: none +;ca_list_path = /etc/pki/ca-trust/extracted/pem/directory-hash + ; A directory containing individual CA certificates + ; as an alternative to ca_list_file. Rarely needed. + ; Default: none +;cert_file = /etc/asterisk/cert.pem + ; If the server requires you to have a client + ; certificate, specify it here and if it wasn't + ; issued by a recognized CA, make sure the matching + ; CA certificate is available in ca_list_file or + ; ca_list_path. + ; Default: none +;priv_key_file = /etc/asterisk/privkey.pem + ; The private key for the client certificate. +;verify_server_cert = no ; Verify that the server certificate is valid. + ; Default: yes +;verify_server_hostname = no ; Verify that the hostname in the server's certificate + ; matches the hostname in the URI configured above. + ; Default: yes diff --git a/include/asterisk/http_websocket.h b/include/asterisk/http_websocket.h index 2bc9e59f43..150639fcb5 100644 --- a/include/asterisk/http_websocket.h +++ b/include/asterisk/http_websocket.h @@ -47,6 +47,26 @@ * */ +/*! \brief WebSocket connection/configuration types. + * + * These may look like they overlap or are redundant, but + * they're shared by other modules like ari and chan_websocket + * and it didn't make sense to force them to define their + * own types. + */ +enum ast_websocket_type { + AST_WS_TYPE_CLIENT_PERSISTENT = (1 << 0), + AST_WS_TYPE_CLIENT_PER_CALL_CONFIG = (1 << 1), + AST_WS_TYPE_CLIENT_PER_CALL = (1 << 2), + AST_WS_TYPE_CLIENT = (1 << 3), + AST_WS_TYPE_INBOUND = (1 << 4), + AST_WS_TYPE_SERVER = (1 << 5), + AST_WS_TYPE_ANY = (0xFFFFFFFF), +}; + +const char *ast_websocket_type_to_str(enum ast_websocket_type type); + + /*! \brief WebSocket operation codes */ enum ast_websocket_opcode { AST_WEBSOCKET_OPCODE_TEXT = 0x1, /*!< Text frame */ diff --git a/include/asterisk/sorcery.h b/include/asterisk/sorcery.h index 8f7e2b2bb2..ea3da000e6 100644 --- a/include/asterisk/sorcery.h +++ b/include/asterisk/sorcery.h @@ -1623,6 +1623,135 @@ int ast_sorcery_is_object_field_registered(const struct ast_sorcery_object_type */ 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 diff --git a/include/asterisk/websocket_client.h b/include/asterisk/websocket_client.h new file mode 100644 index 0000000000..b8a14d3a9f --- /dev/null +++ b/include/asterisk/websocket_client.h @@ -0,0 +1,146 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2025, Sangoma Technologies Corporation + * + * George Joseph + * + * 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 */ diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c index d4bc2242ee..b65d9a0cfa 100644 --- a/res/res_http_websocket.c +++ b/res/res_http_websocket.c @@ -103,6 +103,28 @@ struct ast_websocket { char buf[MAXIMUM_FRAME_SIZE]; /*!< Fixed buffer for reading data into */ }; +const char *ast_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_CLIENT: + return "client"; + case AST_WS_TYPE_INBOUND: + return "inbound"; + case AST_WS_TYPE_SERVER: + return "server"; + case AST_WS_TYPE_ANY: + return "any"; + default: + return "unknown"; + } +} + /*! \brief Hashing function for protocols */ static int protocol_hash_fn(const void *obj, const int flags) { diff --git a/res/res_websocket_client.c b/res/res_websocket_client.c new file mode 100644 index 0000000000..47774593ab --- /dev/null +++ b/res/res_websocket_client.c @@ -0,0 +1,570 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2025, Sangoma Technologies Corporation + * + * George Joseph + * + * 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 + core + ***/ + +/*** DOCUMENTATION + + Websocket Client Configuration + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Websocket Client Configuration + + + 20.15.0 + 21.10.0 + 22.5.0 + + Must be "websocket_client". + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Full URI to remote server. + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Comma separated list of protocols acceptable to the server. + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Server authentication username if required. + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Server authentication password if required. + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Single persistent connection or per-call configuration. + + + Single persistent connection for all calls. + New connection for each call to the Stasis() dialplan app. + + + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Connection timeout (ms). + + + + 20.15.0 + 21.10.0 + 22.5.0 + + On failure, how many times should reconnection be attempted? + + + 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. + + + + + + 20.15.0 + 21.10.0 + 22.5.0 + + How often should reconnection be attempted (ms)? + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Enable TLS + + + + 20.15.0 + 21.10.0 + 22.5.0 + + File containing the server's CA certificate. (optional) + + + + 20.15.0 + 21.10.0 + 22.5.0 + + Path to a directory containing one or more hashed CA certificates. (optional) + + + + 20.15.0 + 21.10.0 + 22.5.0 + + File containing a client certificate. (optional) + + + + 20.15.0 + 21.10.0 + 22.5.0 + + File containing the client's private key. (optional) + + + + 20.15.0 + 21.10.0 + 22.5.0 + + If set to true, verify the server's certificate. (optional) + + + + 20.15.0 + 21.10.0 + 22.5.0 + + If set to true, verify that the server's hostname matches the common name in it's certificate. (optional) + + + + +***/ + + +#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", +); diff --git a/res/res_websocket_client.exports.in b/res/res_websocket_client.exports.in new file mode 100644 index 0000000000..71835d6995 --- /dev/null +++ b/res/res_websocket_client.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIX*ast_websocket_client_*; + local: + *; +};