diff --git a/channels/chan_websocket.c b/channels/chan_websocket.c index 2f28b82d8f..a1c2aba924 100644 --- a/channels/chan_websocket.c +++ b/channels/chan_websocket.c @@ -31,6 +31,61 @@ core ***/ +/*** DOCUMENTATION + + WebSocket Dial Strings: + Dial(WebSocket/connectionid[/websocket_options]) + WebSocket Parameters: + + + 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 INCOMING + + + Options to control how the WebSocket channel behaves. + + + + If not specified, the first codec from the caller's channel will be used. + + + + 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 ANSWER command is + received from the remote app or the remote app calls the + /channels/answer ARI endpoint. + + + + This option allows you to add additional parameters to the + outbound URI. The format is: + v(param1=value1,param2=value2...) + + 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. + + + + + Examples: + + + same => n,Dial(WebSocket/connection1/c(sln16)) + + + same => n,Dial(WebSocket/INCOMING/n) + + + same => n,Dial(WebSocket/connection1/v(${URIENCODE(vari able)}=${URIENCODE(${CHANNEL})},variable2=$(URIENCODE(${EXTEN})})) + + +***/ #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(¶ms, ',', 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 * diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h index c4c37e3f67..50e20fb8f6 100644 --- a/include/asterisk/utils.h +++ b/include/asterisk/utils.h @@ -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. diff --git a/include/asterisk/websocket_client.h b/include/asterisk/websocket_client.h index b8a14d3a9f..f62907407f 100644 --- a/include/asterisk/websocket_client.h +++ b/include/asterisk/websocket_client.h @@ -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. diff --git a/main/utils.c b/main/utils.c index 03c0216ac2..5451ddc49d 100644 --- a/main/utils.c +++ b/main/utils.c @@ -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; diff --git a/res/res_websocket_client.c b/res/res_websocket_client.c index 8ee0aecc7d..290021b6c1 100644 --- a/res/res_websocket_client.c +++ b/res/res_websocket_client.c @@ -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; }