Fully overwrite a same-named file when uploading
[asterisk/asterisk.git] / res / res_http_post.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2006, Digium, Inc.
5  *
6  * Mark Spencer <markster@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 /*!
20  * \file 
21  * \brief HTTP POST upload support for Asterisk HTTP server
22  *
23  * \author Terry Wilson <twilson@digium.com
24  *
25  * \ref AstHTTP - AMI over the http protocol
26  */
27
28 /*** MODULEINFO
29         <depend>gmime</depend>
30  ***/
31
32
33 #include "asterisk.h"
34
35 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
36
37 #include <sys/stat.h>
38 #include <fcntl.h>
39 #include <gmime/gmime.h>
40 #if defined (__OpenBSD__) || defined(__FreeBSD__)
41 #include <libgen.h>
42 #endif
43
44 #include "asterisk/linkedlists.h"
45 #include "asterisk/http.h"
46 #include "asterisk/paths.h"     /* use ast_config_AST_DATA_DIR */
47 #include "asterisk/tcptls.h"
48 #include "asterisk/manager.h"
49 #include "asterisk/cli.h"
50 #include "asterisk/module.h"
51 #include "asterisk/ast_version.h"
52
53 #define MAX_PREFIX 80
54
55 /* just a little structure to hold callback info for gmime */
56 struct mime_cbinfo {
57         int count;
58         const char *post_dir;
59 };
60
61 /* all valid URIs must be prepended by the string in prefix. */
62 static char prefix[MAX_PREFIX];
63
64 static void post_raw(GMimePart *part, const char *post_dir, const char *fn)
65 {
66         char filename[PATH_MAX];
67         GMimeDataWrapper *content;
68         GMimeStream *stream;
69         int fd;
70
71         snprintf(filename, sizeof(filename), "%s/%s", post_dir, fn);
72
73         ast_debug(1, "Posting raw data to %s\n", filename);
74
75         if ((fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666)) == -1) {
76                 ast_log(LOG_WARNING, "Unable to open %s for writing file from a POST!\n", filename);
77
78                 return;
79         }
80
81         stream = g_mime_stream_fs_new(fd);
82
83         content = g_mime_part_get_content_object(part);
84         g_mime_data_wrapper_write_to_stream(content, stream);
85         g_mime_stream_flush(stream);
86
87         g_object_unref(content);
88         g_object_unref(stream);
89 }
90
91 static GMimeMessage *parse_message(FILE *f)
92 {
93         GMimeMessage *message;
94         GMimeParser *parser;
95         GMimeStream *stream;
96
97         stream = g_mime_stream_file_new(f);
98
99         parser = g_mime_parser_new_with_stream(stream);
100         g_mime_parser_set_respect_content_length(parser, 1);
101         
102         g_object_unref(stream);
103
104         message = g_mime_parser_construct_message(parser);
105
106         g_object_unref(parser);
107
108         return message;
109 }
110
111 static void process_message_callback(GMimeObject *part, gpointer user_data)
112 {
113         struct mime_cbinfo *cbinfo = user_data;
114
115         cbinfo->count++;
116
117         /* We strip off the headers before we get here, so should only see GMIME_IS_PART */
118         if (GMIME_IS_MESSAGE_PART(part)) {
119                 ast_log(LOG_WARNING, "Got unexpected GMIME_IS_MESSAGE_PART\n");
120                 return;
121         } else if (GMIME_IS_MESSAGE_PARTIAL(part)) {
122                 ast_log(LOG_WARNING, "Got unexpected GMIME_IS_MESSAGE_PARTIAL\n");
123                 return;
124         } else if (GMIME_IS_MULTIPART(part)) {
125                 GList *l;
126                 
127                 ast_log(LOG_WARNING, "Got unexpected GMIME_IS_MULTIPART, trying to process subparts\n");
128                 l = GMIME_MULTIPART(part)->subparts;
129                 while (l) {
130                         process_message_callback(l->data, cbinfo);
131                         l = l->next;
132                 }
133         } else if (GMIME_IS_PART(part)) {
134                 const char *filename;
135
136                 if (ast_strlen_zero(filename = g_mime_part_get_filename(GMIME_PART(part)))) {
137                         ast_debug(1, "Skipping part with no filename\n");
138                         return;
139                 }
140
141                 post_raw(GMIME_PART(part), cbinfo->post_dir, filename);
142         } else {
143                 ast_log(LOG_ERROR, "Encountered unknown MIME part. This should never happen!\n");
144         }
145 }
146
147 static int process_message(GMimeMessage *message, const char *post_dir)
148 {
149         struct mime_cbinfo cbinfo = {
150                 .count = 0,
151                 .post_dir = post_dir,
152         };
153
154         g_mime_message_foreach_part(message, process_message_callback, &cbinfo);
155
156         return cbinfo.count;
157 }
158
159
160 /* Find a sequence of bytes within a binary array. */
161 static int find_sequence(char * inbuf, int inlen, char * matchbuf, int matchlen)
162 {
163         int current;
164         int comp;
165         int found = 0;
166
167         for (current = 0; current < inlen-matchlen; current++, inbuf++) {
168                 if (*inbuf == *matchbuf) {
169                         found=1;
170                         for (comp = 1; comp < matchlen; comp++) {
171                                 if (inbuf[comp] != matchbuf[comp]) {
172                                         found = 0;
173                                         break;
174                                 }
175                         }
176                         if (found) {
177                                 break;
178                         }
179                 }
180         }
181         if (found) {
182                 return current;
183         } else {
184                 return -1;
185         }
186 }
187
188 /*
189 * The following is a work around to deal with how IE7 embeds the local file name
190 * within the Mime header using full WINDOWS file path with backslash directory delimiters.
191 * This section of code attempts to isolate the directory path and remove it
192 * from what is written into the output file.  In addition, it changes
193 * esc chars (i.e. backslashes) to forward slashes.
194 * This function has two modes.  The first to find a boundary marker.  The
195 * second is to find the filename immediately after the boundary.
196 */
197 static int readmimefile(FILE * fin, FILE * fout, char * boundary, int contentlen)
198 {
199         int find_filename = 0;
200         char buf[4096];
201         int marker;
202         int x;
203         int char_in_buf = 0;
204         int num_to_read;
205         int boundary_len;
206         char * path_end, * path_start, * filespec;
207
208         if (NULL == fin || NULL == fout || NULL == boundary || 0 >= contentlen) {
209                 return -1;
210         }
211
212         boundary_len = strlen(boundary);
213         while (0 < contentlen || 0 < char_in_buf) {
214                 /* determine how much I will read into the buffer */
215                 if (contentlen > sizeof(buf) - char_in_buf) {
216                         num_to_read = sizeof(buf)- char_in_buf;
217                 } else {
218                         num_to_read = contentlen;
219                 }
220
221                 if (0 < num_to_read) {
222                         if (fread(&(buf[char_in_buf]), 1, num_to_read, fin) < num_to_read) {
223                                 ast_log(LOG_WARNING, "fread() failed: %s\n", strerror(errno));
224                                 num_to_read = 0;
225                         }
226                         contentlen -= num_to_read;
227                         char_in_buf += num_to_read;
228                 }
229                 /* If I am looking for the filename spec */
230                 if (find_filename) {
231                         path_end = filespec = NULL;
232                         x = strlen("filename=\"");
233                         marker = find_sequence(buf, char_in_buf, "filename=\"", x );
234                         if (0 <= marker) {
235                                 marker += x;  /* Index beyond the filename marker */
236                                 path_start = &buf[marker];
237                                 for (path_end = path_start, x = 0; x < char_in_buf-marker; x++, path_end++) {
238                                         if ('\\' == *path_end) {        /* convert backslashses to forward slashes */
239                                                 *path_end = '/';
240                                         }
241                                         if ('\"' == *path_end) {        /* If at the end of the file name spec */
242                                                 *path_end = '\0';               /* temporarily null terminate the file spec for basename */
243                                                 filespec = basename(path_start);
244                                                 *path_end = '\"';
245                                                 break;
246                                         }
247                                 }
248                         }
249                         if (filespec) { /* If the file name path was found in the header */
250                                 if (fwrite(buf, 1, marker, fout) != marker) {
251                                         ast_log(LOG_WARNING, "fwrite() failed: %s\n", strerror(errno));
252                                 }
253                                 x = (int)(path_end+1 - filespec);
254                                 if (fwrite(filespec, 1, x, fout) != x) {
255                                         ast_log(LOG_WARNING, "fwrite() failed: %s\n", strerror(errno));
256                                 }
257                                 x = (int)(path_end+1 - buf);
258                                 memmove(buf, &(buf[x]), char_in_buf-x);
259                                 char_in_buf -= x;
260                         }
261                         find_filename = 0;
262                 } else { /* I am looking for the boundary marker */
263                         marker = find_sequence(buf, char_in_buf, boundary, boundary_len);
264                         if (0 > marker) {
265                                 if (char_in_buf < (boundary_len)) {
266                                         /*no possibility to find the boundary, write all you have */
267                                         if (fwrite(buf, 1, char_in_buf, fout) != char_in_buf) {
268                                                 ast_log(LOG_WARNING, "fwrite() failed: %s\n", strerror(errno));
269                                         }
270                                         char_in_buf = 0;
271                                 } else {
272                                         /* write all except for area where the boundary marker could be */
273                                         if (fwrite(buf, 1, char_in_buf -(boundary_len -1), fout) != char_in_buf - (boundary_len - 1)) {
274                                                 ast_log(LOG_WARNING, "fwrite() failed: %s\n", strerror(errno));
275                                         }
276                                         x = char_in_buf -(boundary_len -1);
277                                         memmove(buf, &(buf[x]), char_in_buf-x);
278                                         char_in_buf = (boundary_len -1);
279                                 }
280                         } else {
281                                 /* write up through the boundary, then look for filename in the rest */
282                                 if (fwrite(buf, 1, marker + boundary_len, fout) != marker + boundary_len) {
283                                         ast_log(LOG_WARNING, "fwrite() failed: %s\n", strerror(errno));
284                                 }
285                                 x = marker + boundary_len;
286                                 memmove(buf, &(buf[x]), char_in_buf-x);
287                                 char_in_buf -= marker + boundary_len;
288                                 find_filename =1;
289                         }
290                 }
291         }
292         return 0;
293 }
294
295
296 static struct ast_str *http_post_callback(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *vars, struct ast_variable *headers, int *status, char **title, int *contentlength)
297 {
298         struct ast_variable *var;
299         unsigned long ident = 0;
300         FILE *f;
301         int content_len = 0;
302         struct ast_str *post_dir;
303         GMimeMessage *message;
304         int message_count = 0;
305         char * boundary_marker = NULL;
306
307         if (!urih) {
308                 return ast_http_error((*status = 400),
309                            (*title = ast_strdup("Missing URI handle")),
310                            NULL, "There was an error parsing the request");
311         }
312
313         for (var = vars; var; var = var->next) {
314                 if (strcasecmp(var->name, "mansession_id")) {
315                         continue;
316                 }
317
318                 if (sscanf(var->value, "%lx", &ident) != 1) {
319                         return ast_http_error((*status = 400),
320                                               (*title = ast_strdup("Bad Request")),
321                                               NULL, "The was an error parsing the request.");
322                 }
323
324                 if (!astman_verify_session_writepermissions(ident, EVENT_FLAG_CONFIG)) {
325                         return ast_http_error((*status = 401),
326                                               (*title = ast_strdup("Unauthorized")),
327                                               NULL, "You are not authorized to make this request.");
328                 }
329
330                 break;
331         }
332
333         if (!var) {
334                 return ast_http_error((*status = 401),
335                                       (*title = ast_strdup("Unauthorized")),
336                                       NULL, "You are not authorized to make this request.");
337         }
338
339         if (!(f = tmpfile())) {
340                 ast_log(LOG_ERROR, "Could not create temp file.\n");
341                 return NULL;
342         }
343
344         for (var = headers; var; var = var->next) {
345                 fprintf(f, "%s: %s\r\n", var->name, var->value);
346
347                 if (!strcasecmp(var->name, "Content-Length")) {
348                         if ((sscanf(var->value, "%u", &content_len)) != 1) {
349                                 ast_log(LOG_ERROR, "Invalid Content-Length in POST request!\n");
350                                 fclose(f);
351
352                                 return NULL;
353                         }
354                         ast_debug(1, "Got a Content-Length of %d\n", content_len);
355                 } else if (!strcasecmp(var->name, "Content-Type")) {
356                         boundary_marker = strstr(var->value, "boundary=");
357                         if (boundary_marker) {
358                                 boundary_marker += strlen("boundary=");
359                         }
360                 }
361         }
362
363         fprintf(f, "\r\n");
364
365         if (0 > readmimefile(ser->f, f, boundary_marker, content_len)) {
366                 if (option_debug) {
367                         ast_log(LOG_DEBUG, "Cannot find boundary marker in POST request.\n");
368                 }
369                 fclose(f);
370                 
371                 return NULL;
372         }
373
374         if (fseek(f, SEEK_SET, 0)) {
375                 ast_log(LOG_ERROR, "Failed to seek temp file back to beginning.\n");
376                 fclose(f);
377
378                 return NULL;
379         }
380
381         post_dir = urih->data;
382
383         message = parse_message(f); /* Takes ownership and will close f */
384
385         if (!message) {
386                 ast_log(LOG_ERROR, "Error parsing MIME data\n");
387
388                 return ast_http_error((*status = 400),
389                                       (*title = ast_strdup("Bad Request")),
390                                       NULL, "The was an error parsing the request.");
391         }
392
393         if (!(message_count = process_message(message, ast_str_buffer(post_dir)))) {
394                 ast_log(LOG_ERROR, "Invalid MIME data, found no parts!\n");
395                 g_object_unref(message);
396                 return ast_http_error((*status = 400),
397                                       (*title = ast_strdup("Bad Request")),
398                                       NULL, "The was an error parsing the request.");
399         }
400
401         g_object_unref(message);
402
403         return ast_http_error((*status = 200),
404                               (*title = ast_strdup("OK")),
405                               NULL, "File successfully uploaded.");
406 }
407
408 static int __ast_http_post_load(int reload)
409 {
410         struct ast_config *cfg;
411         struct ast_variable *v;
412         struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
413
414         cfg = ast_config_load2("http.conf", "http", config_flags);
415         if (cfg == CONFIG_STATUS_FILEMISSING || cfg == CONFIG_STATUS_FILEUNCHANGED || cfg == CONFIG_STATUS_FILEINVALID) {
416                 return 0;
417         }
418
419         if (reload) {
420                 ast_http_uri_unlink_all_with_key(__FILE__);
421         }
422
423         if (cfg) {
424                 for (v = ast_variable_browse(cfg, "general"); v; v = v->next) {
425                         if (!strcasecmp(v->name, "prefix")) {
426                                 ast_copy_string(prefix, v->value, sizeof(prefix));
427                                 if (prefix[strlen(prefix)] == '/') {
428                                         prefix[strlen(prefix)] = '\0';
429                                 }
430                         }
431                 }
432
433                 for (v = ast_variable_browse(cfg, "post_mappings"); v; v = v->next) {
434                         struct ast_http_uri *urih;
435                         struct ast_str *ds;
436
437                         if (!(urih = ast_calloc(sizeof(*urih), 1))) {
438                                 ast_config_destroy(cfg);
439                                 return -1;
440                         }
441
442                         if (!(ds = ast_str_create(32))) {
443                                 ast_free(urih);
444                                 ast_config_destroy(cfg);
445                                 return -1;
446                         }
447
448                         urih->description = ast_strdup("HTTP POST mapping");
449                         urih->uri = ast_strdup(v->name);
450                         ast_str_set(&ds, 0, "%s/%s", prefix, v->value);
451                         urih->data = ds;
452                         urih->has_subtree = 0;
453                         urih->supports_get = 0;
454                         urih->supports_post = 1;
455                         urih->callback = http_post_callback;
456                         urih->key = __FILE__;
457                         urih->mallocd = urih->dmallocd = 1;
458
459                         ast_http_uri_link(urih);
460                 }
461
462                 ast_config_destroy(cfg);
463         }
464         return 0;
465 }
466
467 static int unload_module(void)
468 {
469         ast_http_uri_unlink_all_with_key(__FILE__);
470
471         return 0;
472 }
473
474 static int reload(void)
475 {
476         __ast_http_post_load(1);
477
478         return AST_MODULE_LOAD_SUCCESS;
479 }
480
481 static int load_module(void)
482 {
483         g_mime_init(0);
484
485         __ast_http_post_load(0);
486
487         return AST_MODULE_LOAD_SUCCESS;
488 }
489
490 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "HTTP POST support",
491         .load = load_module,
492         .unload = unload_module,
493         .reload = reload,
494 );