MALLOC_DEBUG: Fix some misuses of free() when MALLOC_DEBUG is enabled.
[asterisk/asterisk.git] / res / stasis_recording / stored.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2013, Digium, Inc.
5  *
6  * David M. Lee, II <dlee@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Stored file operations for Stasis
22  *
23  * \author David M. Lee, II <dlee@digium.com>
24  */
25
26 #include "asterisk.h"
27
28 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
29
30 #include "asterisk/astobj2.h"
31 #include "asterisk/paths.h"
32 #include "asterisk/stasis_app_recording.h"
33
34 #include <dirent.h>
35 #include <sys/stat.h>
36 #include <sys/types.h>
37 #include <unistd.h>
38
39 struct stasis_app_stored_recording {
40         AST_DECLARE_STRING_FIELDS(
41                 AST_STRING_FIELD(name); /*!< Recording's name */
42                 AST_STRING_FIELD(file); /*!< Absolute filename, without extension; for use with streamfile */
43                 AST_STRING_FIELD(file_with_ext);        /*!< Absolute filename, with extension; for use with everything else */
44                 );
45
46         const char *format;     /*!< Format name (i.e. filename extension) */
47 };
48
49 static void stored_recording_dtor(void *obj)
50 {
51         struct stasis_app_stored_recording *recording = obj;
52
53         ast_string_field_free_memory(recording);
54 }
55
56 const char *stasis_app_stored_recording_get_file(
57         struct stasis_app_stored_recording *recording)
58 {
59         if (!recording) {
60                 return NULL;
61         }
62         return recording->file;
63 }
64
65 /*!
66  * \brief Split a path into directory and file, resolving canonical directory.
67  *
68  * The path is resolved relative to the recording directory. Both dir and file
69  * are allocated strings, which you must ast_free().
70  *
71  * \param path Path to split.
72  * \param[out] dir Output parameter for directory portion.
73  * \param[out] fail Output parameter for the file portion.
74  * \return 0 on success.
75  * \return Non-zero on error.
76  */
77 static int split_path(const char *path, char **dir, char **file)
78 {
79         RAII_VAR(char *, relative_dir, NULL, ast_free);
80         RAII_VAR(char *, absolute_dir, NULL, ast_free);
81         RAII_VAR(char *, real_dir, NULL, ast_std_free);
82         char *last_slash;
83         const char *file_portion;
84
85         relative_dir = ast_strdup(path);
86         if (!relative_dir) {
87                 return -1;
88         }
89
90         last_slash = strrchr(relative_dir, '/');
91         if (last_slash) {
92                 *last_slash = '\0';
93                 file_portion = last_slash + 1;
94                 ast_asprintf(&absolute_dir, "%s/%s",
95                         ast_config_AST_RECORDING_DIR, relative_dir);
96         } else {
97                 /* There is no directory portion */
98                 file_portion = path;
99                 *relative_dir = '\0';
100                 absolute_dir = ast_strdup(ast_config_AST_RECORDING_DIR);
101         }
102         if (!absolute_dir) {
103                 return -1;
104         }
105
106         real_dir = realpath(absolute_dir, NULL);
107         if (!real_dir) {
108                 return -1;
109         }
110
111 #if defined(__AST_DEBUG_MALLOC)
112         *dir = ast_strdup(real_dir); /* Dupe so we can ast_free() */
113 #else
114         /*
115          * ast_std_free() and ast_free() are the same thing at this time
116          * so we don't need to dupe.
117          */
118         *dir = real_dir;
119         real_dir = NULL;
120 #endif  /* defined(__AST_DEBUG_MALLOC) */
121         *file = ast_strdup(file_portion);
122         return 0;
123 }
124
125 static void safe_closedir(DIR *dirp)
126 {
127         if (!dirp) {
128                 return;
129         }
130         closedir(dirp);
131 }
132
133 /*!
134  * \brief Finds a recording in the given directory.
135  *
136  * This function searchs for a file with the given file name, with a registered
137  * format that matches its extension.
138  *
139  * \param dir_name Directory to search (absolute path).
140  * \param file File name, without extension.
141  * \return Absolute path of the recording file.
142  * \return \c NULL if recording is not found.
143  */
144 static char *find_recording(const char *dir_name, const char *file)
145 {
146         RAII_VAR(DIR *, dir, NULL, safe_closedir);
147         struct dirent entry;
148         struct dirent *result = NULL;
149         char *ext = NULL;
150         char *file_with_ext = NULL;
151
152         dir = opendir(dir_name);
153         if (!dir) {
154                 return NULL;
155         }
156
157         while (readdir_r(dir, &entry, &result) == 0 && result != NULL) {
158                 ext = strrchr(result->d_name, '.');
159
160                 if (!ext) {
161                         /* No file extension; not us */
162                         continue;
163                 }
164                 *ext++ = '\0';
165
166                 if (strcmp(file, result->d_name) == 0) {
167                         if (!ast_get_format_for_file_ext(ext)) {
168                                 ast_log(LOG_WARNING,
169                                         "Recording %s: unrecognized format %s\n",
170                                         result->d_name,
171                                         ext);
172                                 /* Keep looking */
173                                 continue;
174                         }
175                         /* We have a winner! */
176                         break;
177                 }
178         }
179
180         if (!result) {
181                 return NULL;
182         }
183
184         ast_asprintf(&file_with_ext, "%s/%s.%s", dir_name, file, ext);
185         return file_with_ext;
186 }
187
188 /*!
189  * \brief Allocate a recording object.
190  */
191 static struct stasis_app_stored_recording *recording_alloc(void)
192 {
193         RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
194                 ao2_cleanup);
195         int res;
196
197         recording = ao2_alloc(sizeof(*recording), stored_recording_dtor);
198         if (!recording) {
199                 return NULL;
200         }
201
202         res = ast_string_field_init(recording, 255);
203         if (res != 0) {
204                 return NULL;
205         }
206
207         ao2_ref(recording, +1);
208         return recording;
209 }
210
211 static int recording_sort(const void *obj_left, const void *obj_right, int flags)
212 {
213         const struct stasis_app_stored_recording *object_left = obj_left;
214         const struct stasis_app_stored_recording *object_right = obj_right;
215         const char *right_key = obj_right;
216         int cmp;
217
218         switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
219         case OBJ_POINTER:
220                 right_key = object_right->name;
221                 /* Fall through */
222         case OBJ_KEY:
223                 cmp = strcmp(object_left->name, right_key);
224                 break;
225         case OBJ_PARTIAL_KEY:
226                 /*
227                  * We could also use a partial key struct containing a length
228                  * so strlen() does not get called for every comparison instead.
229                  */
230                 cmp = strncmp(object_left->name, right_key, strlen(right_key));
231                 break;
232         default:
233                 /* Sort can only work on something with a full or partial key. */
234                 ast_assert(0);
235                 cmp = 0;
236                 break;
237         }
238         return cmp;
239 }
240
241 static int scan(struct ao2_container *recordings,
242         const char *base_dir, const char *subdir, struct dirent *entry);
243
244 static int scan_file(struct ao2_container *recordings,
245         const char *base_dir, const char *subdir, const char *filename,
246         const char *path)
247 {
248         RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
249                 ao2_cleanup);
250         RAII_VAR(struct ast_str *, name, NULL, ast_free);
251         const char *ext;
252         char *dot;
253
254         ext = strrchr(filename, '.');
255
256         if (!ext) {
257                 ast_verb(4, "  Ignore file without extension: %s\n",
258                         filename);
259                 /* No file extension; not us */
260                 return 0;
261         }
262         ++ext;
263
264         if (!ast_get_format_for_file_ext(ext)) {
265                 ast_verb(4, "  Not a media file: %s\n", filename);
266                 /* Not a media file */
267                 return 0;
268         }
269
270         recording = recording_alloc();
271         if (!recording) {
272                 return -1;
273         }
274
275         ast_string_field_set(recording, file_with_ext, path);
276
277         /* Build file and format from full path */
278         ast_string_field_set(recording, file, path);
279         dot = strrchr(recording->file, '.');
280         *dot = '\0';
281         recording->format = dot + 1;
282
283         /* Removed the recording dir from the file for the name. */
284         ast_string_field_set(recording, name,
285                 recording->file + strlen(ast_config_AST_RECORDING_DIR) + 1);
286
287         /* Add it to the recordings container */
288         ao2_link(recordings, recording);
289
290         return 0;
291 }
292
293 static int scan_dir(struct ao2_container *recordings,
294         const char *base_dir, const char *subdir, const char *dirname,
295         const char *path)
296 {
297         RAII_VAR(DIR *, dir, NULL, safe_closedir);
298         RAII_VAR(struct ast_str *, rel_dirname, NULL, ast_free);
299         struct dirent entry;
300         struct dirent *result = NULL;
301
302         if (strcmp(dirname, ".") == 0 ||
303                 strcmp(dirname, "..") == 0) {
304                 ast_verb(4, "  Ignoring self/parent dir\n");
305                 return 0;
306         }
307
308         /* Build relative dirname */
309         rel_dirname = ast_str_create(80);
310         if (!rel_dirname) {
311                 return -1;
312         }
313         if (!ast_strlen_zero(subdir)) {
314                 ast_str_append(&rel_dirname, 0, "%s/", subdir);
315         }
316         if (!ast_strlen_zero(dirname)) {
317                 ast_str_append(&rel_dirname, 0, "%s", dirname);
318         }
319
320         /* Read the directory */
321         dir = opendir(path);
322         if (!dir) {
323                 ast_log(LOG_WARNING, "Error reading dir '%s'\n", path);
324                 return -1;
325         }
326         while (readdir_r(dir, &entry, &result) == 0 && result != NULL) {
327                 scan(recordings, base_dir, ast_str_buffer(rel_dirname), result);
328         }
329
330         return 0;
331 }
332
333 static int scan(struct ao2_container *recordings,
334         const char *base_dir, const char *subdir, struct dirent *entry)
335 {
336         RAII_VAR(struct ast_str *, path, NULL, ast_free);
337
338         path = ast_str_create(255);
339         if (!path) {
340                 return -1;
341         }
342
343         /* Build file path */
344         ast_str_append(&path, 0, "%s", base_dir);
345         if (!ast_strlen_zero(subdir)) {
346                 ast_str_append(&path, 0, "/%s", subdir);
347         }
348         if (entry) {
349                 ast_str_append(&path, 0, "/%s", entry->d_name);
350         }
351         ast_verb(4, "Scanning '%s'\n", ast_str_buffer(path));
352
353         /* Handle this file */
354         switch (entry->d_type) {
355         case DT_REG:
356                 scan_file(recordings, base_dir, subdir, entry->d_name,
357                         ast_str_buffer(path));
358                 break;
359         case DT_DIR:
360                 scan_dir(recordings, base_dir, subdir, entry->d_name,
361                         ast_str_buffer(path));
362                 break;
363         default:
364                 ast_log(LOG_WARNING, "Skipping %s: not a regular file\n",
365                         ast_str_buffer(path));
366                 break;
367         }
368
369         return 0;
370 }
371
372 struct ao2_container *stasis_app_stored_recording_find_all(void)
373 {
374         RAII_VAR(struct ao2_container *, recordings, NULL, ao2_cleanup);
375         int res;
376
377         recordings = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
378                 AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, recording_sort, NULL);
379         if (!recordings) {
380                 return NULL;
381         }
382
383         res = scan_dir(recordings, ast_config_AST_RECORDING_DIR, "", "",
384                 ast_config_AST_RECORDING_DIR);
385         if (res != 0) {
386                 return NULL;
387         }
388
389         ao2_ref(recordings, +1);
390         return recordings;
391 }
392
393 struct stasis_app_stored_recording *stasis_app_stored_recording_find_by_name(
394         const char *name)
395 {
396         RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
397                 ao2_cleanup);
398         RAII_VAR(char *, dir, NULL, ast_free);
399         RAII_VAR(char *, file, NULL, ast_free);
400         RAII_VAR(char *, file_with_ext, NULL, ast_free);
401         int res;
402         struct stat file_stat;
403
404         errno = 0;
405
406         if (!name) {
407                 errno = EINVAL;
408                 return NULL;
409         }
410
411         recording = recording_alloc();
412         if (!recording) {
413                 return NULL;
414         }
415
416         res = split_path(name, &dir, &file);
417         if (res != 0) {
418                 return NULL;
419         }
420         ast_string_field_build(recording, file, "%s/%s", dir, file);
421
422         if (!ast_begins_with(dir, ast_config_AST_RECORDING_DIR)) {
423                 /* Attempt to escape the recording directory */
424                 ast_log(LOG_WARNING, "Attempt to access invalid recording %s\n",
425                         name);
426                 errno = EACCES;
427                 return NULL;
428         }
429
430         /* The actual name of the recording is file with the config dir
431          * prefix removed.
432          */
433         ast_string_field_set(recording, name,
434                 recording->file + strlen(ast_config_AST_RECORDING_DIR) + 1);
435
436         file_with_ext = find_recording(dir, file);
437         if (!file_with_ext) {
438                 return NULL;
439         }
440         ast_string_field_set(recording, file_with_ext, file_with_ext);
441         recording->format = strrchr(recording->file_with_ext, '.');
442         if (!recording->format) {
443                 return NULL;
444         }
445         ++(recording->format);
446
447         res = stat(file_with_ext, &file_stat);
448         if (res != 0) {
449                 return NULL;
450         }
451
452         if (!S_ISREG(file_stat.st_mode)) {
453                 /* Let's not play if it's not a regular file */
454                 errno = EACCES;
455                 return NULL;
456         }
457
458         ao2_ref(recording, +1);
459         return recording;
460 }
461
462 int stasis_app_stored_recording_delete(
463         struct stasis_app_stored_recording *recording)
464 {
465         /* Path was validated when the recording object was created */
466         return unlink(recording->file_with_ext);
467 }
468
469 struct ast_json *stasis_app_stored_recording_to_json(
470         struct stasis_app_stored_recording *recording)
471 {
472         if (!recording) {
473                 return NULL;
474         }
475
476         return ast_json_pack("{ s: s, s: s }",
477                 "name", recording->name,
478                 "format", recording->format);
479 }