diff --git a/Makefile b/Makefile index a515344242..d896c3e360 100644 --- a/Makefile +++ b/Makefile @@ -536,7 +536,8 @@ OLDHEADERS=$(filter-out $(NEWHEADERS) $(notdir $(DESTDIR)$(ASTHEADERDIR)),$(notd INSTALLDIRS="$(ASTLIBDIR)" "$(ASTMODDIR)" "$(ASTSBINDIR)" "$(ASTETCDIR)" "$(ASTVARRUNDIR)" \ "$(ASTSPOOLDIR)" "$(ASTSPOOLDIR)/dictate" "$(ASTSPOOLDIR)/meetme" \ "$(ASTSPOOLDIR)/monitor" "$(ASTSPOOLDIR)/system" "$(ASTSPOOLDIR)/tmp" \ - "$(ASTSPOOLDIR)/voicemail" "$(ASTHEADERDIR)" "$(ASTHEADERDIR)/doxygen" \ + "$(ASTSPOOLDIR)/voicemail" "$(ASTSPOOLDIR)/recording" \ + "$(ASTHEADERDIR)" "$(ASTHEADERDIR)/doxygen" \ "$(ASTLOGDIR)" "$(ASTLOGDIR)/cdr-csv" "$(ASTLOGDIR)/cdr-custom" \ "$(ASTLOGDIR)/cel-custom" "$(ASTDATADIR)" "$(ASTDATADIR)/documentation" \ "$(ASTDATADIR)/documentation/thirdparty" "$(ASTDATADIR)/firmware" \ diff --git a/apps/app_minivm.c b/apps/app_minivm.c index ba6d6e5a29..2b6f7e4b87 100644 --- a/apps/app_minivm.c +++ b/apps/app_minivm.c @@ -1674,7 +1674,7 @@ static int play_record_review(struct ast_channel *chan, char *playfile, char *re ast_channel_setoption(chan, AST_OPTION_RXGAIN, &record_gain, sizeof(record_gain), 0); if (ast_test_flag(vmu, MVM_OPERATOR)) canceldtmf = "0"; - cmd = ast_play_and_record_full(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, global_silencethreshold, global_maxsilence, unlockdir, acceptdtmf, canceldtmf); + cmd = ast_play_and_record_full(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, global_silencethreshold, global_maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE); if (record_gain) ast_channel_setoption(chan, AST_OPTION_RXGAIN, &zero_gain, sizeof(zero_gain), 0); if (cmd == -1) /* User has hung up, no options to give */ diff --git a/apps/app_voicemail.c b/apps/app_voicemail.c index 90458bb31d..95265b5b10 100644 --- a/apps/app_voicemail.c +++ b/apps/app_voicemail.c @@ -14684,7 +14684,7 @@ static int play_record_review(struct ast_channel *chan, char *playfile, char *re ast_channel_setoption(chan, AST_OPTION_RXGAIN, &record_gain, sizeof(record_gain), 0); if (ast_test_flag(vmu, VM_OPERATOR)) canceldtmf = "0"; - cmd = ast_play_and_record_full(chan, playfile, tempfile, maxtime, fmt, duration, sound_duration, silencethreshold, maxsilence, unlockdir, acceptdtmf, canceldtmf); + cmd = ast_play_and_record_full(chan, playfile, tempfile, maxtime, fmt, duration, sound_duration, silencethreshold, maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE); if (strchr(canceldtmf, cmd)) { /* need this flag here to distinguish between pressing '0' during message recording or after */ canceleddtmf = 1; diff --git a/include/asterisk/app.h b/include/asterisk/app.h index 7ddacfc4e9..91438a2d0f 100644 --- a/include/asterisk/app.h +++ b/include/asterisk/app.h @@ -690,9 +690,23 @@ int ast_control_streamfile_w_cb(struct ast_channel *chan, /*! \brief Play a stream and wait for a digit, returning the digit that was pressed */ int ast_play_and_wait(struct ast_channel *chan, const char *fn); +/*! + * Possible actions to take if a recording already exists + * \since 12 + */ +enum ast_record_if_exists { + /*! Fail the recording. */ + AST_RECORD_IF_EXISTS_FAIL, + /*! Overwrite the existing recording. */ + AST_RECORD_IF_EXISTS_OVERWRITE, + /*! Append to the existing recording. */ + AST_RECORD_IF_EXISTS_APPEND, +}; + /*! * \brief Record a file based on input from a channel - * This function will play "auth-thankyou" upon successful recording. + * This function will play "auth-thankyou" upon successful recording if + * skip_confirmation_sound is false. * * \param chan the channel being recorded * \param playfile Filename of sound to play before recording begins @@ -706,13 +720,15 @@ int ast_play_and_wait(struct ast_channel *chan, const char *fn); * \param path Optional filesystem path to unlock * \param acceptdtmf Character of DTMF to end and accept the recording * \param canceldtmf Character of DTMF to end and cancel the recording + * \param skip_confirmation_sound If true, don't play auth-thankyou at end. Nice for custom recording prompts in apps. + * \param if_exists Action to take if recording already exists. * * \retval -1 failure or hangup * \retval 'S' Recording ended from silence timeout * \retval 't' Recording ended from the message exceeding the maximum duration * \retval dtmfchar Recording ended via the return value's DTMF character for either cancel or accept. */ -int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime_sec, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence_ms, const char *path, const char *acceptdtmf, const char *canceldtmf); +int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime_sec, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence_ms, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists); /*! * \brief Record a file based on input from a channel. Use default accept and cancel DTMF. diff --git a/include/asterisk/channel.h b/include/asterisk/channel.h index fae43d423b..d61494141d 100644 --- a/include/asterisk/channel.h +++ b/include/asterisk/channel.h @@ -1603,6 +1603,18 @@ void ast_channel_setwhentohangup_tv(struct ast_channel *chan, struct timeval off */ int ast_answer(struct ast_channel *chan); +/*! + * \brief Answer a channel, if it's not already answered. + * + * \param chan channel to answer + * + * \details See ast_answer() + * + * \retval 0 on success + * \retval non-zero on failure + */ +int ast_auto_answer(struct ast_channel *chan); + /*! * \brief Answer a channel * diff --git a/include/asterisk/file.h b/include/asterisk/file.h index 844b434293..372c0f7ed2 100644 --- a/include/asterisk/file.h +++ b/include/asterisk/file.h @@ -64,8 +64,8 @@ enum ast_waitstream_fr_cb_values { */ typedef void (ast_waitstream_fr_cb)(struct ast_channel *chan, long ms, enum ast_waitstream_fr_cb_values val); -/*! - * \brief Streams a file +/*! + * \brief Streams a file * \param c channel to stream the file to * \param filename the name of the file you wish to stream, minus the extension * \param preflang the preferred language you wish to have the file streamed to you in @@ -86,12 +86,12 @@ int ast_streamfile(struct ast_channel *c, const char *filename, const char *pref */ int ast_stream_and_wait(struct ast_channel *chan, const char *file, const char *digits); -/*! - * \brief Stops a stream +/*! + * \brief Stops a stream * * \param c The channel you wish to stop playback on * - * Stop playback of a stream + * Stop playback of a stream * * \retval 0 always * diff --git a/include/asterisk/paths.h b/include/asterisk/paths.h index 14da7aaf99..ea0c561237 100644 --- a/include/asterisk/paths.h +++ b/include/asterisk/paths.h @@ -23,6 +23,7 @@ extern const char *ast_config_AST_CONFIG_FILE; extern const char *ast_config_AST_MODULE_DIR; extern const char *ast_config_AST_SPOOL_DIR; extern const char *ast_config_AST_MONITOR_DIR; +extern const char *ast_config_AST_RECORDING_DIR; extern const char *ast_config_AST_VAR_DIR; extern const char *ast_config_AST_DATA_DIR; extern const char *ast_config_AST_LOG_DIR; diff --git a/include/asterisk/stasis_app_recording.h b/include/asterisk/stasis_app_recording.h new file mode 100644 index 0000000000..9c9930406b --- /dev/null +++ b/include/asterisk/stasis_app_recording.h @@ -0,0 +1,203 @@ +/* + * 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. + */ + +#ifndef _ASTERISK_STASIS_APP_RECORDING_H +#define _ASTERISK_STASIS_APP_RECORDING_H + +/*! \file + * + * \brief Stasis Application Recording API. See \ref res_stasis "Stasis + * Application API" for detailed documentation. + * + * \author David M. Lee, II + * \since 12 + */ + +#include "asterisk/app.h" +#include "asterisk/stasis_app.h" + +/*! Opaque struct for handling the recording of media to a file. */ +struct stasis_app_recording; + +/*! State of a recording operation */ +enum stasis_app_recording_state { + /*! The recording has not started yet */ + STASIS_APP_RECORDING_STATE_QUEUED, + /*! The media is currently recording */ + STASIS_APP_RECORDING_STATE_RECORDING, + /*! The media is currently paused */ + STASIS_APP_RECORDING_STATE_PAUSED, + /*! The media has stopped recording */ + STASIS_APP_RECORDING_STATE_COMPLETE, + /*! The media has stopped playing */ + STASIS_APP_RECORDING_STATE_FAILED, +}; + +/*! Valid operation for controlling a recording. */ +enum stasis_app_recording_media_operation { + /*! Stop the recording operation. */ + STASIS_APP_RECORDING_STOP, +}; + +#define STASIS_APP_RECORDING_TERMINATE_INVALID 0 +#define STASIS_APP_RECORDING_TERMINATE_NONE -1 +#define STASIS_APP_RECORDING_TERMINATE_ANY -2 + +struct stasis_app_recording_options { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(name); /*!< name Name of the recording. */ + AST_STRING_FIELD(format); /*!< Format to be recorded (wav, gsm, etc.) */ + ); + /*! Number of seconds of silence before ending the recording. */ + int max_silence_seconds; + /*! Maximum recording duration. 0 for no maximum. */ + int max_duration_seconds; + /*! Which DTMF to use to terminate the recording + * \c STASIS_APP_RECORDING_TERMINATE_NONE to terminate only on hangup + * \c STASIS_APP_RECORDING_TERMINATE_ANY to terminate on any DTMF + */ + char terminate_on; + /*! How to handle recording when a file already exists */ + enum ast_record_if_exists if_exists; + /*! If true, a beep is played at the start of recording */ + int beep:1; +}; + +/*! + * \brief Allocate a recording options object. + * + * Clean up with ao2_cleanup(). + * + * \param name Name of the recording. + * \param format Format to record in. + * \return Newly allocated options object. + * \return \c NULL on error. + */ +struct stasis_app_recording_options *stasis_app_recording_options_create( + const char *name, const char *format); + +/*! + * \brief Parse a string into the recording termination enum. + * + * \param str String to parse. + * \return DTMF value to terminate on. + * \return \c STASIS_APP_RECORDING_TERMINATE_NONE to not terminate on DTMF. + * \return \c STASIS_APP_RECORDING_TERMINATE_ANY to terminate on any DTMF. + * \return \c STASIS_APP_RECORDING_TERMINATE_INVALID if input was invalid. + */ +char stasis_app_recording_termination_parse(const char *str); + +/*! + * \brief Parse a string into the if_exists enum. + * + * \param str String to parse. + * \return How to handle an existing file. + * \return -1 on error. + */ +enum ast_record_if_exists stasis_app_recording_if_exists_parse( + const char *str); + +/*! + * \brief Record media from a channel. + * + * A reference to the \a options object may be kept, so it MUST NOT be modified + * after calling this function. + * + * On error, \c errno is set to indicate the failure reason. + * - \c EINVAL: Invalid input. + * - \c EEXIST: A recording with that name is in session. + * - \c ENOMEM: Out of memory. + * + * \param control Control for \c res_stasis. + * \param options Recording options. + * \return Recording control object. + * \return \c NULL on error. + */ +struct stasis_app_recording *stasis_app_control_record( + struct stasis_app_control *control, + struct stasis_app_recording_options *options); + +/*! + * \brief Gets the current state of a recording operation. + * + * \param recording Recording control object. + * \return The state of the \a recording object. + */ +enum stasis_app_recording_state stasis_app_recording_get_state( + struct stasis_app_recording *recording); + +/*! + * \brief Gets the unique name of a recording object. + * + * \param recording Recording control object. + * \return \a recording's name. + * \return \c NULL if \a recording ic \c NULL + */ +const char *stasis_app_recording_get_name( + struct stasis_app_recording *recording); + +/*! + * \brief Finds the recording object with the given name. + * + * \param name Name of the recording object to find. + * \return Associated \ref stasis_app_recording object. + * \return \c NULL if \a name not found. + */ +struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name); + +/*! + * \brief Construct a JSON model of a recording. + * + * \param recording Recording to conver. + * \return JSON model. + * \return \c NULL on error. + */ +struct ast_json *stasis_app_recording_to_json( + const struct stasis_app_recording *recording); + +/*! + * \brief Possible results from a recording operation. + */ +enum stasis_app_recording_oper_results { + /*! Operation completed successfully. */ + STASIS_APP_RECORDING_OPER_OK, + /*! Operation failed. */ + STASIS_APP_RECORDING_OPER_FAILED, + /*! Operation failed b/c recording is not in session. */ + STASIS_APP_RECORDING_OPER_NOT_RECORDING, +}; + +/*! + * \brief Controls the media for a given recording operation. + * + * \param recording Recording control object. + * \param control Media control operation. + * \return \c STASIS_APP_RECORDING_OPER_OK on success. + * \return \ref stasis_app_recording_oper_results indicating failure. + */ +enum stasis_app_recording_oper_results stasis_app_recording_operation( + struct stasis_app_recording *recording, + enum stasis_app_recording_media_operation operation); + +/*! + * \brief Message type for recording updates. The data is an + * \ref ast_channel_blob. + */ +struct stasis_message_type *stasis_app_recording_snapshot_type(void); + +#endif /* _ASTERISK_STASIS_APP_RECORDING_H */ diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h index ce6db0965b..1848509053 100644 --- a/include/asterisk/utils.h +++ b/include/asterisk/utils.h @@ -718,6 +718,19 @@ void ast_enable_packet_fragmentation(int sock); */ int ast_mkdir(const char *path, int mode); +/*! + * \brief Recursively create directory path, but only if it resolves within + * the given \a base_path. + * + * If \a base_path does not exist, it will not be created and this function + * returns \c EPERM. + * + * \param path The directory path to create + * \param mode The permissions with which to try to create the directory + * \return 0 on success or an error code otherwise + */ +int ast_safe_mkdir(const char *base_path, const char *path, int mode); + #define ARRAY_LEN(a) (size_t) (sizeof(a) / sizeof(0[a])) diff --git a/main/app.c b/main/app.c index a7a9029c95..031f6f28f5 100644 --- a/main/app.c +++ b/main/app.c @@ -1169,7 +1169,7 @@ static int global_maxsilence = 0; * \retval 't' Recording ended from the message exceeding the maximum duration, or via DTMF in prepend mode * \retval dtmfchar Recording ended via the return value's DTMF character for either cancel or accept. */ -static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence, const char *path, int prepend, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound) +static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence, const char *path, int prepend, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists) { int d = 0; char *fmts; @@ -1186,6 +1186,21 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, struct ast_format rfmt; struct ast_silence_generator *silgen = NULL; char prependfile[PATH_MAX]; + int ioflags; /* IO flags for writing output file */ + + ioflags = O_CREAT|O_WRONLY; + + switch (if_exists) { + case AST_RECORD_IF_EXISTS_FAIL: + ioflags |= O_EXCL; + break; + case AST_RECORD_IF_EXISTS_OVERWRITE: + ioflags |= O_TRUNC; + break; + case AST_RECORD_IF_EXISTS_APPEND: + ioflags |= O_APPEND; + break; + } ast_format_clear(&rfmt); if (silencethreshold < 0) { @@ -1239,7 +1254,7 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, end = start = time(NULL); /* pre-initialize end to be same as start in case we never get into loop */ for (x = 0; x < fmtcnt; x++) { - others[x] = ast_writefile(prepend ? prependfile : recordfile, sfmt[x], comment, O_TRUNC, 0, AST_FILE_MODE); + others[x] = ast_writefile(prepend ? prependfile : recordfile, sfmt[x], comment, ioflags, 0, AST_FILE_MODE); ast_verb(3, "x=%d, open writing: %s format: %s, %p\n", x, prepend ? prependfile : recordfile, sfmt[x], others[x]); if (!others[x]) { @@ -1477,19 +1492,19 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, static const char default_acceptdtmf[] = "#"; static const char default_canceldtmf[] = ""; -int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path, const char *acceptdtmf, const char *canceldtmf) +int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists) { - return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, S_OR(acceptdtmf, default_acceptdtmf), S_OR(canceldtmf, default_canceldtmf), 0); + return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, S_OR(acceptdtmf, default_acceptdtmf), S_OR(canceldtmf, default_canceldtmf), skip_confirmation_sound, if_exists); } int ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path) { - return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, default_acceptdtmf, default_canceldtmf, 0); + return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, default_acceptdtmf, default_canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE); } int ast_play_and_prepend(struct ast_channel *chan, char *playfile, char *recordfile, int maxtime, char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence) { - return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, beep, silencethreshold, maxsilence, NULL, 1, default_acceptdtmf, default_canceldtmf, 1); + return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, beep, silencethreshold, maxsilence, NULL, 1, default_acceptdtmf, default_canceldtmf, 1, AST_RECORD_IF_EXISTS_OVERWRITE); } /* Channel group core functions */ diff --git a/main/asterisk.c b/main/asterisk.c index c1ce2c0f18..aa33f31e48 100644 --- a/main/asterisk.c +++ b/main/asterisk.c @@ -373,6 +373,7 @@ struct _cfg_paths { char module_dir[PATH_MAX]; char spool_dir[PATH_MAX]; char monitor_dir[PATH_MAX]; + char recording_dir[PATH_MAX]; char var_dir[PATH_MAX]; char data_dir[PATH_MAX]; char log_dir[PATH_MAX]; @@ -397,6 +398,7 @@ const char *ast_config_AST_CONFIG_FILE = cfg_paths.config_file; const char *ast_config_AST_MODULE_DIR = cfg_paths.module_dir; const char *ast_config_AST_SPOOL_DIR = cfg_paths.spool_dir; const char *ast_config_AST_MONITOR_DIR = cfg_paths.monitor_dir; +const char *ast_config_AST_RECORDING_DIR = cfg_paths.recording_dir; const char *ast_config_AST_VAR_DIR = cfg_paths.var_dir; const char *ast_config_AST_DATA_DIR = cfg_paths.data_dir; const char *ast_config_AST_LOG_DIR = cfg_paths.log_dir; @@ -3306,6 +3308,7 @@ static void ast_readconfig(void) ast_copy_string(cfg_paths.spool_dir, DEFAULT_SPOOL_DIR, sizeof(cfg_paths.spool_dir)); ast_copy_string(cfg_paths.module_dir, DEFAULT_MODULE_DIR, sizeof(cfg_paths.module_dir)); snprintf(cfg_paths.monitor_dir, sizeof(cfg_paths.monitor_dir), "%s/monitor", cfg_paths.spool_dir); + snprintf(cfg_paths.recording_dir, sizeof(cfg_paths.recording_dir), "%s/recording", cfg_paths.spool_dir); ast_copy_string(cfg_paths.var_dir, DEFAULT_VAR_DIR, sizeof(cfg_paths.var_dir)); ast_copy_string(cfg_paths.data_dir, DEFAULT_DATA_DIR, sizeof(cfg_paths.data_dir)); ast_copy_string(cfg_paths.log_dir, DEFAULT_LOG_DIR, sizeof(cfg_paths.log_dir)); @@ -3341,6 +3344,7 @@ static void ast_readconfig(void) } else if (!strcasecmp(v->name, "astspooldir")) { ast_copy_string(cfg_paths.spool_dir, v->value, sizeof(cfg_paths.spool_dir)); snprintf(cfg_paths.monitor_dir, sizeof(cfg_paths.monitor_dir), "%s/monitor", v->value); + snprintf(cfg_paths.recording_dir, sizeof(cfg_paths.recording_dir), "%s/recording", v->value); } else if (!strcasecmp(v->name, "astvarlibdir")) { ast_copy_string(cfg_paths.var_dir, v->value, sizeof(cfg_paths.var_dir)); if (!found.dbdir) diff --git a/main/channel.c b/main/channel.c index 9d1ec69c27..3bc5c0a75c 100644 --- a/main/channel.c +++ b/main/channel.c @@ -3029,6 +3029,15 @@ int ast_answer(struct ast_channel *chan) return __ast_answer(chan, 0); } +inline int ast_auto_answer(struct ast_channel *chan) +{ + if (ast_channel_state(chan) == AST_STATE_UP) { + /* Already answered */ + return 0; + } + return ast_answer(chan); +} + int ast_channel_get_duration(struct ast_channel *chan) { ast_assert(NULL != chan); diff --git a/main/file.c b/main/file.c index 016afd197d..cb495b3100 100644 --- a/main/file.c +++ b/main/file.c @@ -1020,6 +1020,9 @@ int ast_closestream(struct ast_filestream *f) * We close the stream in order to quit queuing frames now, because we might * change the writeformat, which could result in a subsequent write error, if * the format is different. */ + if (f == NULL) { + return 0; + } filestream_close(f); ao2_ref(f, -1); return 0; diff --git a/main/utils.c b/main/utils.c index 208a4d3261..04f6127033 100644 --- a/main/utils.c +++ b/main/utils.c @@ -2105,6 +2105,100 @@ int ast_mkdir(const char *path, int mode) return 0; } +static int safe_mkdir(const char *base_path, char *path, int mode) +{ + RAII_VAR(char *, absolute_path, NULL, free); + + absolute_path = realpath(path, NULL); + + if (absolute_path) { + /* Path exists, but is it in the right place? */ + if (!ast_begins_with(absolute_path, base_path)) { + return EPERM; + } + + /* It is in the right place! */ + return 0; + } else { + /* Path doesn't exist. */ + + /* The slash terminating the subpath we're checking */ + char *path_term = strchr(path, '/'); + /* True indicates the parent path is within base_path */ + int parent_is_safe = 0; + int res; + + while (path_term) { + RAII_VAR(char *, absolute_subpath, NULL, free); + + /* Truncate the path one past the slash */ + char c = *(path_term + 1); + *(path_term + 1) = '\0'; + absolute_subpath = realpath(path, NULL); + + if (absolute_subpath) { + /* Subpath exists, but is it safe? */ + parent_is_safe = ast_begins_with( + absolute_subpath, base_path); + } else if (parent_is_safe) { + /* Subpath does not exist, but parent is safe + * Create it */ + res = mkdir(path, mode); + if (res != 0) { + ast_assert(errno != EEXIST); + return errno; + } + } else { + /* Subpath did not exist, parent was not safe + * Fail! */ + errno = EPERM; + return errno; + } + /* Restore the path */ + *(path_term + 1) = c; + /* Move on to the next slash */ + path_term = strchr(path_term + 1, '/'); + } + + /* Now to build the final path, but only if it's safe */ + if (!parent_is_safe) { + errno = EPERM; + return errno; + } + + res = mkdir(path, mode); + if (res != 0 && errno != EEXIST) { + return errno; + } + + return 0; + } +} + +int ast_safe_mkdir(const char *base_path, const char *path, int mode) +{ + RAII_VAR(char *, absolute_base_path, NULL, free); + RAII_VAR(char *, p, NULL, ast_free); + + if (base_path == NULL || path == NULL) { + errno = EFAULT; + return errno; + } + + p = ast_strdup(path); + if (p == NULL) { + errno = ENOMEM; + return errno; + } + + absolute_base_path = realpath(base_path, NULL); + if (absolute_base_path == NULL) { + return errno; + } + + return safe_mkdir(absolute_base_path, p, mode); +} + int ast_utils_init(void) { dev_urandom_fd = open("/dev/urandom", O_RDONLY); diff --git a/res/res_stasis_http_bridges.c b/res/res_stasis_http_bridges.c index a4801df13d..878c1ce0ae 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 ebcc9e8800..5343714b1a 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 4aa43c9be3..5b80432513 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 3b092df2df..5b55ebc51e 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 0000000000..3d8e11bbd7 --- /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 0000000000..0ad493c49e --- /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 0fbb754871..8db3b697c1 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 57f2a63d20..7e8dc5dbe6 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 7d31c42aa2..d93d59017c 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 acccc124bb..18a5bfe683 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. diff --git a/rest-api-templates/asterisk_processor.py b/rest-api-templates/asterisk_processor.py index 0260b6b55b..6f69b48659 100644 --- a/rest-api-templates/asterisk_processor.py +++ b/rest-api-templates/asterisk_processor.py @@ -139,10 +139,11 @@ class AsteriskProcessor(SwaggerPostProcessor): #: String conversion functions for string to C type. convert_mapping = { - 'const char *': '', + 'string': '', 'int': 'atoi', 'long': 'atol', 'double': 'atof', + 'boolean': 'ast_true', } def __init__(self, wiki_prefix): @@ -194,7 +195,7 @@ class AsteriskProcessor(SwaggerPostProcessor): # Parameter names are camelcase, Asterisk convention is snake case parameter.c_name = snakify(parameter.name) parameter.c_data_type = self.type_mapping[parameter.data_type] - parameter.c_convert = self.convert_mapping[parameter.c_data_type] + parameter.c_convert = self.convert_mapping[parameter.data_type] # You shouldn't put a space between 'char *' and the variable if parameter.c_data_type.endswith('*'): parameter.c_space = '' diff --git a/rest-api-templates/swagger_model.py b/rest-api-templates/swagger_model.py index 2907688c52..aa065b342b 100644 --- a/rest-api-templates/swagger_model.py +++ b/rest-api-templates/swagger_model.py @@ -246,11 +246,9 @@ def load_allowable_values(json, context): value_type = json['valueType'] if value_type == 'RANGE': - if not 'min' in json: - raise SwaggerError("Missing field min", context) - if not 'max' in json: - raise SwaggerError("Missing field max", context) - return AllowableRange(json['min'], json['max']) + if not 'min' in json and not 'max' in json: + raise SwaggerError("Missing fields min/max", context) + return AllowableRange(json.get('min'), json.get('max')) if value_type == 'LIST': if not 'values' in json: raise SwaggerError("Missing field values", context) diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json index f013ef6416..9900db7394 100644 --- a/rest-api/api-docs/channels.json +++ b/rest-api/api-docs/channels.json @@ -565,7 +565,11 @@ "required": false, "allowMultiple": false, "dataType": "int", - "defaultValue": 0 + "defaultValue": 0, + "allowableValues": { + "valueType": "RANGE", + "min": 0 + } }, { "name": "maxSilenceSeconds", @@ -574,16 +578,28 @@ "required": false, "allowMultiple": false, "dataType": "int", - "defaultValue": 0 + "defaultValue": 0, + "allowableValues": { + "valueType": "RANGE", + "min": 0 + } }, { - "name": "append", - "description": "If true, and recording already exists, append to recording", + "name": "ifExists", + "description": "Action to take if a recording with the same name already exists.", "paramType": "query", "required": false, "allowMultiple": false, - "dataType": "boolean", - "defaultValue": false + "dataType": "string", + "defaultValue": "fail", + "allowableValues": { + "valueType": "LIST", + "values": [ + "fail", + "overwrite", + "append" + ] + } }, { "name": "beep", @@ -614,13 +630,17 @@ } ], "errorResponses": [ + { + "code": 400, + "reason": "Invalid parameters" + }, { "code": 404, "reason": "Channel not found" }, { "code": 409, - "reason": "Channel is not in a Stasis application, or the channel is currently bridged with other channels." + "reason": "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." } ] } diff --git a/rest-api/api-docs/recordings.json b/rest-api/api-docs/recordings.json index ce11d17c25..9efdc7bb31 100644 --- a/rest-api/api-docs/recordings.json +++ b/rest-api/api-docs/recordings.json @@ -20,7 +20,7 @@ ] }, { - "path": "/recordings/stored/{recordingId}", + "path": "/recordings/stored/{recordingName}", "description": "Individual recording", "operations": [ { @@ -30,8 +30,8 @@ "responseClass": "StoredRecording", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -46,8 +46,8 @@ "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -70,7 +70,7 @@ ] }, { - "path": "/recordings/live/{recordingId}", + "path": "/recordings/live/{recordingName}", "description": "A recording that is in progress", "operations": [ { @@ -80,8 +80,8 @@ "responseClass": "LiveRecording", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -96,8 +96,8 @@ "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -108,7 +108,7 @@ ] }, { - "path": "/recordings/live/{recordingId}/stop", + "path": "/recordings/live/{recordingName}/stop", "operations": [ { "httpMethod": "POST", @@ -117,8 +117,8 @@ "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -129,17 +129,18 @@ ] }, { - "path": "/recordings/live/{recordingId}/pause", + "path": "/recordings/live/{recordingName}/pause", "operations": [ { "httpMethod": "POST", "summary": "Pause a live recording.", + "notes": "Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused.", "nickname": "pauseRecording", "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -150,7 +151,7 @@ ] }, { - "path": "/recordings/live/{recordingId}/unpause", + "path": "/recordings/live/{recordingName}/unpause", "operations": [ { "httpMethod": "POST", @@ -159,8 +160,8 @@ "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -171,17 +172,18 @@ ] }, { - "path": "/recordings/live/{recordingId}/mute", + "path": "/recordings/live/{recordingName}/mute", "operations": [ { "httpMethod": "POST", "summary": "Mute a live recording.", + "notes": "Muting a recording suspends silence detection, which will be restarted when the recording is unmuted.", "nickname": "muteRecording", "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, @@ -192,7 +194,7 @@ ] }, { - "path": "/recordings/live/{recordingId}/unmute", + "path": "/recordings/live/{recordingName}/unmute", "operations": [ { "httpMethod": "POST", @@ -201,8 +203,8 @@ "responseClass": "void", "parameters": [ { - "name": "recordingId", - "description": "Recording's id", + "name": "recordingName", + "description": "The name of the recording", "paramType": "path", "required": true, "allowMultiple": false, diff --git a/tests/test_utils.c b/tests/test_utils.c index 7cc4cf611b..f956e5b27b 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -42,6 +42,8 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$"); #include "asterisk/channel.h" #include "asterisk/module.h" +#include + AST_TEST_DEFINE(uri_encode_decode_test) { int res = AST_TEST_PASS; @@ -421,6 +423,93 @@ AST_TEST_DEFINE(agi_loaded_test) return res; } +AST_TEST_DEFINE(safe_mkdir_test) +{ + char base_path[] = "/tmp/safe_mkdir.XXXXXX"; + char path[80] = {}; + int res; + struct stat actual; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = "/main/utils/"; + info->summary = "Safe mkdir test"; + info->description = + "This test ensures that ast_safe_mkdir does what it is " + "supposed to"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (mkdtemp(base_path) == NULL) { + ast_test_status_update(test, "Failed to create tmpdir for test\n"); + return AST_TEST_FAIL; + } + + snprintf(path, sizeof(path), "%s/should_work", base_path); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 == res); + res = stat(path, &actual); + ast_test_validate(test, 0 == res); + ast_test_validate(test, S_ISDIR(actual.st_mode)); + + snprintf(path, sizeof(path), "%s/should/also/work", base_path); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 == res); + res = stat(path, &actual); + ast_test_validate(test, 0 == res); + ast_test_validate(test, S_ISDIR(actual.st_mode)); + + snprintf(path, sizeof(path), "%s/even/this/../should/work", base_path); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 == res); + snprintf(path, sizeof(path), "%s/even/should/work", base_path); + res = stat(path, &actual); + ast_test_validate(test, 0 == res); + ast_test_validate(test, S_ISDIR(actual.st_mode)); + + snprintf(path, sizeof(path), + "%s/surprisingly/this/should//////////////////work", base_path); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 == res); + snprintf(path, sizeof(path), + "%s/surprisingly/this/should/work", base_path); + res = stat(path, &actual); + ast_test_validate(test, 0 == res); + ast_test_validate(test, S_ISDIR(actual.st_mode)); + + snprintf(path, sizeof(path), "/should_not_work"); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 != res); + ast_test_validate(test, EPERM == errno); + res = stat(path, &actual); + ast_test_validate(test, 0 != res); + ast_test_validate(test, ENOENT == errno); + + snprintf(path, sizeof(path), "%s/../nor_should_this", base_path); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 != res); + ast_test_validate(test, EPERM == errno); + strncpy(path, "/tmp/nor_should_this", sizeof(path)); + res = stat(path, &actual); + ast_test_validate(test, 0 != res); + ast_test_validate(test, ENOENT == errno); + + snprintf(path, sizeof(path), + "%s/this/especially/should/not/../../../../../work", base_path); + res = ast_safe_mkdir(base_path, path, 0777); + ast_test_validate(test, 0 != res); + ast_test_validate(test, EPERM == errno); + strncpy(path, "/tmp/work", sizeof(path)); + res = stat(path, &actual); + ast_test_validate(test, 0 != res); + ast_test_validate(test, ENOENT == errno); + + return AST_TEST_PASS; +} + AST_TEST_DEFINE(crypt_test) { RAII_VAR(char *, password_crypted, NULL, ast_free); @@ -467,6 +556,7 @@ static int unload_module(void) AST_TEST_UNREGISTER(crypto_loaded_test); AST_TEST_UNREGISTER(adsi_loaded_test); AST_TEST_UNREGISTER(agi_loaded_test); + AST_TEST_UNREGISTER(safe_mkdir_test); AST_TEST_UNREGISTER(crypt_test); return 0; } @@ -481,6 +571,7 @@ static int load_module(void) AST_TEST_REGISTER(crypto_loaded_test); AST_TEST_REGISTER(adsi_loaded_test); AST_TEST_REGISTER(agi_loaded_test); + AST_TEST_REGISTER(safe_mkdir_test); AST_TEST_REGISTER(crypt_test); return AST_MODULE_LOAD_SUCCESS; }