From: George Joseph Date: Fri, 2 May 2025 14:52:54 +0000 (-0600) Subject: res_websocket_client: Create common utilities for websocket clients. X-Git-Tag: 21.10.0-rc1~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1aea2d50aef5ff379a6f6baa4dd9b50e19536f17;p=thirdparty%2Fasterisk.git res_websocket_client: Create common utilities for websocket clients. Since multiple Asterisk capabilities now need to create websocket clients it makes sense to create a common set of utilities rather than making each of those capabilities implement their own. * A new configuration file "websocket_client.conf" is used to store common client parameters in named configuration sections. * APIs are provided to list and retrieve ast_websocket_client objects created from the named configurations. * An API is provided that accepts an ast_websocket_client object, connects to the remote server with retries and returns an ast_websocket object. TLS is supported as is basic authentication. * An observer can be registered to receive notification of loaded or reloaded client objects. * An API is provided to compare an existing client object to one just reloaded and return the fields that were changed. The caller can then decide what action to take based on which fields changed. Also as part of thie commit, several sorcery convenience macros were created to make registering common object fields easier. UserNote: A new module "res_websocket_client" and config file "websocket_client.conf" have been added to support several upcoming new capabilities that need common websocket client configuration. (cherry picked from commit 36fc358bc9f0c67f42578f43b8f39f9255d2b54a) --- 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: + *; +};