From a75fd32212c35b41143442bd757387fad636177a Mon Sep 17 00:00:00 2001 From: "David M. Lee" Date: Wed, 3 Jul 2013 17:58:45 +0000 Subject: ARI - channel recording support This patch is the first step in adding recording support to the Asterisk REST Interface. Recordings are stored in /var/spool/recording. Since recordings may be destructive (overwriting existing files), the API rejects attempts to escape the recording directory (avoiding issues if someone attempts to record to ../../lib/sounds/greeting, for example). (closes issue ASTERISK-21594) (closes issue ASTERISK-21581) Review: https://reviewboard.asterisk.org/r/2612/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@393550 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- res/res_stasis_http_bridges.c | 4 +- res/res_stasis_http_channels.c | 9 +- res/res_stasis_http_recordings.c | 114 ++++----- res/res_stasis_playback.c | 18 +- res/res_stasis_recording.c | 443 ++++++++++++++++++++++++++++++++++ res/res_stasis_recording.exports.in | 6 + res/stasis_http/resource_channels.c | 136 ++++++++++- res/stasis_http/resource_channels.h | 4 +- res/stasis_http/resource_recordings.c | 26 +- res/stasis_http/resource_recordings.h | 40 +-- 10 files changed, 709 insertions(+), 91 deletions(-) create mode 100644 res/res_stasis_recording.c create mode 100644 res/res_stasis_recording.exports.in (limited to 'res') diff --git a/res/res_stasis_http_bridges.c b/res/res_stasis_http_bridges.c index a4801df13..878c1ce0a 100644 --- a/res/res_stasis_http_bridges.c +++ b/res/res_stasis_http_bridges.c @@ -387,10 +387,10 @@ static void stasis_http_record_bridge_cb( args.max_silence_seconds = atoi(i->value); } else if (strcmp(i->name, "append") == 0) { - args.append = atoi(i->value); + args.append = ast_true(i->value); } else if (strcmp(i->name, "beep") == 0) { - args.beep = atoi(i->value); + args.beep = ast_true(i->value); } else if (strcmp(i->name, "terminateOn") == 0) { args.terminate_on = (i->value); diff --git a/res/res_stasis_http_channels.c b/res/res_stasis_http_channels.c index ebcc9e880..5343714b1 100644 --- a/res/res_stasis_http_channels.c +++ b/res/res_stasis_http_channels.c @@ -765,11 +765,11 @@ static void stasis_http_record_channel_cb( if (strcmp(i->name, "maxSilenceSeconds") == 0) { args.max_silence_seconds = atoi(i->value); } else - if (strcmp(i->name, "append") == 0) { - args.append = atoi(i->value); + if (strcmp(i->name, "ifExists") == 0) { + args.if_exists = (i->value); } else if (strcmp(i->name, "beep") == 0) { - args.beep = atoi(i->value); + args.beep = ast_true(i->value); } else if (strcmp(i->name, "terminateOn") == 0) { args.terminate_on = (i->value); @@ -788,8 +788,9 @@ static void stasis_http_record_channel_cb( switch (code) { case 500: /* Internal server error */ + case 400: /* Invalid parameters */ case 404: /* Channel not found */ - case 409: /* Channel is not in a Stasis application, or the channel is currently bridged with other channels. */ + case 409: /* Channel is not in a Stasis application; the channel is currently bridged with other channels; A recording with the same name is currently in progress. */ is_valid = 1; break; default: diff --git a/res/res_stasis_http_recordings.c b/res/res_stasis_http_recordings.c index 4aa43c9be..5b8043251 100644 --- a/res/res_stasis_http_recordings.c +++ b/res/res_stasis_http_recordings.c @@ -91,7 +91,7 @@ static void stasis_http_get_stored_recordings_cb( #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/stored/{recordingId}. + * \brief Parameter parsing callback for /recordings/stored/{recordingName}. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -110,8 +110,8 @@ static void stasis_http_get_stored_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -128,20 +128,20 @@ static void stasis_http_get_stored_recording_cb( is_valid = ari_validate_stored_recording( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingId}\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingId}\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/stored/{recordingId}. + * \brief Parameter parsing callback for /recordings/stored/{recordingName}. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -160,8 +160,8 @@ static void stasis_http_delete_stored_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -178,13 +178,13 @@ static void stasis_http_delete_stored_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingId}\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingId}\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } @@ -233,7 +233,7 @@ static void stasis_http_get_live_recordings_cb( #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}. + * \brief Parameter parsing callback for /recordings/live/{recordingName}. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -252,8 +252,8 @@ static void stasis_http_get_live_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -270,20 +270,20 @@ static void stasis_http_get_live_recording_cb( is_valid = ari_validate_live_recording( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}. + * \brief Parameter parsing callback for /recordings/live/{recordingName}. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -302,8 +302,8 @@ static void stasis_http_cancel_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -320,20 +320,20 @@ static void stasis_http_cancel_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}/stop. + * \brief Parameter parsing callback for /recordings/live/{recordingName}/stop. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -352,8 +352,8 @@ static void stasis_http_stop_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -370,20 +370,20 @@ static void stasis_http_stop_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/stop\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/stop\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/stop\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/stop\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}/pause. + * \brief Parameter parsing callback for /recordings/live/{recordingName}/pause. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -402,8 +402,8 @@ static void stasis_http_pause_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -420,20 +420,20 @@ static void stasis_http_pause_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/pause\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/pause\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/pause\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/pause\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}/unpause. + * \brief Parameter parsing callback for /recordings/live/{recordingName}/unpause. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -452,8 +452,8 @@ static void stasis_http_unpause_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -470,20 +470,20 @@ static void stasis_http_unpause_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/unpause\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/unpause\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/unpause\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/unpause\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}/mute. + * \brief Parameter parsing callback for /recordings/live/{recordingName}/mute. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -502,8 +502,8 @@ static void stasis_http_mute_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -520,20 +520,20 @@ static void stasis_http_mute_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/mute\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/mute\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/mute\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/mute\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } #endif /* AST_DEVMODE */ } /*! - * \brief Parameter parsing callback for /recordings/live/{recordingId}/unmute. + * \brief Parameter parsing callback for /recordings/live/{recordingName}/unmute. * \param get_params GET parameters in the HTTP request. * \param path_vars Path variables extracted from the request. * \param headers HTTP headers. @@ -552,8 +552,8 @@ static void stasis_http_unmute_recording_cb( struct ast_variable *i; for (i = path_vars; i; i = i->next) { - if (strcmp(i->name, "recordingId") == 0) { - args.recording_id = (i->value); + if (strcmp(i->name, "recordingName") == 0) { + args.recording_name = (i->value); } else {} } @@ -570,13 +570,13 @@ static void stasis_http_unmute_recording_cb( is_valid = ari_validate_void( response->message); } else { - ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/unmute\n", code); + ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/unmute\n", code); is_valid = 0; } } if (!is_valid) { - ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/unmute\n"); + ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/unmute\n"); stasis_http_response_error(response, 500, "Internal Server Error", "Response validation failed"); } @@ -584,8 +584,8 @@ static void stasis_http_unmute_recording_cb( } /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_stored_recordingId = { - .path_segment = "recordingId", +static struct stasis_rest_handlers recordings_stored_recordingName = { + .path_segment = "recordingName", .is_wildcard = 1, .callbacks = { [AST_HTTP_GET] = stasis_http_get_stored_recording_cb, @@ -601,10 +601,10 @@ static struct stasis_rest_handlers recordings_stored = { [AST_HTTP_GET] = stasis_http_get_stored_recordings_cb, }, .num_children = 1, - .children = { &recordings_stored_recordingId, } + .children = { &recordings_stored_recordingName, } }; /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_live_recordingId_stop = { +static struct stasis_rest_handlers recordings_live_recordingName_stop = { .path_segment = "stop", .callbacks = { [AST_HTTP_POST] = stasis_http_stop_recording_cb, @@ -613,7 +613,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_stop = { .children = { } }; /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_live_recordingId_pause = { +static struct stasis_rest_handlers recordings_live_recordingName_pause = { .path_segment = "pause", .callbacks = { [AST_HTTP_POST] = stasis_http_pause_recording_cb, @@ -622,7 +622,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_pause = { .children = { } }; /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_live_recordingId_unpause = { +static struct stasis_rest_handlers recordings_live_recordingName_unpause = { .path_segment = "unpause", .callbacks = { [AST_HTTP_POST] = stasis_http_unpause_recording_cb, @@ -631,7 +631,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_unpause = { .children = { } }; /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_live_recordingId_mute = { +static struct stasis_rest_handlers recordings_live_recordingName_mute = { .path_segment = "mute", .callbacks = { [AST_HTTP_POST] = stasis_http_mute_recording_cb, @@ -640,7 +640,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_mute = { .children = { } }; /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_live_recordingId_unmute = { +static struct stasis_rest_handlers recordings_live_recordingName_unmute = { .path_segment = "unmute", .callbacks = { [AST_HTTP_POST] = stasis_http_unmute_recording_cb, @@ -649,15 +649,15 @@ static struct stasis_rest_handlers recordings_live_recordingId_unmute = { .children = { } }; /*! \brief REST handler for /api-docs/recordings.{format} */ -static struct stasis_rest_handlers recordings_live_recordingId = { - .path_segment = "recordingId", +static struct stasis_rest_handlers recordings_live_recordingName = { + .path_segment = "recordingName", .is_wildcard = 1, .callbacks = { [AST_HTTP_GET] = stasis_http_get_live_recording_cb, [AST_HTTP_DELETE] = stasis_http_cancel_recording_cb, }, .num_children = 5, - .children = { &recordings_live_recordingId_stop,&recordings_live_recordingId_pause,&recordings_live_recordingId_unpause,&recordings_live_recordingId_mute,&recordings_live_recordingId_unmute, } + .children = { &recordings_live_recordingName_stop,&recordings_live_recordingName_pause,&recordings_live_recordingName_unpause,&recordings_live_recordingName_mute,&recordings_live_recordingName_unmute, } }; /*! \brief REST handler for /api-docs/recordings.{format} */ static struct stasis_rest_handlers recordings_live = { @@ -666,7 +666,7 @@ static struct stasis_rest_handlers recordings_live = { [AST_HTTP_GET] = stasis_http_get_live_recordings_cb, }, .num_children = 1, - .children = { &recordings_live_recordingId, } + .children = { &recordings_live_recordingName, } }; /*! \brief REST handler for /api-docs/recordings.{format} */ static struct stasis_rest_handlers recordings = { diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c index 3b092df2d..5b55ebc51 100644 --- a/res/res_stasis_playback.c +++ b/res/res_stasis_playback.c @@ -37,6 +37,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/file.h" #include "asterisk/logger.h" #include "asterisk/module.h" +#include "asterisk/paths.h" #include "asterisk/stasis_app_impl.h" #include "asterisk/stasis_app_playback.h" #include "asterisk/stasis_channels.h" @@ -195,7 +196,7 @@ static void *play_uri(struct stasis_app_control *control, RAII_VAR(struct stasis_app_playback *, playback, NULL, playback_cleanup); RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); - const char *file; + RAII_VAR(char *, file, NULL, ast_free); int res; long offsetms; @@ -225,16 +226,27 @@ static void *play_uri(struct stasis_app_control *control, if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) { /* Play sound */ - file = playback->media + strlen(SOUND_URI_SCHEME); + file = ast_strdup(playback->media + strlen(SOUND_URI_SCHEME)); } else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) { /* Play recording */ - file = playback->media + strlen(RECORDING_URI_SCHEME); + const char *relname = + playback->media + strlen(RECORDING_URI_SCHEME); + if (relname[0] == '/') { + file = ast_strdup(relname); + } else { + ast_asprintf(&file, "%s/%s", + ast_config_AST_RECORDING_DIR, relname); + } } else { /* Play URL */ ast_log(LOG_ERROR, "Unimplemented\n"); return NULL; } + if (!file) { + return NULL; + } + res = ast_control_streamfile_lang(chan, file, fwd, rev, stop, pause, restart, playback->skipms, playback->language, &offsetms); diff --git a/res/res_stasis_recording.c b/res/res_stasis_recording.c new file mode 100644 index 000000000..3d8e11bbd --- /dev/null +++ b/res/res_stasis_recording.c @@ -0,0 +1,443 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief res_stasis recording support. + * + * \author David M. Lee, II + */ + +/*** MODULEINFO + res_stasis + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/dsp.h" +#include "asterisk/file.h" +#include "asterisk/module.h" +#include "asterisk/paths.h" +#include "asterisk/stasis_app_impl.h" +#include "asterisk/stasis_app_recording.h" +#include "asterisk/stasis_channels.h" + +/*! Number of hash buckets for recording container. Keep it prime! */ +#define RECORDING_BUCKETS 127 + +/*! Comment is ignored by most formats, so we will ignore it, too. */ +#define RECORDING_COMMENT NULL + +/*! Recording check is unimplemented. le sigh */ +#define RECORDING_CHECK 0 + +STASIS_MESSAGE_TYPE_DEFN(stasis_app_recording_snapshot_type); + +/*! Container of all current recordings */ +static struct ao2_container *recordings; + +struct stasis_app_recording { + /*! Recording options. */ + struct stasis_app_recording_options *options; + /*! Absolute path (minus extension) of the recording */ + char *absolute_name; + /*! Control object for the channel we're playing back to */ + struct stasis_app_control *control; + + /*! Current state of the recording. */ + enum stasis_app_recording_state state; +}; + +static int recording_hash(const void *obj, int flags) +{ + const struct stasis_app_recording *recording = obj; + const char *id = flags & OBJ_KEY ? obj : recording->options->name; + return ast_str_hash(id); +} + +static int recording_cmp(void *obj, void *arg, int flags) +{ + struct stasis_app_recording *lhs = obj; + struct stasis_app_recording *rhs = arg; + const char *rhs_id = flags & OBJ_KEY ? arg : rhs->options->name; + + if (strcmp(lhs->options->name, rhs_id) == 0) { + return CMP_MATCH | CMP_STOP; + } else { + return 0; + } +} + +static const char *state_to_string(enum stasis_app_recording_state state) +{ + switch (state) { + case STASIS_APP_RECORDING_STATE_QUEUED: + return "queued"; + case STASIS_APP_RECORDING_STATE_RECORDING: + return "recording"; + case STASIS_APP_RECORDING_STATE_PAUSED: + return "paused"; + case STASIS_APP_RECORDING_STATE_COMPLETE: + return "done"; + case STASIS_APP_RECORDING_STATE_FAILED: + return "failed"; + } + + return "?"; +} + +static void recording_options_dtor(void *obj) +{ + struct stasis_app_recording_options *options = obj; + + ast_string_field_free_memory(options); +} + +struct stasis_app_recording_options *stasis_app_recording_options_create( + const char *name, const char *format) +{ + RAII_VAR(struct stasis_app_recording_options *, options, NULL, + ao2_cleanup); + + options = ao2_alloc(sizeof(*options), recording_options_dtor); + + if (!options || ast_string_field_init(options, 128)) { + return NULL; + } + ast_string_field_set(options, name, name); + ast_string_field_set(options, format, format); + + ao2_ref(options, +1); + return options; +} + +char stasis_app_recording_termination_parse(const char *str) +{ + if (ast_strlen_zero(str)) { + return STASIS_APP_RECORDING_TERMINATE_NONE; + } + + if (strcasecmp(str, "none") == 0) { + return STASIS_APP_RECORDING_TERMINATE_NONE; + } + + if (strcasecmp(str, "any") == 0) { + return STASIS_APP_RECORDING_TERMINATE_ANY; + } + + if (strcasecmp(str, "#") == 0) { + return '#'; + } + + if (strcasecmp(str, "*") == 0) { + return '*'; + } + + return STASIS_APP_RECORDING_TERMINATE_INVALID; +} + +enum ast_record_if_exists stasis_app_recording_if_exists_parse( + const char *str) +{ + if (ast_strlen_zero(str)) { + /* Default value */ + return AST_RECORD_IF_EXISTS_FAIL; + } + + if (strcasecmp(str, "fail") == 0) { + return AST_RECORD_IF_EXISTS_FAIL; + } + + if (strcasecmp(str, "overwrite") == 0) { + return AST_RECORD_IF_EXISTS_OVERWRITE; + } + + if (strcasecmp(str, "append") == 0) { + return AST_RECORD_IF_EXISTS_APPEND; + } + + return -1; +} + +static void recording_publish(struct stasis_app_recording *recording) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup); + + ast_assert(recording != NULL); + + json = stasis_app_recording_to_json(recording); + if (json == NULL) { + return; + } + + message = ast_channel_blob_create_from_cache( + stasis_app_control_get_channel_id(recording->control), + stasis_app_recording_snapshot_type(), json); + if (message == NULL) { + return; + } + + stasis_app_control_publish(recording->control, message); +} + +static void recording_fail(struct stasis_app_recording *recording) +{ + SCOPED_AO2LOCK(lock, recording); + recording->state = STASIS_APP_RECORDING_STATE_FAILED; + recording_publish(recording); +} + +static void recording_cleanup(struct stasis_app_recording *recording) +{ + ao2_unlink_flags(recordings, recording, + OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA); +} + +static void *record_file(struct stasis_app_control *control, + struct ast_channel *chan, void *data) +{ + RAII_VAR(struct stasis_app_recording *, recording, + NULL, recording_cleanup); + char *acceptdtmf; + int res; + int duration = 0; + + recording = data; + ast_assert(recording != NULL); + + ao2_lock(recording); + recording->state = STASIS_APP_RECORDING_STATE_RECORDING; + recording_publish(recording); + ao2_unlock(recording); + + switch (recording->options->terminate_on) { + case STASIS_APP_RECORDING_TERMINATE_NONE: + case STASIS_APP_RECORDING_TERMINATE_INVALID: + acceptdtmf = ""; + break; + case STASIS_APP_RECORDING_TERMINATE_ANY: + acceptdtmf = "#*0123456789abcd"; + break; + default: + acceptdtmf = ast_alloca(2); + acceptdtmf[0] = recording->options->terminate_on; + acceptdtmf[1] = '\0'; + } + + res = ast_auto_answer(chan); + if (res != 0) { + ast_debug(3, "%s: Failed to answer\n", + ast_channel_uniqueid(chan)); + recording_fail(recording); + return NULL; + } + + ast_play_and_record_full(chan, + recording->options->beep ? "beep" : NULL, + recording->absolute_name, + recording->options->max_duration_seconds, + recording->options->format, + &duration, + NULL, /* sound_duration */ + -1, /* silencethreshold */ + recording->options->max_silence_seconds * 1000, + NULL, /* path */ + acceptdtmf, + NULL, /* canceldtmf */ + 1, /* skip_confirmation_sound */ + recording->options->if_exists); + + ast_debug(3, "%s: Recording complete\n", ast_channel_uniqueid(chan)); + + ao2_lock(recording); + recording->state = STASIS_APP_RECORDING_STATE_COMPLETE; + recording_publish(recording); + ao2_unlock(recording); + + return NULL; +} + +static void recording_dtor(void *obj) +{ + struct stasis_app_recording *recording = obj; + + ao2_cleanup(recording->options); +} + +struct stasis_app_recording *stasis_app_control_record( + struct stasis_app_control *control, + struct stasis_app_recording_options *options) +{ + RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup); + char *last_slash; + + errno = 0; + + if (options == NULL || + ast_strlen_zero(options->name) || + ast_strlen_zero(options->format) || + options->max_silence_seconds < 0 || + options->max_duration_seconds < 0) { + errno = EINVAL; + return NULL; + } + + ast_debug(3, "%s: Sending record(%s.%s) command\n", + stasis_app_control_get_channel_id(control), options->name, + options->format); + + recording = ao2_alloc(sizeof(*recording), recording_dtor); + if (!recording) { + errno = ENOMEM; + return NULL; + } + + ast_asprintf(&recording->absolute_name, "%s/%s", + ast_config_AST_RECORDING_DIR, options->name); + + if (recording->absolute_name == NULL) { + errno = ENOMEM; + return NULL; + } + + if ((last_slash = strrchr(recording->absolute_name, '/'))) { + *last_slash = '\0'; + if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR, + recording->absolute_name, 0777) != 0) { + /* errno set by ast_mkdir */ + return NULL; + } + *last_slash = '/'; + } + + ao2_ref(options, +1); + recording->options = options; + recording->control = control; + recording->state = STASIS_APP_RECORDING_STATE_QUEUED; + + { + RAII_VAR(struct stasis_app_recording *, old_recording, NULL, + ao2_cleanup); + + SCOPED_AO2LOCK(lock, recordings); + + old_recording = ao2_find(recordings, options->name, + OBJ_KEY | OBJ_NOLOCK); + if (old_recording) { + ast_log(LOG_WARNING, + "Recording %s already in progress\n", + recording->options->name); + errno = EEXIST; + return NULL; + } + ao2_link(recordings, recording); + } + + /* A ref is kept in the recordings container; no need to bump */ + stasis_app_send_command_async(control, record_file, recording); + + /* Although this should be bumped for the caller */ + ao2_ref(recording, +1); + return recording; +} + +enum stasis_app_recording_state stasis_app_recording_get_state( + struct stasis_app_recording *recording) +{ + return recording->state; +} + +const char *stasis_app_recording_get_name( + struct stasis_app_recording *recording) +{ + return recording->options->name; +} + +struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name) +{ + RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup); + + recording = ao2_find(recordings, name, OBJ_KEY); + if (recording == NULL) { + return NULL; + } + + ao2_ref(recording, +1); + return recording; +} + +struct ast_json *stasis_app_recording_to_json( + const struct stasis_app_recording *recording) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + + if (recording == NULL) { + return NULL; + } + + json = ast_json_pack("{s: s, s: s, s: s}", + "name", recording->options->name, + "format", recording->options->format, + "state", state_to_string(recording->state)); + + return ast_json_ref(json); +} + +enum stasis_app_recording_oper_results stasis_app_recording_operation( + struct stasis_app_recording *recording, + enum stasis_app_recording_media_operation operation) +{ + ast_assert(0); // TODO + return STASIS_APP_RECORDING_OPER_FAILED; +} + +static int load_module(void) +{ + int r; + + r = STASIS_MESSAGE_TYPE_INIT(stasis_app_recording_snapshot_type); + if (r != 0) { + return AST_MODULE_LOAD_FAILURE; + } + + recordings = ao2_container_alloc(RECORDING_BUCKETS, recording_hash, + recording_cmp); + if (!recordings) { + return AST_MODULE_LOAD_FAILURE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ao2_cleanup(recordings); + recordings = NULL; + STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, + "Stasis application recording support", + .load = load_module, + .unload = unload_module, + .nonoptreq = "res_stasis"); diff --git a/res/res_stasis_recording.exports.in b/res/res_stasis_recording.exports.in new file mode 100644 index 000000000..0ad493c49 --- /dev/null +++ b/res/res_stasis_recording.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXstasis_app_*; + local: + *; +}; diff --git a/res/stasis_http/resource_channels.c b/res/stasis_http/resource_channels.c index 0fbb75487..8db3b697c 100644 --- a/res/stasis_http/resource_channels.c +++ b/res/stasis_http/resource_channels.c @@ -1,4 +1,4 @@ -/* -*- C -*- +/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 2012 - 2013, Digium, Inc. @@ -39,6 +39,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/callerid.h" #include "asterisk/stasis_app.h" #include "asterisk/stasis_app_playback.h" +#include "asterisk/stasis_app_recording.h" #include "asterisk/stasis_channels.h" #include "resource_channels.h" @@ -249,10 +250,139 @@ void stasis_http_play_on_channel(struct ast_variable *headers, stasis_http_response_created(response, playback_url, json); } -void stasis_http_record_channel(struct ast_variable *headers, struct ast_record_channel_args *args, struct stasis_http_response *response) + +void stasis_http_record_channel(struct ast_variable *headers, + struct ast_record_channel_args *args, + struct stasis_http_response *response) { - ast_log(LOG_ERROR, "TODO: stasis_http_record_channel\n"); + RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup); + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup); + RAII_VAR(char *, recording_url, NULL, ast_free); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + RAII_VAR(struct stasis_app_recording_options *, options, NULL, + ao2_cleanup); + RAII_VAR(char *, uri_encoded_name, NULL, ast_free); + size_t uri_name_maxlen; + + ast_assert(response != NULL); + + if (args->max_duration_seconds < 0) { + stasis_http_response_error( + response, 400, "Bad Request", + "max_duration_seconds cannot be negative"); + return; + } + + if (args->max_silence_seconds < 0) { + stasis_http_response_error( + response, 400, "Bad Request", + "max_silence_seconds cannot be negative"); + return; + } + + control = find_control(response, args->channel_id); + if (control == NULL) { + /* Response filled in by find_control */ + return; + } + + options = stasis_app_recording_options_create(args->name, args->format); + if (options == NULL) { + stasis_http_response_error( + response, 500, "Internal Server Error", + "Out of memory"); + } + options->max_silence_seconds = args->max_silence_seconds; + options->max_duration_seconds = args->max_duration_seconds; + options->terminate_on = + stasis_app_recording_termination_parse(args->terminate_on); + options->if_exists = + stasis_app_recording_if_exists_parse(args->if_exists); + options->beep = args->beep; + + if (options->terminate_on == STASIS_APP_RECORDING_TERMINATE_INVALID) { + stasis_http_response_error( + response, 400, "Bad Request", + "terminateOn invalid"); + return; + } + + if (options->if_exists == -1) { + stasis_http_response_error( + response, 400, "Bad Request", + "ifExists invalid"); + return; + } + + recording = stasis_app_control_record(control, options); + if (recording == NULL) { + switch(errno) { + case EINVAL: + /* While the arguments are invalid, we should have + * caught them prior to calling record. + */ + stasis_http_response_error( + response, 500, "Internal Server Error", + "Error parsing request"); + break; + case EEXIST: + stasis_http_response_error(response, 409, "Conflict", + "Recording '%s' already in progress", + args->name); + break; + case ENOMEM: + stasis_http_response_error( + response, 500, "Internal Server Error", + "Out of memory"); + break; + case EPERM: + stasis_http_response_error( + response, 400, "Bad Request", + "Recording name invalid"); + break; + default: + ast_log(LOG_WARNING, + "Unrecognized recording error: %s\n", + strerror(errno)); + stasis_http_response_error( + response, 500, "Internal Server Error", + "Internal Server Error"); + break; + } + return; + } + + uri_name_maxlen = strlen(args->name) * 3; + uri_encoded_name = ast_malloc(uri_name_maxlen); + if (!uri_encoded_name) { + stasis_http_response_error( + response, 500, "Internal Server Error", + "Out of memory"); + return; + } + ast_uri_encode(args->name, uri_encoded_name, uri_name_maxlen, + ast_uri_http); + + ast_asprintf(&recording_url, "/recordings/live/%s", uri_encoded_name); + if (!recording_url) { + stasis_http_response_error( + response, 500, "Internal Server Error", + "Out of memory"); + return; + } + + json = stasis_app_recording_to_json(recording); + if (!json) { + stasis_http_response_error( + response, 500, "Internal Server Error", + "Out of memory"); + return; + } + + stasis_http_response_created(response, recording_url, json); } + void stasis_http_get_channel(struct ast_variable *headers, struct ast_get_channel_args *args, struct stasis_http_response *response) diff --git a/res/stasis_http/resource_channels.h b/res/stasis_http/resource_channels.h index 57f2a63d2..7e8dc5dbe 100644 --- a/res/stasis_http/resource_channels.h +++ b/res/stasis_http/resource_channels.h @@ -247,8 +247,8 @@ struct ast_record_channel_args { int max_duration_seconds; /*! \brief Maximum duration of silence, in seconds. 0 for no limit */ int max_silence_seconds; - /*! \brief If true, and recording already exists, append to recording */ - int append; + /*! \brief Action to take if a recording with the same name already exists. */ + const char *if_exists; /*! \brief Play beep when recording begins */ int beep; /*! \brief DTMF input to terminate recording */ diff --git a/res/stasis_http/resource_recordings.c b/res/stasis_http/resource_recordings.c index 7d31c42aa..d93d59017 100644 --- a/res/stasis_http/resource_recordings.c +++ b/res/stasis_http/resource_recordings.c @@ -27,6 +27,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") +#include "asterisk/stasis_app_recording.h" #include "resource_recordings.h" void stasis_http_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct stasis_http_response *response) @@ -45,10 +46,31 @@ void stasis_http_get_live_recordings(struct ast_variable *headers, struct ast_ge { ast_log(LOG_ERROR, "TODO: stasis_http_get_live_recordings\n"); } -void stasis_http_get_live_recording(struct ast_variable *headers, struct ast_get_live_recording_args *args, struct stasis_http_response *response) + +void stasis_http_get_live_recording(struct ast_variable *headers, + struct ast_get_live_recording_args *args, + struct stasis_http_response *response) { - ast_log(LOG_ERROR, "TODO: stasis_http_get_live_recording\n"); + RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + + recording = stasis_app_recording_find_by_name(args->recording_name); + if (recording == NULL) { + stasis_http_response_error(response, 404, "Not Found", + "Recording not found"); + return; + } + + json = stasis_app_recording_to_json(recording); + if (json == NULL) { + stasis_http_response_error(response, 500, + "Internal Server Error", "Error building response"); + return; + } + + stasis_http_response_ok(response, ast_json_ref(json)); } + void stasis_http_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct stasis_http_response *response) { ast_log(LOG_ERROR, "TODO: stasis_http_cancel_recording\n"); diff --git a/res/stasis_http/resource_recordings.h b/res/stasis_http/resource_recordings.h index acccc124b..18a5bfe68 100644 --- a/res/stasis_http/resource_recordings.h +++ b/res/stasis_http/resource_recordings.h @@ -52,8 +52,8 @@ struct ast_get_stored_recordings_args { void stasis_http_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_get_stored_recording() */ struct ast_get_stored_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Get a stored recording's details. @@ -65,8 +65,8 @@ struct ast_get_stored_recording_args { void stasis_http_get_stored_recording(struct ast_variable *headers, struct ast_get_stored_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_delete_stored_recording() */ struct ast_delete_stored_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Delete a stored recording. @@ -89,8 +89,8 @@ struct ast_get_live_recordings_args { void stasis_http_get_live_recordings(struct ast_variable *headers, struct ast_get_live_recordings_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_get_live_recording() */ struct ast_get_live_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief List live recordings. @@ -102,8 +102,8 @@ struct ast_get_live_recording_args { void stasis_http_get_live_recording(struct ast_variable *headers, struct ast_get_live_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_cancel_recording() */ struct ast_cancel_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Stop a live recording and discard it. @@ -115,8 +115,8 @@ struct ast_cancel_recording_args { void stasis_http_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_stop_recording() */ struct ast_stop_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Stop a live recording and store it. @@ -128,12 +128,14 @@ struct ast_stop_recording_args { void stasis_http_stop_recording(struct ast_variable *headers, struct ast_stop_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_pause_recording() */ struct ast_pause_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Pause a live recording. * + * Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused. + * * \param headers HTTP headers * \param args Swagger parameters * \param[out] response HTTP response @@ -141,8 +143,8 @@ struct ast_pause_recording_args { void stasis_http_pause_recording(struct ast_variable *headers, struct ast_pause_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_unpause_recording() */ struct ast_unpause_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Unpause a live recording. @@ -154,12 +156,14 @@ struct ast_unpause_recording_args { void stasis_http_unpause_recording(struct ast_variable *headers, struct ast_unpause_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_mute_recording() */ struct ast_mute_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Mute a live recording. * + * Muting a recording suspends silence detection, which will be restarted when the recording is unmuted. + * * \param headers HTTP headers * \param args Swagger parameters * \param[out] response HTTP response @@ -167,8 +171,8 @@ struct ast_mute_recording_args { void stasis_http_mute_recording(struct ast_variable *headers, struct ast_mute_recording_args *args, struct stasis_http_response *response); /*! \brief Argument struct for stasis_http_unmute_recording() */ struct ast_unmute_recording_args { - /*! \brief Recording's id */ - const char *recording_id; + /*! \brief The name of the recording */ + const char *recording_name; }; /*! * \brief Unmute a live recording. -- cgit v1.2.3