diff --git a/channels/chan_websocket.c b/channels/chan_websocket.c index 4057aca477..4c5f92cc65 100644 --- a/channels/chan_websocket.c +++ b/channels/chan_websocket.c @@ -31,76 +31,6 @@ 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. - - - - In passthrough mode, the channel driver won't attempt - to re-frame or re-time media coming in over the websocket from - the remote app. This can be used for any codec but MUST be used - for codecs that use packet headers or whose data stream can't be - broken up on arbitrary byte boundaries. In this case, the remote - app is fully responsible for correctly framing and timing media - sent to Asterisk and the MEDIA text commands that could be sent - over the websocket are disabled. Currently, passthrough mode is - automatically set for the opus, speex and g729 codecs. - - - - 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/connection1/c(opus)) - - - 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" @@ -110,6 +40,7 @@ #include "asterisk/http_websocket.h" #include "asterisk/format_cache.h" #include "asterisk/frame.h" +#include "asterisk/json.h" #include "asterisk/lock.h" #include "asterisk/mod_format.h" #include "asterisk/module.h" @@ -118,6 +49,26 @@ #include "asterisk/timing.h" #include "asterisk/translate.h" #include "asterisk/websocket_client.h" +#include "asterisk/sorcery.h" + +static struct ast_sorcery *sorcery = NULL; + +enum webchan_control_msg_format { + WEBCHAN_CONTROL_MSG_FORMAT_PLAIN = 0, + WEBCHAN_CONTROL_MSG_FORMAT_JSON, + WEBCHAN_CONTROL_MSG_FORMAT_INVALID, +}; + +static const char *msg_format_map[] = { + [WEBCHAN_CONTROL_MSG_FORMAT_PLAIN] = "plain-text", + [WEBCHAN_CONTROL_MSG_FORMAT_JSON] = "json", + [WEBCHAN_CONTROL_MSG_FORMAT_INVALID] = "invalid", +}; + +struct webchan_conf_global { + SORCERY_OBJECT(details); + enum webchan_control_msg_format control_msg_format; +}; static struct ast_websocket_server *ast_ws_server; @@ -141,6 +92,7 @@ struct websocket_pvt { size_t leftover_len; char *uri_params; char *leftover_data; + enum webchan_control_msg_format control_msg_format; int no_auto_answer; int passthrough; int optimal_frame_size; @@ -166,6 +118,7 @@ struct websocket_pvt { #define PAUSE_MEDIA "PAUSE_MEDIA" #define CONTINUE_MEDIA "CONTINUE_MEDIA" +#if 0 #define MEDIA_START "MEDIA_START" #define MEDIA_XON "MEDIA_XON" #define MEDIA_XOFF "MEDIA_XOFF" @@ -173,6 +126,7 @@ struct websocket_pvt { #define DRIVER_STATUS "STATUS" #define MEDIA_BUFFERING_COMPLETED "MEDIA_BUFFERING_COMPLETED" #define DTMF_END "DTMF_END" +#endif #define QUEUE_LENGTH_MAX 1000 #define QUEUE_LENGTH_XOFF_LEVEL 900 @@ -198,6 +152,250 @@ static struct ast_channel_tech websocket_tech = { .send_digit_end = webchan_send_dtmf_text, }; +static enum webchan_control_msg_format control_msg_format_from_str(const char *value) +{ + if (ast_strlen_zero(value)) { + return WEBCHAN_CONTROL_MSG_FORMAT_INVALID; + } else if (strcasecmp(value, msg_format_map[WEBCHAN_CONTROL_MSG_FORMAT_PLAIN]) == 0) { + return WEBCHAN_CONTROL_MSG_FORMAT_PLAIN; + } else if (strcasecmp(value, msg_format_map[WEBCHAN_CONTROL_MSG_FORMAT_JSON]) == 0) { + return WEBCHAN_CONTROL_MSG_FORMAT_JSON; + } else { + return WEBCHAN_CONTROL_MSG_FORMAT_INVALID; + } +} + +static const char *control_msg_format_to_str(enum webchan_control_msg_format value) +{ + if (!ARRAY_IN_BOUNDS(value, msg_format_map)) { + return NULL; + } + return msg_format_map[value]; +} + +/*! + * \internal + * \brief Catch-all to print events that don't have any data. + * \warning Do not call directly. + */ +static char *_create_event_nodata(struct websocket_pvt *instance, char *event) +{ + char *payload = NULL; + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json * msg = ast_json_pack("{ s:s s:s }", + "event", event, + "channel_id", ast_channel_uniqueid(instance->channel)); + if (!msg) { + return NULL; + } + payload = ast_json_dump_string_format(msg, AST_JSON_COMPACT); + ast_json_unref(msg); + } else { + payload = ast_strdup(event); + } + + return payload; +} + +#define _create_event_MEDIA_XON(_instance) _create_event_nodata(_instance, "MEDIA_XON"); +#define _create_event_MEDIA_XOFF(_instance) _create_event_nodata(_instance, "MEDIA_XOFF"); +#define _create_event_QUEUE_DRAINED(_instance) _create_event_nodata(_instance, "QUEUE_DRAINED"); + +/*! + * \internal + * \brief Print the MEDIA_START event. + * \warning Do not call directly. + */ +static char *_create_event_MEDIA_START(struct websocket_pvt *instance) +{ + char *payload = NULL; + + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json *msg = ast_json_pack("{s:s, s:s, s:s, s:s, s:s, s:i, s:i, s:o }", + "event", "MEDIA_START", + "connection_id", instance->connection_id, + "channel", ast_channel_name(instance->channel), + "channel_id", ast_channel_uniqueid(instance->channel), + "format", ast_format_get_name(instance->native_format), + "optimal_frame_size", instance->optimal_frame_size, + "ptime", instance->native_codec->default_ms, + "channel_variables", ast_json_channel_vars(ast_channel_varshead( + instance->channel)) + ); + if (!msg) { + return NULL; + } + payload = ast_json_dump_string_format(msg, AST_JSON_COMPACT); + ast_json_unref(msg); + } else { + ast_asprintf(&payload, "%s %s:%s %s:%s %s:%s %s:%s %s:%d %s:%d", + "MEDIA_START", + "connection_id", instance->connection_id, + "channel", ast_channel_name(instance->channel), + "channel_id", ast_channel_uniqueid(instance->channel), + "format", ast_format_get_name(instance->native_format), + "optimal_frame_size", instance->optimal_frame_size, + "ptime", instance->native_codec->default_ms + ); + } + + return payload; +} + +/*! + * \internal + * \brief Print the MEDIA_BUFFERING_COMPLETED event. + * \warning Do not call directly. + */ +static char *_create_event_MEDIA_BUFFERING_COMPLETED(struct websocket_pvt *instance, + const char *id) +{ + char *payload = NULL; + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json *msg = ast_json_pack("{s:s, s:s, s:s}", + "event", "MEDIA_BUFFERING_COMPLETED", + "channel_id", ast_channel_uniqueid(instance->channel), + "correlation_id", S_OR(id, "") + ); + if (!msg) { + return NULL; + } + payload = ast_json_dump_string_format(msg, AST_JSON_COMPACT); + ast_json_unref(msg); + } else { + ast_asprintf(&payload, "%s%s%s", + "MEDIA_BUFFERING_COMPLETED", + S_COR(id, " ",""), S_OR(id, "")); + + } + + return payload; +} + +/*! + * \internal + * \brief Print the DTMF_END event. + * \warning Do not call directly. + */ +static char *_create_event_DTMF_END(struct websocket_pvt *instance, + const char digit) +{ + char *payload = NULL; + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json *msg = ast_json_pack("{s:s, s:s, s:s#}", + "event", "DTMF_END", + "channel_id", ast_channel_uniqueid(instance->channel), + "digit", digit, 1 + ); + if (!msg) { + return NULL; + } + payload = ast_json_dump_string_format(msg, AST_JSON_COMPACT); + ast_json_unref(msg); + } else { + ast_asprintf(&payload, "%s digit:%c channel_id:%s", + "DTMF_END", digit, ast_channel_uniqueid(instance->channel)); + } + + return payload; +} + +/*! + * \internal + * \brief Print the STATUS event. + * \warning Do not call directly. + */ +static char *_create_event_STATUS(struct websocket_pvt *instance) +{ + char *payload = NULL; + + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json *msg = ast_json_pack("{s:s, s:s, s:i, s:i, s:i, s:b, s:b, s:b }", + "event", "STATUS", + "channel_id", ast_channel_uniqueid(instance->channel), + "queue_length", instance->frame_queue_length, + "xon_level", QUEUE_LENGTH_XON_LEVEL, + "xoff_level", QUEUE_LENGTH_XOFF_LEVEL, + "queue_full", instance->queue_full, + "bulk_media", instance->bulk_media_in_progress, + "media_paused", instance->queue_paused + ); + if (!msg) { + return NULL; + } + payload = ast_json_dump_string_format(msg, AST_JSON_COMPACT); + ast_json_unref(msg); + } else { + ast_asprintf(&payload, "%s channel_id:%s queue_length:%d xon_level:%d xoff_level:%d queue_full:%s bulk_media:%s media_paused:%s", + "STATUS", + ast_channel_uniqueid(instance->channel), + instance->frame_queue_length, QUEUE_LENGTH_XON_LEVEL, + QUEUE_LENGTH_XOFF_LEVEL, + S_COR(instance->queue_full, "true", "false"), + S_COR(instance->bulk_media_in_progress, "true", "false"), + S_COR(instance->queue_paused, "true", "false") + ); + } + + return payload; +} + +/*! + * \internal + * \brief Print the ERROR event. + * \warning Do not call directly. + */ +static char *_create_event_ERROR(struct websocket_pvt *instance, + const char *error_text) +{ + char *payload = NULL; + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json *msg = ast_json_pack("{s:s, s:s, s:s}", + "event", "ERROR", + "channel_id", ast_channel_uniqueid(instance->channel), + "error_text", error_text); + if (!msg) { + return NULL; + } + payload = ast_json_dump_string_format(msg, AST_JSON_COMPACT); + ast_json_unref(msg); + } else { + ast_asprintf(&payload, "%s channel_id:%s error_text:%s", + "ERROR", ast_channel_uniqueid(instance->channel), error_text); + } + + return payload; +} + +/*! + * \def create_event + * \brief Use this macro to create events passing in any event-specific parameters. + */ +#define create_event(_instance, _event, ...) \ + _create_event_ ## _event(_instance, ##__VA_ARGS__) + +/*! + * \def send_event + * \brief Use this macro to create and send events passing in any event-specific parameters. + */ +#define send_event(_instance, _event, ...) \ +({ \ + int _res = -1; \ + char *_payload = _create_event_ ## _event(_instance, ##__VA_ARGS__); \ + if (_payload) { \ + _res = ast_websocket_write_string(_instance->websocket, _payload); \ + if (_res != 0) { \ + ast_log(LOG_ERROR, "%s: Unable to send event %s\n", \ + ast_channel_name(instance->channel), _payload); \ + } else { \ + ast_debug(4, "%s: Sent %s\n", \ + ast_channel_name(instance->channel), _payload); \ + }\ + ast_free(_payload); \ + } \ + (_res); \ +}) + static void set_channel_format(struct websocket_pvt * instance, struct ast_format *fmt) { @@ -240,7 +438,7 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance) instance->queue_full = 0; ast_debug(4, "%s: WebSocket sending MEDIA_XON\n", ast_channel_name(instance->channel)); - ast_websocket_write_string(instance->websocket, MEDIA_XON); + send_event(instance, MEDIA_XON); } queued_frame = AST_LIST_REMOVE_HEAD(&instance->frame_queue, frame_list); @@ -256,7 +454,7 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance) instance->report_queue_drained = 0; ast_debug(4, "%s: WebSocket sending QUEUE_DRAINED\n", ast_channel_name(instance->channel)); - ast_websocket_write_string(instance->websocket, QUEUE_DRAINED); + send_event(instance, QUEUE_DRAINED); } return NULL; } @@ -284,7 +482,7 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance) */ ast_websocket_write_string(instance->websocket, queued_frame->data.ptr); - ast_debug(4, "%s: WebSocket sending %s\n", + ast_debug(4, "%s: Sent %s\n", ast_channel_name(instance->channel), (char *)queued_frame->data.ptr); } /* @@ -445,9 +643,7 @@ static int queue_frame_from_buffer(struct websocket_pvt *instance, instance->frame_queue_length++; if (!instance->queue_full && instance->frame_queue_length >= QUEUE_LENGTH_XOFF_LEVEL) { instance->queue_full = 1; - ast_debug(4, "%s: WebSocket sending %s\n", - ast_channel_name(instance->channel), MEDIA_XOFF); - ast_websocket_write_string(instance->websocket, MEDIA_XOFF); + send_event(instance, MEDIA_XOFF); } } @@ -484,31 +680,40 @@ static int queue_option_frame(struct websocket_pvt *instance, return 0; } -static int process_text_message(struct websocket_pvt *instance, - char *payload, uint64_t payload_len) +/*! + * \internal + * \brief Handle commands from the websocket + * + * \param instance + * \param buffer Allocated by caller so don't free. + * \retval 0 Success + * \retval -1 Failure + */ +static int handle_command(struct websocket_pvt *instance, char *buffer) { int res = 0; - char *command; + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + const char *command = NULL; + char *data = NULL; - if (payload_len > MAX_TEXT_MESSAGE_LEN) { - ast_log(LOG_WARNING, "%s: WebSocket TEXT message of length %d exceeds maximum length of %d\n", - ast_channel_name(instance->channel), (int)payload_len, MAX_TEXT_MESSAGE_LEN); - return 0; + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + struct ast_json_error json_error; + + json = ast_json_load_buf(buffer, strlen(buffer), &json_error); + if (!json) { + send_event(instance, ERROR, "Unable to parse JSON command"); + return -1; + } + command = ast_json_object_string_get(json, "command"); + } else { + command = buffer; + data = strchr(buffer, ' '); + if (data) { + *data = '\0'; + data++; + } } - /* - * Unfortunately, payload is not NULL terminated even when it's - * a TEXT frame so we need to allocate a new buffer, copy - * the data into it, and NULL terminate it. - */ - command = ast_alloca(payload_len + 1); - memcpy(command, payload, payload_len); /* Safe */ - command[payload_len] = '\0'; - command = ast_strip(command); - - ast_debug(4, "%s: WebSocket %s command received\n", - ast_channel_name(instance->channel), command); - if (ast_strings_equal(command, ANSWER_CHANNEL)) { ast_queue_control(instance->channel, AST_CONTROL_ANSWER); @@ -525,13 +730,17 @@ static int process_text_message(struct websocket_pvt *instance, instance->bulk_media_in_progress = 1; AST_LIST_UNLOCK(&instance->frame_queue); - } else if (ast_begins_with(command, STOP_MEDIA_BUFFERING)) { - char *id; + } else if (ast_strings_equal(command, STOP_MEDIA_BUFFERING)) { + const char *id; char *option; SCOPED_LOCK(frame_queue_lock, &instance->frame_queue, AST_LIST_LOCK, AST_LIST_UNLOCK); - id = ast_strip(command + strlen(STOP_MEDIA_BUFFERING)); + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) { + id = ast_json_object_string_get(json, "correlation_id"); + } else { + id = data; + } if (instance->passthrough) { ast_debug(4, "%s: WebSocket in passthrough mode. Ignoring %s command.\n", @@ -551,10 +760,9 @@ static int process_text_message(struct websocket_pvt *instance, } } instance->leftover_len = 0; - res = ast_asprintf(&option, "%s%s%s", MEDIA_BUFFERING_COMPLETED, - S_COR(!ast_strlen_zero(id), " ", ""), S_OR(id, "")); - if (res <= 0 || !option) { - return res; + option = create_event(instance, MEDIA_BUFFERING_COMPLETED, id); + if (!option) { + return -1; } res = queue_option_frame(instance, option); ast_free(option); @@ -589,26 +797,7 @@ static int process_text_message(struct websocket_pvt *instance, AST_LIST_UNLOCK(&instance->frame_queue); } else if (ast_strings_equal(command, GET_DRIVER_STATUS)) { - char *status = NULL; - - res = ast_asprintf(&status, "%s channel_id:%s queue_length:%d xon_level:%d xoff_level:%d queue_full:%s bulk_media:%s media_paused:%s", - DRIVER_STATUS, - ast_channel_uniqueid(instance->channel), - instance->frame_queue_length, QUEUE_LENGTH_XON_LEVEL, - QUEUE_LENGTH_XOFF_LEVEL, - S_COR(instance->queue_full, "true", "false"), - S_COR(instance->bulk_media_in_progress, "true", "false"), - S_COR(instance->queue_paused, "true", "false") - ); - if (res <= 0 || !status) { - ast_free(status); - res = -1; - } else { - ast_debug(4, "%s: WebSocket status: %s\n", - ast_channel_name(instance->channel), status); - res = ast_websocket_write_string(instance->websocket, status); - ast_free(status); - } + return send_event(instance, STATUS); } else if (ast_strings_equal(command, PAUSE_MEDIA)) { if (instance->passthrough) { @@ -638,6 +827,39 @@ static int process_text_message(struct websocket_pvt *instance, return res; } +static int process_text_message(struct websocket_pvt *instance, + char *payload, uint64_t payload_len) +{ + char *command; + + if (payload_len == 0) { + ast_log(LOG_WARNING, "%s: WebSocket TEXT message has 0 length\n", + ast_channel_name(instance->channel)); + return 0; + } + + if (payload_len > MAX_TEXT_MESSAGE_LEN) { + ast_log(LOG_WARNING, "%s: WebSocket TEXT message of length %d exceeds maximum length of %d\n", + ast_channel_name(instance->channel), (int)payload_len, MAX_TEXT_MESSAGE_LEN); + return 0; + } + + /* + * Unfortunately, payload is not NULL terminated even when it's + * a TEXT frame so we need to allocate a new buffer, copy + * the data into it, and NULL terminate it. + */ + command = ast_alloca(payload_len + 1); + memcpy(command, payload, payload_len); /* Safe */ + command[payload_len] = '\0'; + command = ast_strip(command); + + ast_debug(4, "%s: Received: %s\n", + ast_channel_name(instance->channel), command); + + return handle_command(instance, command); +} + static int process_binary_message(struct websocket_pvt *instance, char *payload, uint64_t payload_len) { @@ -838,34 +1060,15 @@ static int read_from_ws_and_queue(struct websocket_pvt *instance) static void *read_thread_handler(void *obj) { RAII_VAR(struct websocket_pvt *, instance, obj, ao2_cleanup); - RAII_VAR(char *, command, NULL, ast_free); int res = 0; ast_debug(3, "%s: Read thread started\n", ast_channel_name(instance->channel)); - /* - * We need to tell the remote app what channel this media is for. - * This is especially important for outbound connections otherwise - * the app won't know who the media is for. - */ - res = ast_asprintf(&command, "%s connection_id:%s channel:%s channel_id:%s format:%s optimal_frame_size:%d ptime:%d", MEDIA_START, - instance->connection_id, ast_channel_name(instance->channel), - ast_channel_uniqueid(instance->channel), - ast_format_get_name(instance->native_format), - instance->optimal_frame_size, instance->native_codec->default_ms); - if (res <= 0 || !command) { - ast_queue_control(instance->channel, AST_CONTROL_HANGUP); - ast_log(LOG_ERROR, "%s: Failed to create MEDIA_START\n", ast_channel_name(instance->channel)); - return NULL; - } - res = ast_websocket_write_string(instance->websocket, command); - if (res != 0) { - ast_log(LOG_ERROR, "%s: Failed to send MEDIA_START\n", ast_channel_name(instance->channel)); + res = send_event(instance, MEDIA_START); + if (res != 0 ) { ast_queue_control(instance->channel, AST_CONTROL_HANGUP); return NULL; } - ast_debug(3, "%s: Sent %s\n", ast_channel_name(instance->channel), - command); if (!instance->no_auto_answer) { ast_debug(3, "%s: ANSWER by auto_answer\n", ast_channel_name(instance->channel)); @@ -1277,6 +1480,7 @@ enum { OPT_WS_NO_AUTO_ANSWER = (1 << 1), OPT_WS_URI_PARAM = (1 << 2), OPT_WS_PASSTHROUGH = (1 << 3), + OPT_WS_MSG_FORMAT = (1 << 4), }; enum { @@ -1284,6 +1488,7 @@ enum { OPT_ARG_WS_NO_AUTO_ANSWER, OPT_ARG_WS_URI_PARAM, OPT_ARG_WS_PASSTHROUGH, + OPT_ARG_WS_MSG_FORMAT, OPT_ARG_ARRAY_SIZE }; @@ -1292,6 +1497,7 @@ AST_APP_OPTIONS(websocket_options, BEGIN_OPTIONS AST_APP_OPTION('n', OPT_WS_NO_AUTO_ANSWER), AST_APP_OPTION_ARG('v', OPT_WS_URI_PARAM, OPT_ARG_WS_URI_PARAM), AST_APP_OPTION('p', OPT_WS_PASSTHROUGH), + AST_APP_OPTION_ARG('f', OPT_WS_MSG_FORMAT, OPT_ARG_WS_MSG_FORMAT), END_OPTIONS ); static struct ast_channel *webchan_request(const char *type, @@ -1310,6 +1516,9 @@ static struct ast_channel *webchan_request(const char *type, struct ast_flags opts = { 0, }; char *opt_args[OPT_ARG_ARRAY_SIZE]; const char *requestor_name = requestor ? ast_channel_name(requestor) : "no channel"; + RAII_VAR(struct webchan_conf_global *, global_cfg, NULL, ao2_cleanup); + + global_cfg = ast_sorcery_retrieve_by_id(sorcery, "global", "global"); ast_debug(3, "%s: WebSocket channel requested\n", requestor_name); @@ -1405,6 +1614,19 @@ static struct ast_channel *webchan_request(const char *type, ast_debug(3, "%s: Using final URI '%s'\n", requestor_name, instance->uri_params); } + if (ast_test_flag(&opts, OPT_WS_MSG_FORMAT)) { + instance->control_msg_format = control_msg_format_from_str(opt_args[OPT_ARG_WS_MSG_FORMAT]); + + if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_INVALID) { + ast_log(LOG_WARNING, "%s: 'f/control message format' dialstring parameter value missing or invalid. " + "Defaulting to 'plain-text'\n", + ast_channel_name(requestor)); + instance->control_msg_format = WEBCHAN_CONTROL_MSG_FORMAT_PLAIN; + } + } else if (global_cfg) { + instance->control_msg_format = global_cfg->control_msg_format; + } + chan = ast_channel_alloc(1, AST_STATE_DOWN, "", "", "", "", "", assignedids, requestor, 0, "WebSocket/%s/%p", args.connection_id, instance); if (!chan) { @@ -1502,28 +1724,13 @@ static int webchan_hangup(struct ast_channel *ast) static int webchan_send_dtmf_text(struct ast_channel *ast, char digit, unsigned int duration) { - struct websocket_pvt *instance = ast_channel_tech_pvt(ast); - char *command; - int res = 0; + struct websocket_pvt *instance = ast_channel_tech_pvt(ast); - if (!instance) { - return -1; - } + if (!instance) { + return -1; + } - res = ast_asprintf(&command, "%s digit:%c channel_id:%s", DTMF_END, digit, ast_channel_uniqueid(instance->channel)); - if (res <= 0 || !command) { - ast_log(LOG_ERROR, "%s: Failed to create DTMF_END\n", ast_channel_name(instance->channel)); - return 0; - } - res = ast_websocket_write_string(instance->websocket, command); - if (res != 0) { - ast_log(LOG_ERROR, "%s: Failed to send DTMF_END\n", ast_channel_name(instance->channel)); - ast_free(command); - return 0; - } - ast_debug(3, "%s: Sent %s\n", ast_channel_name(instance->channel), command); - ast_free(command); - return 0; + return send_event(instance, DTMF_END, digit); } /*! @@ -1687,6 +1894,85 @@ static struct ast_http_uri http_uri = { .no_decode_uri = 1, }; +AO2_STRING_FIELD_HASH_FN(instance_proxy, connection_id) +AO2_STRING_FIELD_CMP_FN(instance_proxy, connection_id) +AO2_STRING_FIELD_SORT_FN(instance_proxy, connection_id) + +static int global_control_message_format_from_str(const struct aco_option *opt, + struct ast_variable *var, void *obj) +{ + struct webchan_conf_global *cfg = obj; + + cfg->control_msg_format = control_msg_format_from_str(var->value); + + if (cfg->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_INVALID) { + ast_log(LOG_ERROR, "chan_websocket.conf: Invalid value '%s' for " + "control_mesage_format. Must be 'plain-text' or 'json'\n", + var->value); + return -1; + } + + return 0; +} + +static int global_control_message_format_to_str(const void *obj, const intptr_t *args, char **buf) +{ + const struct webchan_conf_global *cfg = obj; + + *buf = ast_strdup(control_msg_format_to_str(cfg->control_msg_format)); + + return 0; +} + +static void *global_alloc(const char *name) +{ + struct webchan_conf_global *cfg = ast_sorcery_generic_alloc( + sizeof(*cfg), NULL); + + if (!cfg) { + return NULL; + } + + return cfg; +} + +static int global_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct webchan_conf_global *cfg = obj; + + ast_debug(1, "control_msg_format: %s\n", + control_msg_format_to_str(cfg->control_msg_format)); + + return 0; +} + +static int load_config(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, "global", "config", + "chan_websocket.conf,criteria=type=global,single_object=yes,explicit_name=global"); + + if (ast_sorcery_object_register(sorcery, "global", global_alloc, NULL, global_apply)) { + ast_log(LOG_ERROR, "Failed to register chan_websocket global object with sorcery\n"); + ast_sorcery_unref(sorcery); + sorcery = NULL; + return -1; + } + + ast_sorcery_object_field_register_nodoc(sorcery, "global", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_register_cust(global, control_message_format, "plain-text"); + + ast_sorcery_load(sorcery); + + return 0; +} + /*! \brief Function called when our module is unloaded */ static int unload_module(void) { @@ -1701,12 +1987,19 @@ static int unload_module(void) ao2_cleanup(instances); instances = NULL; + ast_sorcery_unref(sorcery); + sorcery = NULL; + return 0; } -AO2_STRING_FIELD_HASH_FN(instance_proxy, connection_id) -AO2_STRING_FIELD_CMP_FN(instance_proxy, connection_id) -AO2_STRING_FIELD_SORT_FN(instance_proxy, connection_id) +static int reload_module(void) +{ + ast_debug(2, "Reloading chan_websocket configuration\n"); + ast_sorcery_reload(sorcery); + + return 0; +} /*! \brief Function called when our module is loaded */ static int load_module(void) @@ -1714,6 +2007,11 @@ static int load_module(void) int res = 0; struct ast_websocket_protocol *protocol; + res = load_config(); + if (res != 0) { + return AST_MODULE_LOAD_DECLINE; + } + if (!(websocket_tech.capabilities = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT))) { return AST_MODULE_LOAD_DECLINE; } @@ -1758,6 +2056,7 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Websocket Media Chann .support_level = AST_MODULE_SUPPORT_CORE, .load = load_module, .unload = unload_module, + .reload = reload_module, .load_pri = AST_MODPRI_CHANNEL_DRIVER, .requires = "res_http_websocket,res_websocket_client", ); diff --git a/channels/chan_websocket_doc.xml b/channels/chan_websocket_doc.xml new file mode 100644 index 0000000000..e264783bd3 --- /dev/null +++ b/channels/chan_websocket_doc.xml @@ -0,0 +1,119 @@ + + + + + Configuration for chan_websocket + + WebSocket Channel Driver + + + + + + 20.18.0 + 22.8.0 + 23.2.0 + + Global settings for chan_websocket + + + 20.18.0 + 22.8.0 + 23.2.0 + + Determines the format used for sending and receiving + control mesages on the websocket. + + + + + The legacy plain text single-line message format. + + + The new JSON format. + + + + + + + + + + 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. + + + + + format: + + + + + In passthrough mode, the channel driver won't attempt + to re-frame or re-time media coming in over the websocket from + the remote app. This can be used for any codec but MUST be used + for codecs that use packet headers or whose data stream can't be + broken up on arbitrary byte boundaries. In this case, the remote + app is fully responsible for correctly framing and timing media + sent to Asterisk and the MEDIA text commands that could be sent + over the websocket are disabled. Currently, passthrough mode is + automatically set for the opus, speex and g729 codecs. + + + + 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/connection1/c(opus)) + + + same => n,Dial(WebSocket/INCOMING/n) + + + same => n,Dial(WebSocket/connection1/v(${URIENCODE(vari able)}=${URIENCODE(${CHANNEL})},variable2=$(URIENCODE(${EXTEN})})) + + + same => n,Dial(WebSocket/connection1/f(json)) + + + diff --git a/configs/samples/chan_websocket.conf.sample b/configs/samples/chan_websocket.conf.sample new file mode 100644 index 0000000000..19a6b7a1ac --- /dev/null +++ b/configs/samples/chan_websocket.conf.sample @@ -0,0 +1,10 @@ +; Configuration for chan_websocket +; +;[general] +;control_message_format = plain-text ; The format for the control messages sent + ; and received on the websocket. + ; plain-text: The legacy single-line message + ; format. + ; json: All messages are properly formatted + ; JSON. + ; Default: plain-text diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c index 8218896f0e..d989abb9ab 100644 --- a/res/ari/resource_channels.c +++ b/res/ari/resource_channels.c @@ -2213,9 +2213,10 @@ static int external_media_websocket(struct ast_ari_channels_external_media_args struct ast_channel *chan; struct varshead *vars; - if (ast_asprintf(&endpoint, "WebSocket/%s/c(%s)", + if (ast_asprintf(&endpoint, "WebSocket/%s%s%s", args->external_host, - args->format) == -1) { + S_COR(args->transport_data, "/", ""), + S_OR(args->transport_data, "")) == -1) { return 1; } diff --git a/res/ari/resource_channels.h b/res/ari/resource_channels.h index b79f5f0255..e090dc204e 100644 --- a/res/ari/resource_channels.h +++ b/res/ari/resource_channels.h @@ -861,6 +861,8 @@ struct ast_ari_channels_external_media_args { const char *direction; /*! An arbitrary data field */ const char *data; + /*! Transport-specific data. For websocket this is appended to the dialstring. */ + const char *transport_data; }; /*! * \brief Body parsing function for /channels/externalMedia. diff --git a/res/res_ari_channels.c b/res/res_ari_channels.c index b37e830753..5474b60f42 100644 --- a/res/res_ari_channels.c +++ b/res/res_ari_channels.c @@ -2975,6 +2975,10 @@ int ast_ari_channels_external_media_parse_body( if (field) { args->data = ast_json_string_get(field); } + field = ast_json_object_get(body, "transport_data"); + if (field) { + args->transport_data = ast_json_string_get(field); + } return 0; } @@ -3027,6 +3031,9 @@ static void ast_ari_channels_external_media_cb( if (strcmp(i->name, "data") == 0) { args.data = (i->value); } else + if (strcmp(i->name, "transport_data") == 0) { + args.transport_data = (i->value); + } else {} } args.variables = body; diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json index a50b8720de..f7ea634d07 100644 --- a/rest-api/api-docs/channels.json +++ b/rest-api/api-docs/channels.json @@ -2027,6 +2027,14 @@ "required": false, "allowMultiple": false, "dataType": "string" + }, + { + "name": "transport_data", + "description": "Transport-specific data. For websocket this is appended to the dialstring.", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string" } ], "errorResponses": [