<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="p - Passthrough mode">
- <para>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.
- </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="Make an outbound WebSocket connection using connection 'connection1' and the 'opus' codec. Passthrough mode will automatically be set.">
- same => n,Dial(WebSocket/connection1/c(opus))
- </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"
#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"
#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;
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;
#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"
#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
.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)
{
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);
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;
}
*/
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);
}
/*
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);
}
}
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;
-
- 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;
- }
+ RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+ const char *command = NULL;
+ char *data = NULL;
- /*
- * 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);
+ if (instance->control_msg_format == WEBCHAN_CONTROL_MSG_FORMAT_JSON) {
+ struct ast_json_error json_error;
- ast_debug(4, "%s: WebSocket %s command received\n",
- ast_channel_name(instance->channel), command);
+ 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++;
+ }
+ }
if (ast_strings_equal(command, ANSWER_CHANNEL)) {
ast_queue_control(instance->channel, AST_CONTROL_ANSWER);
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",
}
}
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);
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) {
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)
{
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));
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 {
OPT_ARG_WS_NO_AUTO_ANSWER,
OPT_ARG_WS_URI_PARAM,
OPT_ARG_WS_PASSTHROUGH,
+ OPT_ARG_WS_MSG_FORMAT,
OPT_ARG_ARRAY_SIZE
};
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,
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);
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) {
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);
}
/*!
.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)
{
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)
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;
}
.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",
);