2 * rs-serve - (c) 2013 Niklas E. Cathor
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.
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/>.
19 * Gets parsed requests and performs the requested actions / sends the requested
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);
33 evhtp_res storage_handle_head(evhtp_request_t *request, gss_buffer_t authuser) {
35 return handle_get_or_head(request, authuser, 0);
37 return EVHTP_RES_METHNALLOWED;
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);
46 evhtp_res storage_handle_put(evhtp_request_t *request, gss_buffer_t authuser) {
47 log_debug("HANDLE PUT");
49 if(request->uri->path->file == NULL) {
50 // PUT to directories aren't allowed
54 char *storage_root = NULL;
55 char *disk_path = make_disk_path(REQUEST_GET_USER(request),
56 REQUEST_GET_PATH(request),
59 if(disk_path == NULL) {
60 return EVHTP_RES_SERVERR;
63 // check if file exists (needed for preconditions and response code)
65 memset(&stat_buf, 0, sizeof(struct stat));
66 int exists = stat(disk_path, &stat_buf) == 0;
68 // check preconditions
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
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)) {
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
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) {
92 // look up uid and gid of current user, so we can chown() correctly.
97 struct passwd *user_entry = user_get_entry(REQUEST_GET_USER(request), &bufptr);
99 uid = user_entry->pw_uid;
100 gid = user_entry->pw_gid;
104 return EVHTP_RES_SERVERR;
109 // create parent directories
112 char *path_copy = strdup(REQUEST_GET_PATH(request));
113 if(path_copy == NULL) {
114 log_error("strdup() failed: %s", strerror(errno));
117 return EVHTP_RES_SERVERR;
119 char *dir_path = dirname(path_copy);
120 if(strcmp(dir_path, ".") == 0) { // PUT to file below root directory
123 char *saveptr = NULL;
125 int dirfd = open(storage_root, O_RDONLY), prevfd;
127 log_error("failed to open() storage path (\"%s\"): %s", storage_root, strerror(errno));
131 return EVHTP_RES_SERVERR;
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);
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);
151 if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
152 log_error("mkdirat() failed: %s", strerror(errno));
157 return EVHTP_RES_SERVERR;
161 if(fchownat(dirfd, dir_name, uid, gid, AT_SYMLINK_NOFOLLOW) != 0) {
162 log_warn("failed to chown() newly created directory: %s", strerror(errno));
167 dirfd = openat(prevfd, dir_name, O_RDONLY);
170 log_error("failed to openat() next directory (\"%s\"): %s",
171 dir_name, strerror(errno));
175 return EVHTP_RES_SERVERR;
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);
190 log_error("open() failed to open file \"%s\": %s", disk_path, strerror(errno));
192 return EVHTP_RES_SERVERR;
197 if(fchown(fd, uid, gid) != 0) {
198 log_warn("Failed to chown() newly created file: %s", strerror(errno));
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);
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");
210 if(content_type_header != NULL) {
211 content_type = content_type_header->val;
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.");
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));
226 return EVHTP_RES_SERVERR;
229 char *etag_string = get_etag(disk_path);
231 ADD_RESP_HEADER_CP(request, "Content-Type", content_type);
232 ADD_RESP_HEADER_CP(request, "ETag", etag_string);
237 return exists ? EVHTP_RES_OK : EVHTP_RES_CREATED;
240 evhtp_res storage_handle_delete(evhtp_request_t *request, gss_buffer_t authuser) {
242 if(request->uri->path->file == NULL) {
243 // DELETE to directories aren't allowed
247 char *storage_root = NULL;
248 char *disk_path = make_disk_path(REQUEST_GET_USER(request),
249 REQUEST_GET_PATH(request),
252 if(disk_path == NULL) {
253 return EVHTP_RES_SERVERR;
256 struct stat stat_buf;
257 if(stat(disk_path, &stat_buf) == 0) {
259 if(S_ISDIR(stat_buf.st_mode)) {
263 char *etag_string = get_etag(disk_path);
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)) {
270 ADD_RESP_HEADER_CP(request, "ETag", etag_string);
272 // file exists, delete it.
273 if(unlink(disk_path) == -1) {
274 log_error("unlink() failed: %s", strerror(errno));
275 return EVHTP_RES_SERVERR;
279 * remove empty parents
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));
285 return EVHTP_RES_SERVERR;
288 int rootdirfd = open(storage_root, O_RDONLY);
289 if(rootdirfd == -1) {
290 log_error("failed to open() storage root: %s", strerror(errno));
293 return EVHTP_RES_SERVERR;
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);
304 if(errno == ENOTEMPTY || errno == EEXIST) {
305 // non-empty directory reached
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));
313 return EVHTP_RES_SERVERR;
320 // file doesn't exist, return 404.
329 size_t json_buf_writer(char *buf, size_t count, void *arg) {
330 return evbuffer_add((struct evbuffer*)arg, buf, count);
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);
339 log_error("opendir() failed: %s", strerror(errno));
340 return EVHTP_RES_SERVERR;
342 struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
343 pathconf(disk_path, _PC_NAME_MAX) + 1);
344 struct dirent *resultp = NULL;
346 log_error("malloc() failed while creating directory pointer: %s",
348 return EVHTP_RES_SERVERR;
351 struct json *json = new_json(json_buf_writer, buf);
353 struct stat file_stat_buf;
356 json_start_object(json);
359 readdir_r(dir, entryp, &resultp);
360 if(resultp == NULL) {
363 if(strcmp(entryp->d_name, ".") == 0 ||
364 strcmp(entryp->d_name, "..") == 0) {
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);
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);
378 json_write_key_val(json, key_string, val_string);
383 json_end_object(json);
387 char *etag = get_etag(disk_path);
389 log_error("get_etag() failed");
392 return EVHTP_RES_SERVERR;
395 ADD_RESP_HEADER(request, "Content-Type", "application/json; charset=UTF-8");
396 ADD_RESP_HEADER_CP(request, "ETag", etag);
404 static evhtp_res serve_file_head(evhtp_request_t *request, char *disk_path, struct stat *stat_buf, const char *mime_type) {
406 log_debug("serve file head");
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;
416 log_debug("HEAD directory found");
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;
425 log_debug("HEAD file found");
428 char *etag_string = get_etag(disk_path);
429 if(etag_string == NULL) {
430 log_error("get_etag() failed");
431 return EVHTP_RES_SERVERR;
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) {
439 return EVHTP_RES_NOTMOD;
443 char *length_string = malloc(24);
444 if(length_string == NULL) {
445 log_error("malloc() failed: %s", strerror(errno));
447 return EVHTP_RES_SERVERR;
449 snprintf(length_string, 24, "%ld", stat_buf->st_size);
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";
466 // xattr detected mime type and allocated memory for it
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);
479 free((char*)mime_type);
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);
488 log_error("open() failed: %s", strerror(errno));
489 return EVHTP_RES_SERVERR;
491 while(evbuffer_read(request->buffer_out, fd, 4096) != 0);
496 static char *make_disk_path(char *dom_user, char *path, gss_buffer_t authuser, char **storage_root) {
498 // FIXME: use passwd->pwdir instead of /home/{user}/
500 // calculate maximum length of path
501 int pathlen = ( strlen(dom_user) + strlen(path) +
504 RS_HOME_SERVE_ROOT_LEN );
505 char *disk_path = malloc(pathlen + 1);
508 char principal [1026];
510 if(disk_path == NULL) {
511 log_error("malloc() failed: %s", strerror(errno));
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);
517 log_error("malloc() failed: %s", strerror(errno));
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");
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';
531 log_debug("Considering acceptable principal \"%s\"", principal);
532 authorized = (len == authuser->length) && (0 == memcmp (principal, authuser->value, len));
536 log_error ("Failed to open access control list");
542 log_error ("Access control list does not contain authorized user");
547 log_debug ("xsfile = \"%s\"", xsfile);
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);
557 // remove all /.. segments
558 // (we don't try to resolve them, but instead treat them as garbage)
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);
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);
572 sprintf(disk_path, "/home/%s/%s%s", dom_user, RS_HOME_SERVE_ROOT, path);
573 log_debug ("disk_path = \"%s\"", disk_path);
577 static evhtp_res handle_get_or_head(evhtp_request_t *request, gss_buffer_t authuser, int include_body) {
579 log_debug("HANDLE GET / HEAD (body: %s)", include_body ? "true" : "false");
581 char *disk_path = make_disk_path(REQUEST_GET_USER(request),
582 REQUEST_GET_PATH(request),
585 if(disk_path == NULL) {
586 return EVHTP_RES_SERVERR;
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;
596 return EVHTP_RES_NOTFOUND;
599 // check for directory
600 if(request->uri->path->file == NULL) {
601 // directory requested
603 return serve_directory(request, disk_path, &stat_buf);
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;
610 evhtp_res head_result = serve_file_head(request, disk_path, &stat_buf, NULL);
611 if(head_result != 0) {
615 return serve_file(request, disk_path, &stat_buf);