Demonstration release of the principles underpinning krsd.
[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  */
23
24 static char *make_disk_path(char *dom_user, char *path, gss_buffer_t authuser, char **storage_root);
25 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path,
26                                  struct stat *stat_buf);
27 static evhtp_res serve_file_head(evhtp_request_t *request_t, char *disk_path,
28                            struct stat *stat_buf,const char *mime_type);
29 static evhtp_res serve_file(evhtp_request_t *request, const char *disk_path,
30                       struct stat *stat_buf);
31 static evhtp_res handle_get_or_head(evhtp_request_t *request, gss_buffer_t authuser, int include_body);
32
33 evhtp_res storage_handle_head(evhtp_request_t *request, gss_buffer_t authuser) {
34   if(RS_EXPERIMENTAL) {
35     return handle_get_or_head(request, authuser, 0);
36   } else {
37     return EVHTP_RES_METHNALLOWED;
38   }
39 }
40
41 evhtp_res storage_handle_get(evhtp_request_t *request, gss_buffer_t authuser) {
42   log_debug("storage_handle_get()");
43   return handle_get_or_head(request, authuser, 1);
44 }
45
46 evhtp_res storage_handle_put(evhtp_request_t *request, gss_buffer_t authuser) {
47   log_debug("HANDLE PUT");
48
49   if(request->uri->path->file == NULL) {
50     // PUT to directories aren't allowed
51     return 400;
52   }
53
54   char *storage_root = NULL;
55   char *disk_path = make_disk_path(REQUEST_GET_USER(request),
56                                    REQUEST_GET_PATH(request),
57                                    authuser,
58                                    &storage_root);
59   if(disk_path == NULL) {
60     return EVHTP_RES_SERVERR;
61   }
62
63   // check if file exists (needed for preconditions and response code)
64   struct stat stat_buf;
65   memset(&stat_buf, 0, sizeof(struct stat));
66   int exists = stat(disk_path, &stat_buf) == 0;
67
68   // check preconditions
69   do {
70
71     // PUT and DELETE requests MAY have an 'If-Match' request header [HTTP], and
72     // MUST fail with a 412 response code if that doesn't match the document's
73     // current version.
74
75     evhtp_header_t *if_match = evhtp_headers_find_header(request->headers_in, "If-Match");
76     if(if_match && ((!exists) || strcmp(get_etag(disk_path), if_match->val) != 0)) {
77       return 412;
78     }
79
80     // A PUT request MAY have an 'If-None-Match:*' header [HTTP], in which
81     // case it MUST fail with a 412 response code if the document already
82     // exists.
83
84     evhtp_header_t *if_none_match = evhtp_headers_find_header(request->headers_in, "If-None-Match");
85     if(if_none_match && strcmp(if_none_match->val, "*") == 0 && exists) {
86       return 412;
87     }
88
89   } while(0);
90
91 #if 0
92   // look up uid and gid of current user, so we can chown() correctly.
93   uid_t uid;
94   gid_t gid;
95   do {
96     char *bufptr = NULL;
97     struct passwd *user_entry = user_get_entry(REQUEST_GET_USER(request), &bufptr);
98     if(user_entry > 0) {
99       uid = user_entry->pw_uid;
100       gid = user_entry->pw_gid;
101       free(user_entry);
102       free(bufptr);
103     } else {
104       return EVHTP_RES_SERVERR;
105     }
106   } while(0);
107 #endif
108
109   // create parent directories
110   do {
111
112     char *path_copy = strdup(REQUEST_GET_PATH(request));
113     if(path_copy == NULL) {
114       log_error("strdup() failed: %s", strerror(errno));
115       free(disk_path);
116       free(storage_root);
117       return EVHTP_RES_SERVERR;
118     }
119     char *dir_path = dirname(path_copy);
120     if(strcmp(dir_path, ".") == 0) { // PUT to file below root directory
121       continue;
122     }
123     char *saveptr = NULL;
124     char *dir_name;
125     int dirfd = open(storage_root, O_RDONLY), prevfd;
126     if(dirfd == -1) {
127       log_error("failed to open() storage path (\"%s\"): %s", storage_root, strerror(errno));
128       free(disk_path);
129       free(path_copy);
130       free(storage_root);
131       return EVHTP_RES_SERVERR;
132     }
133     struct stat dir_stat;
134     log_debug("strtok_r(\"%s\", ...), (dir_path: %p, saveptr: %p)", dir_path, dir_path, saveptr);
135     for(dir_name = strtok_r(dir_path, "/", &saveptr);
136         dir_name != NULL;
137         dir_name = strtok_r(NULL, "/", &saveptr)) {
138       if(fstatat(dirfd, dir_name, &dir_stat, 0) == 0) {
139         if(! S_ISDIR(dir_stat.st_mode)) {
140           // exists, but not a directory
141           log_error("Can't PUT to %s, found a non-directory parent.", request->uri->path->full);
142           close(dirfd);
143           free(disk_path);
144           free(path_copy);
145           free(storage_root);
146           return 400;
147         } else {
148           // directory exists
149         }
150       } else {
151         if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
152           log_error("mkdirat() failed: %s", strerror(errno));
153           close(dirfd);
154           free(disk_path);
155           free(path_copy);
156           free(storage_root);
157           return EVHTP_RES_SERVERR;
158         }
159
160 #if 0
161         if(fchownat(dirfd, dir_name, uid, gid, AT_SYMLINK_NOFOLLOW) != 0) {
162           log_warn("failed to chown() newly created directory: %s", strerror(errno));
163         }
164 #endif
165       }
166       prevfd = dirfd;
167       dirfd = openat(prevfd, dir_name, O_RDONLY);
168       close(prevfd);
169       if(dirfd == -1) {
170         log_error("failed to openat() next directory (\"%s\"): %s",
171                   dir_name, strerror(errno));
172         free(disk_path);
173         free(path_copy);
174         free(storage_root);
175         return EVHTP_RES_SERVERR;
176       }
177     }
178
179     free(path_copy);
180     free(storage_root);
181     close(dirfd);
182
183   } while(0);
184
185   // open (and possibly create) file
186   int fd = open(disk_path, O_NONBLOCK | O_CREAT | O_WRONLY | O_TRUNC,
187                 RS_FILE_CREATE_MODE);
188
189   if(fd == -1) {
190     log_error("open() failed to open file \"%s\": %s", disk_path, strerror(errno));
191     free(disk_path);
192     return EVHTP_RES_SERVERR;
193   }
194
195   if(! exists) {
196 #if 0
197     if(fchown(fd, uid, gid) != 0) {
198       log_warn("Failed to chown() newly created file: %s", strerror(errno));
199     }
200 #endif
201   }
202
203   // write buffered data
204   // TODO: open (and write) file earlier in the request, so it doesn't have to be buffered completely.
205   evbuffer_write(request->buffer_in, fd);
206
207   char *content_type = "application/octet-stream; charset=binary";
208   evhtp_kv_t *content_type_header = evhtp_headers_find_header(request->headers_in, "Content-Type");
209
210   if(content_type_header != NULL) {
211     content_type = content_type_header->val;
212   }
213   
214   // remember content type in extended attributes
215   if(content_type_to_xattr(disk_path, content_type) != 0) {
216     log_error("Setting xattr for content type failed. Ignoring.");
217   }
218
219   close(fd);
220
221   // stat created file again to generate new etag
222   memset(&stat_buf, 0, sizeof(struct stat));
223   if(stat(disk_path, &stat_buf) != 0) {
224     log_error("failed to stat() file after writing: %s", strerror(errno));
225     free(disk_path);
226     return EVHTP_RES_SERVERR;
227   }
228
229   char *etag_string = get_etag(disk_path);
230
231   ADD_RESP_HEADER_CP(request, "Content-Type", content_type);
232   ADD_RESP_HEADER_CP(request, "ETag", etag_string);
233
234   free(etag_string);
235   free(disk_path);
236
237   return exists ? EVHTP_RES_OK : EVHTP_RES_CREATED;
238 }
239
240 evhtp_res storage_handle_delete(evhtp_request_t *request, gss_buffer_t authuser) {
241
242   if(request->uri->path->file == NULL) {
243     // DELETE to directories aren't allowed
244     return 400;
245   }
246
247   char *storage_root = NULL;
248   char *disk_path = make_disk_path(REQUEST_GET_USER(request),
249                                    REQUEST_GET_PATH(request),
250                                    authuser,
251                                    &storage_root);
252   if(disk_path == NULL) {
253     return EVHTP_RES_SERVERR;
254   }
255
256   struct stat stat_buf;
257   if(stat(disk_path, &stat_buf) == 0) {
258
259     if(S_ISDIR(stat_buf.st_mode)) {
260       return 400;
261     }
262
263     char *etag_string = get_etag(disk_path);
264
265     evhtp_header_t *if_match = evhtp_headers_find_header(request->headers_in, "If-Match");
266     if(if_match && (strcmp(etag_string, if_match->val) != 0)) {
267       return 412;
268     }
269
270     ADD_RESP_HEADER_CP(request, "ETag", etag_string);
271
272     // file exists, delete it.
273     if(unlink(disk_path) == -1) {
274       log_error("unlink() failed: %s", strerror(errno));
275       return EVHTP_RES_SERVERR;
276     }
277     
278     /* 
279      * remove empty parents
280      */
281     char *path_copy = strdup(REQUEST_GET_PATH(request));
282     if(path_copy == NULL) {
283       log_error("strdup() failed to copy path: %s", strerror(errno));
284       free(disk_path);
285       return EVHTP_RES_SERVERR;
286     }
287     char *dir_path;
288     int rootdirfd = open(storage_root, O_RDONLY);
289     if(rootdirfd == -1) {
290       log_error("failed to open() storage root: %s", strerror(errno));
291       free(path_copy);
292       free(disk_path);
293       return EVHTP_RES_SERVERR;
294     }
295     int result;
296     // skip leading slash
297     char *relative_path = path_copy + 1;
298     for(dir_path = dirname(relative_path);
299         ! (dir_path[0] == '.' && dir_path[1] == 0); // reached root
300         dir_path = dirname(dir_path)) {
301       log_debug("unlinking %s (relative to %s)", dir_path, storage_root);
302       result = unlinkat(rootdirfd, dir_path, AT_REMOVEDIR);
303       if(result != 0) {
304         if(errno == ENOTEMPTY || errno == EEXIST) {
305           // non-empty directory reached
306           break;
307         } else {
308           // other error occured
309           log_error("(while trying to remove %s)\n", dir_path);
310           log_error("unlinkat() failed to remove parent directory: %s", strerror(errno));
311           free(path_copy);
312           free(disk_path);
313           return EVHTP_RES_SERVERR;
314         }
315       }
316     }
317     close(rootdirfd);
318     free(path_copy);
319   } else {
320     // file doesn't exist, return 404.
321     return 404;
322   }
323
324   free(storage_root);
325
326   return 200;
327 }
328
329 size_t json_buf_writer(char *buf, size_t count, void *arg) {
330   return evbuffer_add((struct evbuffer*)arg, buf, count);
331 }
332
333 // serve a directory response for the given request
334 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path, struct stat *stat_buf) {
335   size_t disk_path_len = strlen(disk_path);
336   struct evbuffer *buf = request->buffer_out;
337   DIR *dir = opendir(disk_path);
338   if(dir == NULL) {
339     log_error("opendir() failed: %s", strerror(errno));
340     return EVHTP_RES_SERVERR;
341   }
342   struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
343                                  pathconf(disk_path, _PC_NAME_MAX) + 1);
344   struct dirent *resultp = NULL;
345   if(entryp == NULL) {
346     log_error("malloc() failed while creating directory pointer: %s",
347               strerror(errno));
348     return EVHTP_RES_SERVERR;
349   }
350
351   struct json *json = new_json(json_buf_writer, buf);
352
353   struct stat file_stat_buf;
354   int entry_len;
355
356   json_start_object(json);
357
358   for(;;) {
359     readdir_r(dir, entryp, &resultp);
360     if(resultp == NULL) {
361       break;
362     }
363     if(strcmp(entryp->d_name, ".") == 0 ||
364        strcmp(entryp->d_name, "..") == 0) {
365       // skip.
366       continue;
367     }
368     entry_len = strlen(entryp->d_name);
369     char full_path[disk_path_len + entry_len + 1];
370     sprintf(full_path, "%s%s", disk_path, entryp->d_name);
371     stat(full_path, &file_stat_buf);
372
373     char key_string[entry_len + 2];
374     sprintf(key_string, "%s%s", entryp->d_name,
375             S_ISDIR(file_stat_buf.st_mode) ? "/": "");
376     char *val_string = get_etag(full_path);
377
378     json_write_key_val(json, key_string, val_string);
379
380     free(val_string);
381   }
382
383   json_end_object(json);
384
385   free_json(json);
386
387   char *etag = get_etag(disk_path);
388   if(etag == NULL) {
389     log_error("get_etag() failed");
390     free(entryp);
391     closedir(dir);
392     return EVHTP_RES_SERVERR;
393   }
394
395   ADD_RESP_HEADER(request, "Content-Type", "application/json; charset=UTF-8");
396   ADD_RESP_HEADER_CP(request, "ETag", etag);
397
398   free(etag);
399   free(entryp);
400   closedir(dir);
401   return EVHTP_RES_OK;
402 }
403
404 static evhtp_res serve_file_head(evhtp_request_t *request, char *disk_path, struct stat *stat_buf, const char *mime_type) {
405
406   log_debug("serve file head");
407
408   if(request->uri->path->file == NULL) {
409     log_debug("HEAD dir requested");
410     // directory was requested
411     if(! S_ISDIR(stat_buf->st_mode)) {
412       log_debug("HEAD file found");
413       // but is actually a file
414       return EVHTP_RES_NOTFOUND;
415     }
416     log_debug("HEAD directory found");
417   } else {
418     log_debug("HEAD file requested");
419     // file was requested
420     if(S_ISDIR(stat_buf->st_mode)) {
421       log_debug("HEAD directory found");
422       // but is actually a directory
423       return EVHTP_RES_NOTFOUND;
424     }
425     log_debug("HEAD file found");
426   }
427     
428   char *etag_string = get_etag(disk_path);
429   if(etag_string == NULL) {
430     log_error("get_etag() failed");
431     return EVHTP_RES_SERVERR;
432   }
433
434   evhtp_header_t *if_none_match_header = evhtp_headers_find_header(request->headers_in, "If-None-Match");
435   if(if_none_match_header) {
436     // FIXME: support multiple comma-separated ETags in If-None-Match header
437     if(strcmp(if_none_match_header->val, etag_string) == 0) {
438       free(etag_string);
439       return EVHTP_RES_NOTMOD;
440     }
441   }
442
443   char *length_string = malloc(24);
444   if(length_string == NULL) {
445     log_error("malloc() failed: %s", strerror(errno));
446     free(etag_string);
447     return EVHTP_RES_SERVERR;
448   }
449   snprintf(length_string, 24, "%ld", stat_buf->st_size);
450
451   int free_mime_type = 0;
452   // mime type is either passed in ... (such as for directory listings)
453   if(mime_type == NULL) {
454     // ... or detected based on xattr
455     mime_type = content_type_from_xattr(disk_path);
456     if(mime_type == NULL) {
457       // ... or guessed by libmagic
458       log_debug("mime type not given, detecting...");
459       mime_type = magic_file(magic_cookie, disk_path);
460       if(mime_type == NULL) {
461         // ... or defaulted to "application/octet-stream"
462         log_error("magic failed: %s", magic_error(magic_cookie));
463         mime_type = "application/octet-stream; charset=binary";
464       }
465     } else {
466       // xattr detected mime type and allocated memory for it
467       free_mime_type = 1;
468     }
469   }
470
471   log_info("setting Content-Type of %s: %s", request->uri->path->full, mime_type);
472   ADD_RESP_HEADER_CP(request, "Content-Type", mime_type);
473   ADD_RESP_HEADER_CP(request, "Content-Length", length_string);
474   ADD_RESP_HEADER_CP(request, "ETag", etag_string);
475
476   free(etag_string);
477   free(length_string);
478   if(free_mime_type) {
479     free((char*)mime_type);
480   }
481   return 0;
482 }
483
484 // serve a file body for the given request
485 static evhtp_res serve_file(evhtp_request_t *request, const char *disk_path, struct stat *stat_buf) {
486   int fd = open(disk_path, O_RDONLY | O_NONBLOCK);
487   if(fd < 0) {
488     log_error("open() failed: %s", strerror(errno));
489     return EVHTP_RES_SERVERR;
490   }
491   while(evbuffer_read(request->buffer_out, fd, 4096) != 0);
492   close(fd);
493   return EVHTP_RES_OK;
494 }
495
496 static char *make_disk_path(char *dom_user, char *path, gss_buffer_t authuser, char **storage_root) {
497
498   // FIXME: use passwd->pwdir instead of /home/{user}/
499
500   // calculate maximum length of path
501   int pathlen = ( strlen(dom_user) + strlen(path) +
502                   6 + // "/home/"
503                   1 + // another slash
504                   RS_HOME_SERVE_ROOT_LEN );
505   char *disk_path = malloc(pathlen + 1);
506   char *xsfile = NULL;
507   FILE *xsf;
508   char principal [1026];
509   bool authorized;
510   if(disk_path == NULL) {
511     log_error("malloc() failed: %s", strerror(errno));
512     return NULL;
513   }
514   log_debug("Constructing disk_path for dom_user = \"%s\"", dom_user);
515   xsfile = malloc( 7 + RS_HOME_SERVE_ROOT_LEN + strlen(dom_user) + 1 + 17);
516   if(xsfile == NULL) {
517     log_error("malloc() failed: %s", strerror(errno));
518     free(disk_path);
519     return NULL;
520   }
521   sprintf(xsfile, "/home/%s/%s/.k5remotestorage", dom_user, RS_HOME_SERVE_ROOT);
522   log_debug("Access control list = \"%s\"", xsfile);
523   xsf = fopen (xsfile, "r");
524   authorized = false;
525   if (xsf) {
526     while ((!authorized) && fgets (principal, sizeof (principal)-1, xsf)) {
527       int len = strlen (principal);
528       if ((len > 1) && (principal [len-1] == '\n')) {
529         principal [--len] = '\0';
530       }
531       log_debug("Considering acceptable principal \"%s\"", principal);
532       authorized = (len == authuser->length) && (0 == memcmp (principal, authuser->value, len));
533     }
534     fclose (xsf);
535   } else {
536     log_error ("Failed to open access control list");
537     free(xsfile);
538     free(disk_path);
539     return NULL;
540   }
541   if (!authorized) {
542     log_error ("Access control list does not contain authorized user");
543     free(xsfile);
544     free(disk_path);
545     return NULL;
546   }
547   log_debug ("xsfile = \"%s\"", xsfile);
548   if(storage_root) {
549     // Cut off .k5remotestorage and reuse for *storage_root
550     xsfile [7 + RS_HOME_SERVE_ROOT_LEN + strlen (dom_user)] = '\0';
551     *storage_root = xsfile;
552     log_debug ("storage_root = \"%s\"", storage_root);
553   } else {
554     free (xsfile);
555     xsfile = NULL;
556   }
557   // remove all /.. segments
558   // (we don't try to resolve them, but instead treat them as garbage)
559   char *pos = NULL;
560   while((pos = strstr(path, "/..")) != NULL) { // FIXME: this would also filter out valid paths like /foo/..bar
561     int restlen = strlen(pos + 3);
562     memmove(pos, pos + 3, restlen);
563     pos[restlen] = 0;
564   }
565   // remove all duplicate slashes (nasty things may be done with them at times)
566   while((pos = strstr(path, "//")) != NULL) {
567     int restlen = strlen(pos + 2);
568     memmove(pos, pos + 2, restlen);
569     pos[restlen] = 0;
570   }
571   // build path
572   sprintf(disk_path, "/home/%s/%s%s", dom_user, RS_HOME_SERVE_ROOT, path);
573   log_debug ("disk_path = \"%s\"", disk_path);
574   return disk_path;
575 }
576
577 static evhtp_res handle_get_or_head(evhtp_request_t *request, gss_buffer_t authuser, int include_body) {
578
579   log_debug("HANDLE GET / HEAD (body: %s)", include_body ? "true" : "false");
580
581   char *disk_path = make_disk_path(REQUEST_GET_USER(request),
582                                    REQUEST_GET_PATH(request),
583                                    authuser,
584                                    NULL);
585   if(disk_path == NULL) {
586     return EVHTP_RES_SERVERR;
587   }
588
589   // stat
590   struct stat stat_buf;
591   if(stat(disk_path, &stat_buf) != 0) {
592     if(errno != ENOENT && errno != ENOTDIR) {
593       log_error("stat() failed for path \"%s\": %s", disk_path, strerror(errno));
594       return EVHTP_RES_SERVERR;
595     } else {
596       return EVHTP_RES_NOTFOUND;
597     }
598   }
599   // check for directory
600   if(request->uri->path->file == NULL) {
601     // directory requested
602     if(include_body) {
603       return serve_directory(request, disk_path, &stat_buf);
604     } else {
605       evhtp_res head_status = serve_file_head(request, disk_path, &stat_buf, "application/json");
606       return head_status != 0 ? head_status : EVHTP_RES_OK;
607     }
608   } else {
609     // file requested
610     evhtp_res head_result = serve_file_head(request, disk_path, &stat_buf, NULL);
611     if(head_result != 0) {
612       return head_result;
613     }
614     if(include_body) {
615       return serve_file(request, disk_path, &stat_buf);
616     } else {
617       return EVHTP_RES_OK;
618     }
619   }
620 }