GET / HEAD on files & dirs works as expected again!
[krsd] / src / handler / storage.c
1 /*
2  * rs-serve - (c) 2013 Niklas E. Cathor
3  *
4  * This program is distributed in the hope that it will be useful,
5  * but WITHOUT ANY WARRANTY; without even the implied warranty of
6  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
7  * GNU Affero General Public License for more details.
8  *
9  * You should have received a copy of the GNU Affero General Public License
10  * along with this program. If not, see <http://www.gnu.org/licenses/>.
11  */
12
13 #include "rs-serve.h"
14
15 /*
16  * Storage Handler
17  * ---------------
18  *
19  * Gets parsed requests and performs the requested actions / sends the requested
20  * response.
21  *
22  * These functions are only called from storage processes.
23  */
24
25 static char *escape_name(const char *name);
26 static char *make_etag(struct stat *stat_buf);
27 static char *make_disk_path(char *user, char *path);
28 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path,
29                                  struct stat *stat_buf);
30 static int serve_file_head(evhtp_request_t *request_t, char *disk_path,
31                            struct stat *stat_buf,const char *mime_type);
32 static int serve_file(evhtp_request_t *request, const char *disk_path,
33                       struct stat *stat_buf);
34 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body);
35 static int content_type_to_xattr(int fd, const char *content_type);
36
37
38 evhtp_res storage_handle_head(evhtp_request_t *request) {
39   return handle_get_or_head(request, 0);
40 }
41
42 evhtp_res storage_handle_get(evhtp_request_t *request) {
43   return handle_get_or_head(request, 1);
44 }
45
46 int storage_begin_put(struct rs_request *request) {
47   char *path_copy = strdup(request->path);
48   if(path_copy == NULL) {
49     log_error("strdup() failed: %s", strerror(errno));
50     return 500;
51   }
52   char *dir_path = dirname(path_copy);
53   char *saveptr = NULL;
54   char *dir_name;
55   int dirfd = open("/", O_RDONLY), prevfd;
56   if(dirfd == -1) {
57     log_error("failed to open() root directory: %s", strerror(errno));
58     free(path_copy);
59     return 500;
60   }
61   struct stat dir_stat;
62   for(dir_name = strtok_r(dir_path, "/", &saveptr);
63       dir_name != NULL;
64       dir_name = strtok_r(NULL, "/", &saveptr)) {
65     if(fstatat(dirfd, dir_name, &dir_stat, 0) == 0) {
66       if(! S_ISDIR(dir_stat.st_mode)) {
67         // exists, but not a directory
68         log_error("Can't PUT to %s, found a non-directory parent.", request->path);
69         close(dirfd);
70         free(path_copy);
71         return 400;
72       } else {
73         // directory exists
74       }
75     } else {
76       if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
77         log_error("mkdirat() failed: %s", strerror(errno));
78         close(dirfd);
79         free(path_copy);
80         return 500;
81       }
82     }
83     prevfd = dirfd;
84     dirfd = openat(prevfd, dir_name, O_RDONLY);
85     close(prevfd);
86     if(dirfd == -1) {
87       log_error("failed to openat() next directory (\"%s\"): %s",
88                 dir_name, strerror(errno));
89       free(path_copy);
90       return 500;
91     }
92   }
93
94   char *expectation = NULL;
95   struct rs_header *header;
96   for(header = request->headers;
97       header != NULL;
98       header = header->next) {
99     if(strcmp(header->key, "Expect") == 0) {
100       expectation = header->value;
101       break;
102     }
103   }
104
105   // dirname() possibly made previous copy unusable
106   strcpy(path_copy, request->path);
107   char *file_name = basename(path_copy);
108
109   // open (and possibly create) file
110   int fd = openat(dirfd, file_name,
111                   O_NONBLOCK | O_CREAT | O_WRONLY | O_TRUNC,
112                   RS_FILE_CREATE_MODE);
113   free(path_copy);
114   close(dirfd);
115
116   if(fd == -1) {
117     log_error("openat() failed to open file \"%s\": %s", file_name, strerror(errno));
118     return 500;
119   }
120
121   request->file_fd = fd;
122
123   if(expectation != NULL) {
124     if(strcasecmp(expectation, "100-continue") == 0) {
125       send_response_head(request, 100, NULL);
126     } else {
127       log_error("Expectation \"%s\" cannot be met.", expectation);
128       return 417;
129     }
130   }
131
132   return 0;
133 }
134
135 int storage_end_put(struct rs_request *request) {
136   struct stat stat_buf;
137   log_debug("fstatting now");
138   if(fstat(request->file_fd, &stat_buf) != 0) {
139     log_error("fstat() after PUT failed: %s", strerror(errno));
140     return 500;
141   }
142   log_debug("trying to find content-type header");
143   struct rs_header *header;
144   char *content_type = "application/octet-stream; charset=binary";
145   for(header = request->headers;
146       header != NULL;
147       header = header->next) {
148     if(strcmp(header->key, "Content-Type") == 0) {
149       log_debug("got content type header value: %s", header->value);
150       content_type = header->value;
151       break;
152     }
153   }
154   if(content_type_to_xattr(request->file_fd, content_type) != 0) {
155     log_error("Setting xattr for content type failed. Ignoring.");
156   }
157   close(request->file_fd);
158   int head_result = serve_file_head(request, request->path, &stat_buf, content_type);
159   if(head_result != 0) {
160     return head_result;
161   }
162   send_response_empty(request);
163   return 0;
164 }
165
166 int storage_handle_delete(struct rs_request *request) {
167   return 501;
168 }
169
170 static char *make_etag(struct stat *stat_buf) {
171   char *etag = malloc(21);
172   if(etag == NULL) {
173     log_error("malloc() failed: %s", strerror(errno));
174     return NULL;
175   }
176   snprintf(etag, 20, "%lld", ((long long int)stat_buf->st_mtime) * 1000);
177   return etag;
178 }
179
180 // escape backslashes (/) and double quotes (") to put the given string
181 // in quoted JSON strings.
182 static char *escape_name(const char *name) {
183   int max_len = strlen(name) * 2, i = 0;
184   char *escaped = malloc(max_len + 1);
185   if(escaped == NULL) {
186     perror("malloc() failed");
187     return NULL;
188   }
189   const char *name_p;
190   for(name_p = name; *name_p != 0; name_p++) {
191     if(*name_p == '"' || *name_p == '\\') {
192       escaped[i++] = '\\';
193     }
194     escaped[i++] = *name_p;
195   }
196   escaped[i++] = 0;
197   escaped = realloc(escaped, i);
198   return escaped;
199 }
200
201 // serve a directory response for the given request
202 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path, struct stat *stat_buf) {
203   size_t disk_path_len = strlen(disk_path);
204   struct evbuffer *buf = evbuffer_new();
205   if(buf == NULL) {
206     log_error("evbuffer_new() failed: %s", strerror(errno));
207     return 500;
208   }
209   DIR *dir = opendir(disk_path);
210   if(dir == NULL) {
211     log_error("opendir() failed: %s", strerror(errno));
212     return 500;
213   }
214   struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
215                                  pathconf(disk_path, _PC_NAME_MAX) + 1);
216   struct dirent *resultp = NULL;
217   if(entryp == NULL) {
218     log_error("malloc() failed while creating directory pointer: %s",
219               strerror(errno));
220     return 500;
221   }
222   struct stat file_stat_buf;
223   int entry_len;
224   int first = 1;
225   for(;;) {
226     readdir_r(dir, entryp, &resultp);
227     if(resultp == NULL) {
228       break;
229     }
230     if(strcmp(entryp->d_name, ".") == 0 ||
231        strcmp(entryp->d_name, "..") == 0) {
232       // skip.
233       continue;
234     }
235     if(first) {
236       evbuffer_add(buf, "{", 1);
237       first = 0;
238     } else {
239       evbuffer_add(buf, ",", 1);
240     }
241     entry_len = strlen(entryp->d_name);
242     char full_path[disk_path_len + entry_len + 1];
243     sprintf(full_path, "%s%s", disk_path, entryp->d_name);
244     stat(full_path, &file_stat_buf);
245
246     log_debug("Listing add entry %s", entryp->d_name);
247     
248     char *escaped_name = escape_name(entryp->d_name);
249     if(! escaped_name) {
250       // failed to allocate name
251       free(entryp);
252       free(dir);
253       return 500;
254     }
255     evbuffer_add_printf(buf, "\"%s%s\":%lld", escaped_name,
256                         S_ISDIR(file_stat_buf.st_mode) ? "/" : "",
257                         ((long long)file_stat_buf.st_mtime) * 1000);
258     free(escaped_name);
259   }
260   if(first) {
261     // empty directory.
262     evbuffer_add(buf, "{", 1);
263   }
264   evbuffer_add(buf, "}\n", 2);
265
266   char *etag = make_etag(stat_buf);
267   if(etag == NULL) {
268     log_error("make_etag() failed");
269     free(entryp);
270     closedir(dir);
271     return 500;
272   }
273
274   ADD_RESP_HEADER(request, "Content-Type", "application/json; charset=UTF-8");
275   ADD_RESP_HEADER_CP(request, "ETag", etag);
276
277   // FIXME: why do I need to use *_chunk_* here???
278   evhtp_send_reply_chunk_start(request, EVHTP_RES_OK);
279   evhtp_send_reply_chunk(request, buf);
280   evhtp_send_reply_chunk_end(request);
281   free(etag);
282   free(entryp);
283   closedir(dir);
284   return 0;
285 }
286
287 static char *get_xattr(const char *path, const char *key, int maxlen) {
288   int len = 32;
289   char *value = malloc(32);
290   for(value = malloc(len);len<=maxlen;value = realloc(value, len+=16)) {
291     if(value == NULL) {
292       log_error("malloc() / realloc() failed: %s", strerror(errno));
293       return NULL;
294     }
295     int actual_len = getxattr(path, key, value, len);
296     log_debug("getxattr() for key %s returned actual length of %d bytes", key, actual_len);
297     if(actual_len > 0) {
298       return value;
299     } else {
300       if(errno == ERANGE) {
301         // buffer too small.
302         continue;
303       } else if(errno == ENOATTR) {
304         // attribute not set
305         free(value);
306         return NULL;
307       } else if(errno == ENOTSUP) {
308         // xattr not supported
309         log_error("File system doesn't support extended attributes! You may want to use another one.");
310         free(value);
311         return NULL;
312       } else {
313         log_error("Unexpected error while getting %s attribute: %s", key, strerror(errno));
314         free(value);
315         return NULL;
316       }
317     }
318   }
319   log_error("%s attribute seems to be longer than %d bytes. That is simply unreasonable.", key, maxlen);
320   free(value);
321   return NULL;
322 }
323
324 static char *content_type_from_xattr(const char *path) {
325   char *mime_type = get_xattr(path, "user.mime_type", 128);
326   if(mime_type == NULL) {
327     return NULL;
328   }
329   char *charset = get_xattr(path, "user.charset", 64);
330   if(charset == NULL) {
331     return mime_type;
332   }
333   int mt_len = strlen(mime_type);
334   char *content_type = realloc(mime_type, mt_len + strlen(charset) + 10 + 1);
335   if(content_type == NULL) {
336     log_error("realloc() failed: %s", strerror(errno));
337     free(charset);
338     return mime_type;
339   }
340   log_debug("content_type now: %s (also charset is \"%s\")", content_type, charset);
341   sprintf(content_type + mt_len, "; charset=%s", charset);
342   free(charset);
343   return content_type;
344 }
345
346 static int content_type_to_xattr(int fd, const char *content_type) {
347   char *content_type_copy = strdup(content_type), *saveptr = NULL;
348   if(content_type_copy == NULL) {
349     log_error("strdup() failed: %s", strerror(errno));
350     return -1;
351   }
352   char *mime_type = strtok_r(content_type_copy, ";", &saveptr);
353   log_debug("extracted mime type: %s", mime_type);
354   char *rest = strtok_r(NULL, "", &saveptr);
355   char *charset_begin = NULL, *charset = NULL;
356   if(rest) {
357     charset_begin = strstr(rest, "charset=");
358     if(charset_begin) {
359       charset = charset_begin + 8;
360       log_debug("extracted charset: %s", charset);
361     }
362   }
363   if(charset == NULL) {
364     // FIXME: should this rather be binary or us-ascii?
365     charset = "UTF-8";
366     log_debug("guessed charset: %s", charset);
367   }
368   if(fsetxattr(fd, "user.mime_type", mime_type, strlen(mime_type) + 1, 0) != 0) {
369     log_error("fsetxattr() failed: %s", strerror(errno));
370     free(content_type_copy);
371     return -1;
372   }
373   log_debug("fsetxattr (mime_type) done");
374   if(fsetxattr(fd, "user.charset", mime_type, strlen(charset) + 1, 0) != 0) {
375     log_error("fsetxattr() failed: %s", strerror(errno));
376     free(content_type_copy);
377     return -1;
378   }
379   log_debug("fsetxattr (charset) done");
380   free(content_type_copy);
381   return 0;
382 }
383
384
385 static int serve_file_head(evhtp_request_t *request, char *disk_path, struct stat *stat_buf, const char *mime_type) {
386   log_debug("serve_file_head for path %s", disk_path);
387   int free_mime_type = 0;
388   // mime type is either passed in ... (such as for directory listings)
389   if(mime_type == NULL) {
390     // ... or detected based on xattr
391     mime_type = content_type_from_xattr(disk_path);
392     if(mime_type == NULL) {
393       // ... or guessed by libmagic
394       log_debug("mime type not given, detecting...");
395       mime_type = magic_file(magic_cookie, disk_path);
396       if(mime_type == NULL) {
397         // ... or defaulted to "application/octet-stream"
398         log_error("magic failed: %s", magic_error(magic_cookie));
399         mime_type = "application/octet-stream; charset=binary";
400       }
401     } else {
402       // xattr detected mime type and allocated memory for it
403       free_mime_type = 1;
404     }
405   }
406   struct rs_header content_type_header = {
407     .key = "Content-Type",
408     // header won't be freed (it's completely stack based / constant),
409     // so cast is valid here to satisfy type check.
410     // (struct rs_header.value cannot be constant, because in the case of a request
411     //  header it will be free()'d at some point)
412     .value = (char*)mime_type,
413     .next = &RS_DEFAULT_HEADERS
414   };
415   char *length_string = malloc(24);
416   if(length_string == NULL) {
417     log_error("malloc() failed: %s", strerror(errno));
418     return 500;
419   }
420   snprintf(length_string, 24, "%ld", stat_buf->st_size);
421   char *etag_string = make_etag(stat_buf);
422   if(etag_string == NULL) {
423     log_error("make_etag() failed");
424     free(length_string);
425     return 500;
426   }
427
428   ADD_RESP_HEADER_CP(request, "Content-Length", length_string);
429   ADD_RESP_HEADER_CP(request, "ETag", etag_string);
430
431   log_debug("HAVE SET CONTENT LENGTH: %s", length_string);
432
433   evhtp_send_reply_chunk_start(request, 200);
434
435   free(etag_string);
436   free(length_string);
437   if(free_mime_type) {
438     free((char*)mime_type);
439   }
440   return 0;
441 }
442
443 // serve a file body for the given request
444 static int serve_file(evhtp_request_t *request, const char *disk_path, struct stat *stat_buf) {
445   int fd = open(disk_path, O_RDONLY | O_NONBLOCK);
446   if(fd < 0) {
447     log_error("open() failed: %s", strerror(errno));
448     return 500;
449   }
450   struct evbuffer *buf = evbuffer_new();
451   while(evbuffer_read(buf, fd, 4096) != 0) {
452     evhtp_send_reply_chunk(request, buf);
453   }
454   evbuffer_free(buf);
455   evhtp_send_reply_chunk_end(request);
456   return 0;
457 }
458
459 static char *make_disk_path(char *user, char *path) {
460
461   // FIXME: use passwd->pwdir instead of /home/{user}/
462
463   // calculate maximum length of path
464   int pathlen = ( strlen(user) + strlen(path) +
465                   6 + // "/home/"
466                   1 + // another slash
467                   RS_HOME_SERVE_ROOT_LEN );
468   char *disk_path = malloc(pathlen);
469   if(disk_path == NULL) {
470     log_error("malloc() failed: %s", strerror(errno));
471     return NULL;
472   }
473   // remove all /.. segments
474   // (we don't try to resolve them, but instead treat them as garbage)
475   char *traverse_pos = NULL;
476   while((traverse_pos = strstr(path, "/..")) != NULL) {
477     int restlen = strlen(traverse_pos + 3);
478     memmove(traverse_pos, traverse_pos + 3, restlen);
479     traverse_pos[restlen] = 0;
480   }
481   // build path
482   sprintf(disk_path, "/home/%s/%s%s", user, RS_HOME_SERVE_ROOT, path);
483   return disk_path;
484 }
485                       
486
487 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body) {
488   char *disk_path = make_disk_path(request->uri->path->match_start,
489                                    request->uri->path->match_end);
490   if(disk_path == NULL) {
491     return 500;
492   }
493
494   log_debug("HANDLE GET OR HEAD, DISK PATH: %s", disk_path);
495
496   // stat
497   struct stat stat_buf;
498   if(stat(disk_path, &stat_buf) != 0) {
499     if(errno != ENOENT) {
500       log_error("stat() failed for path \"%s\": %s", disk_path, strerror(errno));
501       return 500;
502     } else {
503       return 404;
504     }
505   }
506   // check for directory
507   if(request->uri->path->file == NULL) {
508     // directory requested
509     if(! S_ISDIR(stat_buf.st_mode)) {
510       // not a directory.
511       return 404;
512     }
513     // directory found
514     if(include_body) {
515       return serve_directory(request, disk_path, &stat_buf);
516     } else {
517       return serve_file_head(request, disk_path, &stat_buf, "application/json");
518     }
519   } else {
520     // file requested
521     if(S_ISDIR(stat_buf.st_mode)) {
522       // found, but is a directory
523       return 404;
524     }
525     // file found
526     int head_result = serve_file_head(request, disk_path, &stat_buf, NULL);
527     if(head_result != 0) {
528       return head_result;
529     }
530     if(include_body) {
531       return serve_file(request, disk_path, &stat_buf);
532     } else {
533       // ?
534     }
535   }
536 }