chan_websocket: Allow additional URI parameters to be added to the outgoing URI.

* Added a new option to the WebSocket dial string to capture the additional
  URI parameters.
* Added a new API ast_uri_verify_encoded() that verifies that a string
  either doesn't need URI encoding or that it has already been encoded.
* Added a new API ast_websocket_client_add_uri_params() to add the params
  to the client websocket session.
* Added XML documentation that will show up with `core show application Dial`
  that shows how to use it.

Resolves: #1352

UserNote: A new WebSocket channel driver option `v` has been added to the
Dial application that allows you to specify additional URI parameters on
outgoing connections. Run `core show application Dial` from the Asterisk CLI
to see how to use it.
This commit is contained in:
George Joseph
2025-08-13 13:22:38 -06:00
committed by github-actions[bot]
parent c1065d3444
commit e33185dedb
5 changed files with 214 additions and 2 deletions

View File

@@ -31,6 +31,61 @@
<support_level>core</support_level>
***/
/*** DOCUMENTATION
<info name="Dial_Resource" language="en_US" tech="WebSocket">
<para>WebSocket Dial Strings:</para>
<para><literal>Dial(WebSocket/connectionid[/websocket_options])</literal></para>
<para>WebSocket Parameters:</para>
<enumlist>
<enum name="connectionid">
<para>For outgoing WebSockets, this is the ID of the connection
in websocket_client.conf to use for the call. To accept incoming
WebSocket connections use the literal <literal>INCOMING</literal></para>
</enum>
<enum name="websocket_options">
<para>Options to control how the WebSocket channel behaves.</para>
<enumlist>
<enum name="c(codec) - Specify the codec to use in the channel">
<para></para>
<para> If not specified, the first codec from the caller's channel will be used.
</para>
</enum>
<enum name="n - Don't auto answer">
<para>Normally, the WebSocket channel will be answered when
connection is established with the remote app. If this
option is specified however, the channel will not be
answered until the <literal>ANSWER</literal> command is
received from the remote app or the remote app calls the
/channels/answer ARI endpoint.
</para>
</enum>
<enum name="v(uri_parameters) - Add parameters to the outbound URI">
<para>This option allows you to add additional parameters to the
outbound URI. The format is:
<literal>v(param1=value1,param2=value2...)</literal>
</para>
<para>You must ensure that no parameter name or value contains
characters not valid in a URL. The easiest way to do this is to
use the URIENCODE() dialplan function to encode them. Be aware
though that each name and value must be encoded separately. You
can't simply encode the whole string.</para>
</enum>
</enumlist>
</enum>
</enumlist>
<para>Examples:
</para>
<example title="Make an outbound WebSocket connection using connection 'connection1' and the 'sln16' codec.">
same => n,Dial(WebSocket/connection1/c(sln16))
</example>
<example title="Listen for an incoming WebSocket connection and don't auto-answer it.">
same => n,Dial(WebSocket/INCOMING/n)
</example>
<example title="Add URI parameters.">
same => n,Dial(WebSocket/connection1/v(${URIENCODE(vari able)}=${URIENCODE(${CHANNEL})},variable2=$(URIENCODE(${EXTEN})}))
</example>
</info>
***/
#include "asterisk.h"
#include "asterisk/app.h"
@@ -69,6 +124,7 @@ struct websocket_pvt {
pthread_t outbound_read_thread;
size_t bytes_read;
size_t leftover_len;
char *uri_params;
char *leftover_data;
int no_auto_answer;
int optimal_frame_size;
@@ -827,6 +883,10 @@ static int webchan_call(struct ast_channel *ast, const char *dest,
ast_debug(3, "%s: WebSocket call requested to %s. cid: %s\n",
ast_channel_name(ast), dest, instance->connection_id);
if (!ast_strlen_zero(instance->uri_params)) {
ast_websocket_client_add_uri_params(instance->client, instance->uri_params);
}
instance->websocket = ast_websocket_client_connect(instance->client,
instance, ast_channel_name(ast), &result);
if (!instance->websocket || result != WS_OK) {
@@ -909,6 +969,8 @@ static void websocket_destructor(void *data)
ast_free(instance->leftover_data);
instance->leftover_data = NULL;
}
ast_free(instance->uri_params);
}
struct instance_proxy {
@@ -1099,20 +1161,50 @@ static int set_channel_variables(struct websocket_pvt *instance)
return 0;
}
static int validate_uri_parameters(const char *uri_params)
{
char *params = ast_strdupa(uri_params);
char *nvp = NULL;
char *nv = NULL;
/*
* uri_params should be a comma-separated list of key=value pairs.
* For example:
* name1=value1,name2=value2
* We're verifying that each name and value either doesn't need
* to be encoded or that it already is.
*/
while((nvp = ast_strsep(&params, ',', 0))) {
/* nvp will be name1=value1 */
while((nv = ast_strsep(&nvp, '=', 0))) {
/* nv will be either name1 or value1 */
if (!ast_uri_verify_encoded(nv)) {
return 0;
}
}
}
return 1;
}
enum {
OPT_WS_CODEC = (1 << 0),
OPT_WS_NO_AUTO_ANSWER = (1 << 1),
OPT_WS_URI_PARAM = (1 << 2),
};
enum {
OPT_ARG_WS_CODEC,
OPT_ARG_WS_NO_AUTO_ANSWER,
OPT_ARG_WS_URI_PARAM,
OPT_ARG_ARRAY_SIZE
};
AST_APP_OPTIONS(websocket_options, BEGIN_OPTIONS
AST_APP_OPTION_ARG('c', OPT_WS_CODEC, OPT_ARG_WS_CODEC),
AST_APP_OPTION('n', OPT_WS_NO_AUTO_ANSWER),
AST_APP_OPTION_ARG('v', OPT_WS_URI_PARAM, OPT_ARG_WS_URI_PARAM),
END_OPTIONS );
static struct ast_channel *webchan_request(const char *type,
@@ -1187,6 +1279,42 @@ static struct ast_channel *webchan_request(const char *type,
instance->no_auto_answer = ast_test_flag(&opts, OPT_WS_NO_AUTO_ANSWER);
if (ast_test_flag(&opts, OPT_WS_URI_PARAM)
&& !ast_strlen_zero(opt_args[OPT_ARG_WS_URI_PARAM])) {
char *comma;
if (ast_strings_equal(args.connection_id, INCOMING_CONNECTION_ID)) {
ast_log(LOG_ERROR,
"%s: URI parameters are not allowed for 'WebSocket/INCOMING' channels\n",
requestor_name);
goto failure;
}
ast_debug(3, "%s: Using URI parameters '%s'\n",
requestor_name, opt_args[OPT_ARG_WS_URI_PARAM]);
if (!validate_uri_parameters(opt_args[OPT_ARG_WS_URI_PARAM])) {
ast_log(LOG_ERROR, "%s: Invalid URI parameters '%s' in WebSocket/%s dial string\n",
requestor_name, opt_args[OPT_ARG_WS_URI_PARAM],
args.connection_id);
goto failure;
}
instance->uri_params = ast_strdup(opt_args[OPT_ARG_WS_URI_PARAM]);
comma = instance->uri_params;
/*
* The normal separator for query string components is an
* ampersand ('&') but the Dial app interprets them as additional
* channels to dial in parallel so we instruct users to separate
* the parameters with commas (',') instead. We now have to
* convert those commas back to ampersands.
*/
while ((comma = strchr(comma,','))) {
*comma = '&';
}
ast_debug(3, "%s: Using final URI '%s'\n", requestor_name, instance->uri_params);
}
chan = ast_channel_alloc(1, AST_STATE_DOWN, "", "", "", "", "", assignedids,
requestor, 0, "WebSocket/%s/%p", args.connection_id, instance);
if (!chan) {
@@ -1246,7 +1374,6 @@ failure:
return NULL;
}
/*!
* \internal
*

View File

@@ -419,6 +419,19 @@ char *ast_uri_encode(const char *string, char *outbuf, int buflen, struct ast_fl
*/
void ast_uri_decode(char *s, struct ast_flags spec);
/*!
* \brief Verify if a string is valid as a URI component
*
* This function checks if the string either doesn't need encoding
* or is already properly URI encoded.
* Valid characters are 'a-zA-Z0-9.+_-' and '%xx' escape sequences.
*
* \param string String to be checked
* \retval 1 if the string is valid
* \retval 0 if the string is not valid
*/
int ast_uri_verify_encoded(const char *string);
/*! ast_xml_escape
\brief Escape reserved characters for use in XML.

View File

@@ -74,6 +74,7 @@ struct ast_websocket_client {
int tls_enabled; /*!< TLS enabled */
int verify_server_cert; /*!< Verify server certificate */
int verify_server_hostname; /*!< Verify server hostname */
AST_STRING_FIELD_EXTENDED(uri_params); /*!< Additional URI parameters */
};
/*!
@@ -137,6 +138,15 @@ void ast_websocket_client_observer_remove(
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 Add additional parameters to the URI.
*
* \param wc A pointer to the ast_websocket_structure
* \param uri_params A string containing URLENCODED parameters to append to the URI.
*/
void ast_websocket_client_add_uri_params(struct ast_websocket_client *wc,
const char *uri_params);
/*!
* \brief Force res_websocket_client to reload its configuration.
* \return 0 on success, -1 on failure.

View File

@@ -778,6 +778,42 @@ void ast_uri_decode(char *s, struct ast_flags spec)
*o = '\0';
}
int ast_uri_verify_encoded(const char *string)
{
const char *ptr = string;
size_t length;
char *endl;
if (!string) {
return 0;
}
length = strlen(string);
endl = (char *)string + length;
while (*ptr) {
if (*ptr == '%') {
unsigned int tmp;
/* Make sure there are at least 2 characters left to decode */
if (ptr + 2 >= endl) {
return 0;
}
/* Try to parse the next two characters as hex */
if (sscanf(ptr + 1, "%2x", &tmp) != 1) {
return 0;
}
/* All good, move past the '%' and the two hex digits */
ptr += 3;
} else if (!isalnum((unsigned char ) *ptr) && !strchr("-_.+", *ptr)) {
return 0;
} else {
ptr++;
}
}
return 1; /* all characters are valid */
}
char *ast_escape_quoted(const char *string, char *outbuf, int buflen)
{
const char *ptr = string;

View File

@@ -237,19 +237,40 @@ verify_server_hostname = no
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 = wc->uri,
.uri = S_OR(uri, wc->uri),
.protocols = wc->protocols,
.username = wc->username,
.password = wc->password,
@@ -357,6 +378,11 @@ static void *wc_alloc(const char *id)
return NULL;
}
if (ast_string_field_init_extended(wc, uri_params) != 0) {
ao2_cleanup(wc);
return NULL;
}
ast_debug(2, "%s: Allocated websocket client config\n", id);
return wc;
}