]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
chan_websocket: Allow additional URI parameters to be added to the outgoing URI.
authorGeorge Joseph <gjoseph@sangoma.com>
Wed, 13 Aug 2025 19:22:38 +0000 (13:22 -0600)
committerAsterisk Development Team <asteriskteam@digium.com>
Wed, 10 Sep 2025 19:55:24 +0000 (19:55 +0000)
* 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.

(cherry picked from commit d4d7f2e6e45e8655c98cb592cf70eb77fdc7539b)

channels/chan_websocket.c
include/asterisk/utils.h
include/asterisk/websocket_client.h
main/utils.c
res/res_websocket_client.c

index 2f28b82d8fe224aecfcb56318332d04d85d05f62..a1c2aba924c73efa58883ff1ffc8a09d5ca6de67 100644 (file)
        <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
  *
index c4c37e3f67a08f8db7bcc893744a4591e2ecd622..50e20fb8f6b50cc7cb4342b201598f3de61cf25a 100644 (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.
 
index b8a14d3a9f0bd7318cdd196ac5811f695ecfd555..f62907407f52a40271bb040a3e2c6ed77b7fcf48 100644 (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.
index 03c0216ac2b210dff04a36198f5394cf3f82398d..5451ddc49d2521785fa6573f89966bdb5806dd11 100644 (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;
index 8ee0aecc7d50bbd4bd9bc14fc1075ba48a3aefd1..290021b6c1bb008d6ddafb879eb922120ee791b6 100644 (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;
 }