Files
asterisk/res/res_websocket_client.c
George Joseph d8df207318 WebSocket Enhancements: Proxies and Keepalives for ARI and Media Outbound Websockets.
See the notes below for high-level descriptions of the new features.

* Proxies

Outbound/forward HTTP proxies are now supported and configurable in
websocket_client.conf. You can specify a host:port plus optional proxy_username
and proxy_password. Because WebSockets aren't consistently supported among
proxies (specifically passing through UPGRADEs), the CONNECT method is always
used to establish a TCP tunnel through the proxy. This is required if a TLS
session is to be established with the WebSocket server anyway.  It's important
to understand that that negotiation with the proxy is ALWAYS unsecured. Once
the proxy establishes the tunnel, the TLS session will be negotiated directly
with the remote WebSocket server via the tunnel.

* Keepalives

Both TCP-level and WebSocket PING/PONG keepalives can be configured and are
available with either the curl or tcptls client implementations. The TCP
keepalives are handled entirely by the operating system and require no
resources from Asterisk but by their very nature, they can't traverse proxies.
WebSocket PING/PONGs are implemented in the Asterisk websocket code and require
a scheduler thread to keep track of them so they're a bit more complicated but
they do traverse proxies.  Which one is used is completely up to the admin.
You could use both.

* Other Changes

A few changes were needed to res/ari/ari_websockets and
res/res_aeap/transport_websocket to add explicit calls to ast_websocket_close.
They had been assuming that the websocket session destructor would close the
websocket when it unreffed it but the keepalive process now holds a reference
so the destructor wouldn't actually run without the call to ast_websocket_close
to stop the keepalives.

A few new methods were added to tcptls.c to allow switching an existing
connection from unsecured to TLS.  These were required because the initial
connection and handshake with a proxy is always unsecured but then needs
to be switched to TLS if required for the remote WebSocket server.

There was a bug in sorcery.h where the ast_sorcery_register_uint macro
was referencing _stringify (which doesn't exist) instead of _sorcery_stringify.

Resolves: #1881
Resolves: #1933

UserNote: Forward/outbound proxies can now be specified for outbound websockets.
See the websocket_client.conf.sample file for configuration information.

UserNote: TCP-level or WebSocket PING/PONG keepalives can now be enabled on
outbound websockets.  They can help detect network failures even when a
persistent connection is idle. See the websocket_client.conf.sample
file for configuration information.

DeveloperNote: The addition of the proxy and keepalive configuration parameters
pushed the websocket client parameter count over 32. This necessitated changing
the size of the ast_ws_client_fields enum from a 32 bit bitfield to a 64-bit
bitfield with a corresponding change to the ast_websocket_client structure.
2026-06-09 14:22:56 +00:00

844 lines
27 KiB
C

/*
* 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>
<see-also>
<ref type="link">/Configuration/Channel-Drivers/WebSocket/</ref>
<ref type="link">/Configuration/Interfaces/Asterisk-REST-Interface-ARI/ARI-Outbound-Websockets/</ref>
</see-also>
<description>
<para>
These config objects are currently shared by the following Asterisk capabilities:
</para>
<enumlist>
<enum name="chan_websocket"><para>The WebSocket channel driver.</para></enum>
<enum name="res_ari"><para>ARI Outbound WebSockets.</para></enum>
</enumlist>
<para>
They may have more specific information or restrictions on the parameters below.
</para>
<example title="websocket_client.conf">
;
; A connection for use by chan_websocket
[media_connection1]
type = websocket_client
uri = ws://localhost:8787
protocols = media
username = media_username
password = media_password
connection_type = per_call_config
connection_timeout = 500
reconnect_interval = 500
reconnect_attempts = 5
tls_enabled = no
;
; A TLS connection for use by ARI Outbound Websocket
[ari_connection1]
type = websocket_client
uri = wss://localhost:8765
protocols = ari
username = some_username
password = some_password
connection_type = persistent
connection_timeout = 500
reconnect_interval = 500
reconnect_attempts = 5
tls_enabled = yes
ca_list_file = /etc/pki/tls/cert.pem
verify_server_cert = no
verify_server_hostname = no
</example>
</description>
<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>
<configOption name="proxy_host">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Proxy host including port for outbound proxy if required. (optional)</synopsis>
<description>
<para>
If an outbound proxy is required to reach the websocket server,
specify a host in the form <literal>&lt;host&gt;:&lt;port7gt;</literal>.
Currently only http (non-TLS) proxies are supported although the tunnelled
connection to the websocket server can have TLS enabled.
</para>
</description>
</configOption>
<configOption name="proxy_username">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Proxy authentication username if required. (optional)</synopsis>
</configOption>
<configOption name="proxy_password">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Proxy authentication password if required. (optional)</synopsis>
</configOption>
<configOption name="enable_tcp_keepalives">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Enable TCP Keepalives. (optional)</synopsis>
</configOption>
<configOption name="tcp_keepalive_time">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Start sending keepalives when no data has been sent for this many seconds. (optional)</synopsis>
</configOption>
<configOption name="tcp_keepalive_interval">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Send keepalives at this interval in seconds. (optional)</synopsis>
<description>
<para>
If a reply isn't received by the time the next keepalive is due
to be sent, it's considered missed so this option also controls
how long it takes to detect a failure.
</para>
</description>
</configOption>
<configOption name="tcp_keepalive_probes">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Close the connection after this many missed replies. (optional)</synopsis>
<description>
<para>
If a reply isn't received by the time the next keepalive is due
to be sent, it's considered missed. The time to detect a failure
is therefore between (probes * interval) and
((probes + 1) * interval) seconds. If the connection closes
and reconnect_interval reconnect_attempts are set, a new connection
will be attempted using those parameters.
</para>
</description>
</configOption>
<configOption name="enable_pingpongs">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Enable WebSocket PING/PONGs.. (optional)</synopsis>
</configOption>
<configOption name="pingpong_interval">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Send WebSocket PINGs at this interval in seconds. (optional)</synopsis>
<description>
<para>
If a reply isn't received by the time the next PING is due
to be sent, it's considered missed so this option also controls
how long it takes to detect a failure.
</para>
</description>
</configOption>
<configOption name="pingpong_probes">
<since>
<version>20.21.0</version>
<version>22.11.0</version>
<version>23.5.0</version>
</since>
<synopsis>Close the connection after this many missed PONG replies. (optional)</synopsis>
<description>
<para>
If a reply isn't received by the time the next PING is due
to be sent, it's considered missepingd. The time to detect a failure
is therefore between (probes * interval) and
((probes + 1) * interval) seconds. If the connection closes
and reconnect_interval reconnect_attempts are set, a new connection
will be attempted using those parameters.
</para>
</description>
</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;
void ast_websocket_client_add_uri_params(struct ast_websocket_client *wc,
const char *uri_params)
{
ast_string_field_set(wc, uri_params, uri_params);
}
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;
char *uri = NULL;
if (ast_strlen_zero(display_name)) {
display_name = ast_sorcery_object_get_id(wc);
}
if (!ast_strlen_zero(wc->uri_params)) {
/*
* If the configured URI doesn't already contain parameters, we append the
* new ones to the URI path component with '?'. If it does, we append the
* new ones to the existing ones with a '&'.
*/
char sep = '?';
uri = ast_alloca(strlen(wc->uri) + strlen(wc->uri_params) + 2);
if (strchr(wc->uri, '?')) {
sep = '&';
}
sprintf(uri, "%s%c%s", wc->uri, sep, wc->uri_params); /*Safe */
}
while (1) {
struct ast_websocket *astws = NULL;
struct ast_websocket_client_options options = {
.uri = S_OR(uri, wc->uri),
.protocols = wc->protocols,
.username = wc->username,
.password = wc->password,
.timeout = wc->connect_timeout,
.suppress_connection_msgs = 1,
.proxy_host = wc->proxy_host,
.proxy_username = wc->proxy_username,
.proxy_password = wc->proxy_password,
.tcp_keepalives = wc->tcp_keepalives,
.tcp_keepalive_time = wc->tcp_keepalive_time,
.tcp_keepalive_interval = wc->tcp_keepalive_interval,
.tcp_keepalive_probes = wc->tcp_keepalive_probes,
.pingpongs = wc->pingpongs,
.pingpong_interval = wc->pingpong_interval,
.pingpong_probes = wc->pingpong_probes,
.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;
}
if (ast_string_field_init_extended(wc, uri_params) != 0) {
ao2_cleanup(wc);
return NULL;
}
if (ast_string_field_init_extended(wc, proxy_host) != 0) {
ao2_cleanup(wc);
return NULL;
}
if (ast_string_field_init_extended(wc, proxy_username) != 0) {
ao2_cleanup(wc);
return NULL;
}
if (ast_string_field_init_extended(wc, proxy_password) != 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 (!ast_strlen_zero(wc->proxy_host)) {
char *host = NULL;
char *port = NULL;
char *s = ast_strdupa(wc->proxy_host);
if (!ast_sockaddr_split_hostport(s, &host, &port, PARSE_PORT_REQUIRE)) {
ast_log(LOG_WARNING, "%s: proxy_host '%s' is missing a port\n", id, wc->proxy_host);
res = -1;
}
}
if (wc->tcp_keepalives) {
if (!wc->tcp_keepalive_time || !wc->tcp_keepalive_interval || !wc->tcp_keepalive_probes) {
ast_log(LOG_WARNING, "%s: tcp_keepalive_time, tcp_keepalive_interval and tcp_keepalive_probes must all be non-zero\n", id);
res = -1;
}
}
if (wc->pingpongs) {
if (!wc->pingpong_interval || !wc->pingpong_probes) {
ast_log(LOG_WARNING, "%s: pingpong_interval and pingpong_probes must be non-zero\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 if (ast_strings_equal(v->name, "proxy_host")) {
changed |= AST_WS_CLIENT_FIELD_PROXY_HOST;
} else if (ast_strings_equal(v->name, "proxy_username")) {
changed |= AST_WS_CLIENT_FIELD_PROXY_USERNAME;
} else if (ast_strings_equal(v->name, "proxy_password")) {
changed |= AST_WS_CLIENT_FIELD_PROXY_PASSWORD;
} else if (ast_strings_equal(v->name, "enable_tcp_keepalives")) {
changed |= AST_WS_CLIENT_FIELD_TCP_KEEPALIVES;
} else if (ast_strings_equal(v->name, "tcp_keepalive_time")) {
changed |= AST_WS_CLIENT_FIELD_TCP_KEEPALIVE_TIME;
} else if (ast_strings_equal(v->name, "tcp_keepalive_interval")) {
changed |= AST_WS_CLIENT_FIELD_TCP_KEEPALIVE_INTERVAL;
} else if (ast_strings_equal(v->name, "tcp_keepalive_probes")) {
changed |= AST_WS_CLIENT_FIELD_TCP_KEEPALIVE_PROBES;
} else if (ast_strings_equal(v->name, "enable_pingpongs")) {
changed |= AST_WS_CLIENT_FIELD_PINGPONGS;
} else if (ast_strings_equal(v->name, "pingpong_interval")) {
changed |= AST_WS_CLIENT_FIELD_PINGPONG_INTERVAL;
} else if (ast_strings_equal(v->name, "pingpong_probes")) {
changed |= AST_WS_CLIENT_FIELD_PINGPONG_PROBES;
} 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_register_sf(websocket_client, ast_websocket_client, proxy_host, proxy_host, "");
ast_sorcery_register_sf(websocket_client, ast_websocket_client, proxy_username, proxy_username, "");
ast_sorcery_register_sf(websocket_client, ast_websocket_client, proxy_password, proxy_password, "");
ast_sorcery_register_bool(websocket_client, ast_websocket_client, enable_tcp_keepalives, tcp_keepalives, "no");
ast_sorcery_register_uint(websocket_client, ast_websocket_client, tcp_keepalive_time, tcp_keepalive_time, 20);
ast_sorcery_register_uint(websocket_client, ast_websocket_client, tcp_keepalive_interval, tcp_keepalive_interval, 20);
ast_sorcery_register_uint(websocket_client, ast_websocket_client, tcp_keepalive_probes, tcp_keepalive_probes, 3);
ast_sorcery_register_bool(websocket_client, ast_websocket_client, enable_pingpongs, pingpongs, "no");
ast_sorcery_register_uint(websocket_client, ast_websocket_client, pingpong_interval, pingpong_interval, 20);
ast_sorcery_register_uint(websocket_client, ast_websocket_client, pingpong_probes, pingpong_probes, 3);
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",
);