Demonstration release of the principles underpinning krsd.
[krsd] / src / handler / storage.c
index 008922e..d3579bd 100644 (file)
  * Gets parsed requests and performs the requested actions / sends the requested
  * response.
  *
- * These functions are only called from storage processes.
  */
 
-static char *escape_name(const char *name);
-static char *make_etag(struct stat *stat_buf);
-static int serve_directory(struct rs_request *request, struct stat *stat_buf);
-static int serve_file_head(struct rs_request *request, struct stat *stat_buf,
-                           const char *mime_type);
-static int serve_file(struct rs_request *request, struct stat *stat_buf);
-static int handle_get_or_head(struct rs_request *request, int include_body);
+static char *make_disk_path(char *dom_user, char *path, gss_buffer_t authuser, char **storage_root);
+static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path,
+                                 struct stat *stat_buf);
+static evhtp_res serve_file_head(evhtp_request_t *request_t, char *disk_path,
+                           struct stat *stat_buf,const char *mime_type);
+static evhtp_res serve_file(evhtp_request_t *request, const char *disk_path,
+                      struct stat *stat_buf);
+static evhtp_res handle_get_or_head(evhtp_request_t *request, gss_buffer_t authuser, int include_body);
 
-
-int storage_handle_head(struct rs_request *request) {
-  return handle_get_or_head(request, 0);
+evhtp_res storage_handle_head(evhtp_request_t *request, gss_buffer_t authuser) {
+  if(RS_EXPERIMENTAL) {
+    return handle_get_or_head(request, authuser, 0);
+  } else {
+    return EVHTP_RES_METHNALLOWED;
+  }
 }
 
-int storage_handle_get(struct rs_request *request) {
-  return handle_get_or_head(request, 1);
+evhtp_res storage_handle_get(evhtp_request_t *request, gss_buffer_t authuser) {
+  log_debug("storage_handle_get()");
+  return handle_get_or_head(request, authuser, 1);
 }
 
-int storage_handle_put(struct rs_request *request) {
-  return 501;
-}
+evhtp_res storage_handle_put(evhtp_request_t *request, gss_buffer_t authuser) {
+  log_debug("HANDLE PUT");
 
-int storage_handle_delete(struct rs_request *request) {
-  return 501;
-}
+  if(request->uri->path->file == NULL) {
+    // PUT to directories aren't allowed
+    return 400;
+  }
 
+  char *storage_root = NULL;
+  char *disk_path = make_disk_path(REQUEST_GET_USER(request),
+                                   REQUEST_GET_PATH(request),
+                                  authuser,
+                                   &storage_root);
+  if(disk_path == NULL) {
+    return EVHTP_RES_SERVERR;
+  }
 
+  // check if file exists (needed for preconditions and response code)
+  struct stat stat_buf;
+  memset(&stat_buf, 0, sizeof(struct stat));
+  int exists = stat(disk_path, &stat_buf) == 0;
 
-static char *make_etag(struct stat *stat_buf) {
-  char *etag = malloc(21);
-  if(etag == NULL) {
-    log_error("malloc() failed: %s", strerror(errno));
-    return NULL;
+  // check preconditions
+  do {
+
+    // PUT and DELETE requests MAY have an 'If-Match' request header [HTTP], and
+    // MUST fail with a 412 response code if that doesn't match the document's
+    // current version.
+
+    evhtp_header_t *if_match = evhtp_headers_find_header(request->headers_in, "If-Match");
+    if(if_match && ((!exists) || strcmp(get_etag(disk_path), if_match->val) != 0)) {
+      return 412;
+    }
+
+    // A PUT request MAY have an 'If-None-Match:*' header [HTTP], in which
+    // case it MUST fail with a 412 response code if the document already
+    // exists.
+
+    evhtp_header_t *if_none_match = evhtp_headers_find_header(request->headers_in, "If-None-Match");
+    if(if_none_match && strcmp(if_none_match->val, "*") == 0 && exists) {
+      return 412;
+    }
+
+  } while(0);
+
+#if 0
+  // look up uid and gid of current user, so we can chown() correctly.
+  uid_t uid;
+  gid_t gid;
+  do {
+    char *bufptr = NULL;
+    struct passwd *user_entry = user_get_entry(REQUEST_GET_USER(request), &bufptr);
+    if(user_entry > 0) {
+      uid = user_entry->pw_uid;
+      gid = user_entry->pw_gid;
+      free(user_entry);
+      free(bufptr);
+    } else {
+      return EVHTP_RES_SERVERR;
+    }
+  } while(0);
+#endif
+
+  // create parent directories
+  do {
+
+    char *path_copy = strdup(REQUEST_GET_PATH(request));
+    if(path_copy == NULL) {
+      log_error("strdup() failed: %s", strerror(errno));
+      free(disk_path);
+      free(storage_root);
+      return EVHTP_RES_SERVERR;
+    }
+    char *dir_path = dirname(path_copy);
+    if(strcmp(dir_path, ".") == 0) { // PUT to file below root directory
+      continue;
+    }
+    char *saveptr = NULL;
+    char *dir_name;
+    int dirfd = open(storage_root, O_RDONLY), prevfd;
+    if(dirfd == -1) {
+      log_error("failed to open() storage path (\"%s\"): %s", storage_root, strerror(errno));
+      free(disk_path);
+      free(path_copy);
+      free(storage_root);
+      return EVHTP_RES_SERVERR;
+    }
+    struct stat dir_stat;
+    log_debug("strtok_r(\"%s\", ...), (dir_path: %p, saveptr: %p)", dir_path, dir_path, saveptr);
+    for(dir_name = strtok_r(dir_path, "/", &saveptr);
+        dir_name != NULL;
+        dir_name = strtok_r(NULL, "/", &saveptr)) {
+      if(fstatat(dirfd, dir_name, &dir_stat, 0) == 0) {
+        if(! S_ISDIR(dir_stat.st_mode)) {
+          // exists, but not a directory
+          log_error("Can't PUT to %s, found a non-directory parent.", request->uri->path->full);
+          close(dirfd);
+          free(disk_path);
+          free(path_copy);
+          free(storage_root);
+          return 400;
+        } else {
+          // directory exists
+        }
+      } else {
+        if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
+          log_error("mkdirat() failed: %s", strerror(errno));
+          close(dirfd);
+          free(disk_path);
+          free(path_copy);
+          free(storage_root);
+          return EVHTP_RES_SERVERR;
+        }
+
+#if 0
+        if(fchownat(dirfd, dir_name, uid, gid, AT_SYMLINK_NOFOLLOW) != 0) {
+          log_warn("failed to chown() newly created directory: %s", strerror(errno));
+        }
+#endif
+      }
+      prevfd = dirfd;
+      dirfd = openat(prevfd, dir_name, O_RDONLY);
+      close(prevfd);
+      if(dirfd == -1) {
+        log_error("failed to openat() next directory (\"%s\"): %s",
+                  dir_name, strerror(errno));
+        free(disk_path);
+        free(path_copy);
+        free(storage_root);
+        return EVHTP_RES_SERVERR;
+      }
+    }
+
+    free(path_copy);
+    free(storage_root);
+    close(dirfd);
+
+  } while(0);
+
+  // open (and possibly create) file
+  int fd = open(disk_path, O_NONBLOCK | O_CREAT | O_WRONLY | O_TRUNC,
+                RS_FILE_CREATE_MODE);
+
+  if(fd == -1) {
+    log_error("open() failed to open file \"%s\": %s", disk_path, strerror(errno));
+    free(disk_path);
+    return EVHTP_RES_SERVERR;
+  }
+
+  if(! exists) {
+#if 0
+    if(fchown(fd, uid, gid) != 0) {
+      log_warn("Failed to chown() newly created file: %s", strerror(errno));
+    }
+#endif
   }
-  snprintf(etag, 20, "%lld", ((long long int)stat_buf->st_mtime) * 1000);
-  return etag;
+
+  // write buffered data
+  // TODO: open (and write) file earlier in the request, so it doesn't have to be buffered completely.
+  evbuffer_write(request->buffer_in, fd);
+
+  char *content_type = "application/octet-stream; charset=binary";
+  evhtp_kv_t *content_type_header = evhtp_headers_find_header(request->headers_in, "Content-Type");
+
+  if(content_type_header != NULL) {
+    content_type = content_type_header->val;
+  }
+  
+  // remember content type in extended attributes
+  if(content_type_to_xattr(disk_path, content_type) != 0) {
+    log_error("Setting xattr for content type failed. Ignoring.");
+  }
+
+  close(fd);
+
+  // stat created file again to generate new etag
+  memset(&stat_buf, 0, sizeof(struct stat));
+  if(stat(disk_path, &stat_buf) != 0) {
+    log_error("failed to stat() file after writing: %s", strerror(errno));
+    free(disk_path);
+    return EVHTP_RES_SERVERR;
+  }
+
+  char *etag_string = get_etag(disk_path);
+
+  ADD_RESP_HEADER_CP(request, "Content-Type", content_type);
+  ADD_RESP_HEADER_CP(request, "ETag", etag_string);
+
+  free(etag_string);
+  free(disk_path);
+
+  return exists ? EVHTP_RES_OK : EVHTP_RES_CREATED;
 }
 
-// escape backslashes (/) and double quotes (") to put the given string
-// in quoted JSON strings.
-static char *escape_name(const char *name) {
-  int max_len = strlen(name) * 2, i = 0;
-  char *escaped = malloc(max_len + 1);
-  if(escaped == NULL) {
-    perror("malloc() failed");
-    return NULL;
+evhtp_res storage_handle_delete(evhtp_request_t *request, gss_buffer_t authuser) {
+
+  if(request->uri->path->file == NULL) {
+    // DELETE to directories aren't allowed
+    return 400;
   }
-  const char *name_p;
-  for(name_p = name; *name_p != 0; name_p++) {
-    if(*name_p == '"' || *name_p == '\\') {
-      escaped[i++] = '\\';
+
+  char *storage_root = NULL;
+  char *disk_path = make_disk_path(REQUEST_GET_USER(request),
+                                   REQUEST_GET_PATH(request),
+                                  authuser,
+                                   &storage_root);
+  if(disk_path == NULL) {
+    return EVHTP_RES_SERVERR;
+  }
+
+  struct stat stat_buf;
+  if(stat(disk_path, &stat_buf) == 0) {
+
+    if(S_ISDIR(stat_buf.st_mode)) {
+      return 400;
+    }
+
+    char *etag_string = get_etag(disk_path);
+
+    evhtp_header_t *if_match = evhtp_headers_find_header(request->headers_in, "If-Match");
+    if(if_match && (strcmp(etag_string, if_match->val) != 0)) {
+      return 412;
+    }
+
+    ADD_RESP_HEADER_CP(request, "ETag", etag_string);
+
+    // file exists, delete it.
+    if(unlink(disk_path) == -1) {
+      log_error("unlink() failed: %s", strerror(errno));
+      return EVHTP_RES_SERVERR;
+    }
+    
+    /* 
+     * remove empty parents
+     */
+    char *path_copy = strdup(REQUEST_GET_PATH(request));
+    if(path_copy == NULL) {
+      log_error("strdup() failed to copy path: %s", strerror(errno));
+      free(disk_path);
+      return EVHTP_RES_SERVERR;
     }
-    escaped[i++] = *name_p;
+    char *dir_path;
+    int rootdirfd = open(storage_root, O_RDONLY);
+    if(rootdirfd == -1) {
+      log_error("failed to open() storage root: %s", strerror(errno));
+      free(path_copy);
+      free(disk_path);
+      return EVHTP_RES_SERVERR;
+    }
+    int result;
+    // skip leading slash
+    char *relative_path = path_copy + 1;
+    for(dir_path = dirname(relative_path);
+        ! (dir_path[0] == '.' && dir_path[1] == 0); // reached root
+        dir_path = dirname(dir_path)) {
+      log_debug("unlinking %s (relative to %s)", dir_path, storage_root);
+      result = unlinkat(rootdirfd, dir_path, AT_REMOVEDIR);
+      if(result != 0) {
+        if(errno == ENOTEMPTY || errno == EEXIST) {
+          // non-empty directory reached
+          break;
+        } else {
+          // other error occured
+          log_error("(while trying to remove %s)\n", dir_path);
+          log_error("unlinkat() failed to remove parent directory: %s", strerror(errno));
+          free(path_copy);
+          free(disk_path);
+          return EVHTP_RES_SERVERR;
+        }
+      }
+    }
+    close(rootdirfd);
+    free(path_copy);
+  } else {
+    // file doesn't exist, return 404.
+    return 404;
   }
-  escaped[i++] = 0;
-  escaped = realloc(escaped, i);
-  return escaped;
+
+  free(storage_root);
+
+  return 200;
+}
+
+size_t json_buf_writer(char *buf, size_t count, void *arg) {
+  return evbuffer_add((struct evbuffer*)arg, buf, count);
 }
 
 // serve a directory response for the given request
-static int serve_directory(struct rs_request *request, struct stat *stat_buf) {
-  struct evbuffer *buf = evbuffer_new();
-  if(buf == NULL) {
-    log_error("evbuffer_new() failed: %s", strerror(errno));
-    return 500;
-  }
-  DIR *dir = opendir(request->path);
+static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path, struct stat *stat_buf) {
+  size_t disk_path_len = strlen(disk_path);
+  struct evbuffer *buf = request->buffer_out;
+  DIR *dir = opendir(disk_path);
   if(dir == NULL) {
     log_error("opendir() failed: %s", strerror(errno));
-    return 500;
+    return EVHTP_RES_SERVERR;
   }
   struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
-                                 pathconf(request->path, _PC_NAME_MAX) + 1);
+                                 pathconf(disk_path, _PC_NAME_MAX) + 1);
   struct dirent *resultp = NULL;
   if(entryp == NULL) {
     log_error("malloc() failed while creating directory pointer: %s",
               strerror(errno));
-    return 500;
+    return EVHTP_RES_SERVERR;
   }
+
+  struct json *json = new_json(json_buf_writer, buf);
+
   struct stat file_stat_buf;
   int entry_len;
-  int first = 1;
+
+  json_start_object(json);
+
   for(;;) {
     readdir_r(dir, entryp, &resultp);
     if(resultp == NULL) {
@@ -113,124 +365,98 @@ static int serve_directory(struct rs_request *request, struct stat *stat_buf) {
       // skip.
       continue;
     }
-    if(first) {
-      evbuffer_add(buf, "{", 1);
-      first = 0;
-    } else {
-      evbuffer_add(buf, ",", 1);
-    }
     entry_len = strlen(entryp->d_name);
-    char full_path[request->path_len + entry_len + 1];
-    sprintf(full_path, "%s%s", request->path, entryp->d_name);
+    char full_path[disk_path_len + entry_len + 1];
+    sprintf(full_path, "%s%s", disk_path, entryp->d_name);
     stat(full_path, &file_stat_buf);
-    
-    char *escaped_name = escape_name(entryp->d_name);
-    if(! escaped_name) {
-      // failed to allocate name
-      free(entryp);
-      free(dir);
-      return 500;
-    }
-    evbuffer_add_printf(buf, "\"%s%s\":%lld", escaped_name,
-                        S_ISDIR(file_stat_buf.st_mode) ? "/" : "",
-                        ((long long)file_stat_buf.st_mtime) * 1000);
-    free(escaped_name);
-  }
-  if(first) {
-    // empty directory.
-    evbuffer_add(buf, "{", 1);
-  }
-  evbuffer_add(buf, "}", 1);
-  struct rs_header type_header = {
-    .key = "Content-Type",
-    .value = "application/json",
-    .next = NULL
-  };
-  struct rs_header etag_header = {
-    .key = "ETag",
-    .value = make_etag(stat_buf),
-    .next = &type_header
-  };
-  if(etag_header.value == NULL) {
-    log_error("make_etag() failed");
+
+    char key_string[entry_len + 2];
+    sprintf(key_string, "%s%s", entryp->d_name,
+            S_ISDIR(file_stat_buf.st_mode) ? "/": "");
+    char *val_string = get_etag(full_path);
+
+    json_write_key_val(json, key_string, val_string);
+
+    free(val_string);
+  }
+
+  json_end_object(json);
+
+  free_json(json);
+
+  char *etag = get_etag(disk_path);
+  if(etag == NULL) {
+    log_error("get_etag() failed");
     free(entryp);
     closedir(dir);
-    return 500;
+    return EVHTP_RES_SERVERR;
   }
-  send_response_head(request, 200, &etag_header);
-  free(etag_header.value);
-  send_response_body(request, buf);
+
+  ADD_RESP_HEADER(request, "Content-Type", "application/json; charset=UTF-8");
+  ADD_RESP_HEADER_CP(request, "ETag", etag);
+
+  free(etag);
   free(entryp);
   closedir(dir);
-  return 0;
+  return EVHTP_RES_OK;
 }
 
-static char *get_xattr(const char *path, const char *key, int maxlen) {
-  int len = 32;
-  char *value = malloc(32);
-  for(value = malloc(len);len<=maxlen;value = realloc(value, len+=16)) {
-    if(value == NULL) {
-      log_error("malloc() / realloc() failed: %s", strerror(errno));
-      return NULL;
+static evhtp_res serve_file_head(evhtp_request_t *request, char *disk_path, struct stat *stat_buf, const char *mime_type) {
+
+  log_debug("serve file head");
+
+  if(request->uri->path->file == NULL) {
+    log_debug("HEAD dir requested");
+    // directory was requested
+    if(! S_ISDIR(stat_buf->st_mode)) {
+      log_debug("HEAD file found");
+      // but is actually a file
+      return EVHTP_RES_NOTFOUND;
     }
-    if(getxattr(path, key, value, len) > 0) {
-      return value;
-    } else {
-      if(errno == ERANGE) {
-        // buffer too small.
-        continue;
-      } else if(errno == ENOATTR) {
-        // attribute not set
-        free(value);
-        return NULL;
-      } else if(errno == ENOTSUP) {
-        // xattr not supported
-        log_error("File system doesn't support extended attributes! You may want to use another one.");
-        free(value);
-        return NULL;
-      } else {
-        log_error("Unexpected error while getting %s attribute: %s", key, strerror(errno));
-        free(value);
-        return NULL;
-      }
+    log_debug("HEAD directory found");
+  } else {
+    log_debug("HEAD file requested");
+    // file was requested
+    if(S_ISDIR(stat_buf->st_mode)) {
+      log_debug("HEAD directory found");
+      // but is actually a directory
+      return EVHTP_RES_NOTFOUND;
     }
+    log_debug("HEAD file found");
   }
-  log_error("%s attribute seems to be longer than %d bytes. That is simply unreasonable.", key, maxlen);
-  free(value);
-  return NULL;
-}
-
-static char *mime_type_from_xattr(const char *path) {
-  char *mime_type = get_xattr(path, "user.mime_type", 128);
-  if(mime_type == NULL) {
-    return NULL;
+    
+  char *etag_string = get_etag(disk_path);
+  if(etag_string == NULL) {
+    log_error("get_etag() failed");
+    return EVHTP_RES_SERVERR;
   }
-  char *charset = get_xattr(path, "user.charset", 64);
-  if(charset == NULL) {
-    return mime_type;
+
+  evhtp_header_t *if_none_match_header = evhtp_headers_find_header(request->headers_in, "If-None-Match");
+  if(if_none_match_header) {
+    // FIXME: support multiple comma-separated ETags in If-None-Match header
+    if(strcmp(if_none_match_header->val, etag_string) == 0) {
+      free(etag_string);
+      return EVHTP_RES_NOTMOD;
+    }
   }
-  int mt_len = strlen(mime_type);
-  char *full_mime_type = realloc(mime_type, mt_len + strlen(charset) + 10 + 1);
-  if(full_mime_type == NULL) {
-    log_error("realloc() failed: %s", strerror(errno));
-    free(charset);
-    return mime_type;
+
+  char *length_string = malloc(24);
+  if(length_string == NULL) {
+    log_error("malloc() failed: %s", strerror(errno));
+    free(etag_string);
+    return EVHTP_RES_SERVERR;
   }
-  sprintf(full_mime_type + mt_len, "; charset=%s", charset);
-  free(charset);
-  return full_mime_type;
-}  
+  snprintf(length_string, 24, "%ld", stat_buf->st_size);
 
-static int serve_file_head(struct rs_request *request, struct stat *stat_buf, const char *mime_type) {
   int free_mime_type = 0;
   // mime type is either passed in ... (such as for directory listings)
   if(mime_type == NULL) {
     // ... or detected based on xattr
-    mime_type = mime_type_from_xattr(request->path);
+    mime_type = content_type_from_xattr(disk_path);
     if(mime_type == NULL) {
       // ... or guessed by libmagic
       log_debug("mime type not given, detecting...");
-      mime_type = magic_file(magic_cookie, request->path);
+      mime_type = magic_file(magic_cookie, disk_path);
       if(mime_type == NULL) {
         // ... or defaulted to "application/octet-stream"
         log_error("magic failed: %s", magic_error(magic_cookie));
@@ -241,38 +467,12 @@ static int serve_file_head(struct rs_request *request, struct stat *stat_buf, co
       free_mime_type = 1;
     }
   }
-  struct rs_header content_type_header = {
-    .key = "Content-Type",
-    // header won't be freed (it's completely stack based / constant),
-    // so cast is valid here to satisfy type check.
-    // (struct rs_header.value cannot be constant, because in the case of a request
-    //  header it will be free()'d at some point)
-    .value = (char*)mime_type,
-    .next = NULL
-  };
-  char *length_string = malloc(24);
-  if(length_string == NULL) {
-    log_error("malloc() failed: %s", strerror(errno));
-    return 500;
-  }
-  snprintf(length_string, 24, "%ld", stat_buf->st_size);
-  struct rs_header length_header = {
-    .key = "Content-Length",
-    .value = length_string,
-    .next = &content_type_header
-  };
-  char *etag_string = make_etag(stat_buf);
-  if(etag_string == NULL) {
-    log_error("make_etag() failed");
-    free(length_string);
-    return 500;
-  }
-  struct rs_header etag_header = {
-    .key = "ETag",
-    .value = etag_string,
-    .next = &length_header
-  };
-  send_response_head(request, 200, &etag_header);
+
+  log_info("setting Content-Type of %s: %s", request->uri->path->full, mime_type);
+  ADD_RESP_HEADER_CP(request, "Content-Type", mime_type);
+  ADD_RESP_HEADER_CP(request, "Content-Length", length_string);
+  ADD_RESP_HEADER_CP(request, "ETag", etag_string);
+
   free(etag_string);
   free(length_string);
   if(free_mime_type) {
@@ -282,61 +482,139 @@ static int serve_file_head(struct rs_request *request, struct stat *stat_buf, co
 }
 
 // serve a file body for the given request
-static int serve_file(struct rs_request *request, struct stat *stat_buf) {
-  int fd = open(request->path, O_RDONLY | O_NONBLOCK);
+static evhtp_res serve_file(evhtp_request_t *request, const char *disk_path, struct stat *stat_buf) {
+  int fd = open(disk_path, O_RDONLY | O_NONBLOCK);
   if(fd < 0) {
     log_error("open() failed: %s", strerror(errno));
-    return 500;
+    return EVHTP_RES_SERVERR;
   }
-  send_response_body_fd(request, fd);
-  return 0;
+  while(evbuffer_read(request->buffer_out, fd, 4096) != 0);
+  close(fd);
+  return EVHTP_RES_OK;
+}
+
+static char *make_disk_path(char *dom_user, char *path, gss_buffer_t authuser, char **storage_root) {
+
+  // FIXME: use passwd->pwdir instead of /home/{user}/
+
+  // calculate maximum length of path
+  int pathlen = ( strlen(dom_user) + strlen(path) +
+                  6 + // "/home/"
+                  1 + // another slash
+                  RS_HOME_SERVE_ROOT_LEN );
+  char *disk_path = malloc(pathlen + 1);
+  char *xsfile = NULL;
+  FILE *xsf;
+  char principal [1026];
+  bool authorized;
+  if(disk_path == NULL) {
+    log_error("malloc() failed: %s", strerror(errno));
+    return NULL;
+  }
+  log_debug("Constructing disk_path for dom_user = \"%s\"", dom_user);
+  xsfile = malloc( 7 + RS_HOME_SERVE_ROOT_LEN + strlen(dom_user) + 1 + 17);
+  if(xsfile == NULL) {
+    log_error("malloc() failed: %s", strerror(errno));
+    free(disk_path);
+    return NULL;
+  }
+  sprintf(xsfile, "/home/%s/%s/.k5remotestorage", dom_user, RS_HOME_SERVE_ROOT);
+  log_debug("Access control list = \"%s\"", xsfile);
+  xsf = fopen (xsfile, "r");
+  authorized = false;
+  if (xsf) {
+    while ((!authorized) && fgets (principal, sizeof (principal)-1, xsf)) {
+      int len = strlen (principal);
+      if ((len > 1) && (principal [len-1] == '\n')) {
+        principal [--len] = '\0';
+      }
+      log_debug("Considering acceptable principal \"%s\"", principal);
+      authorized = (len == authuser->length) && (0 == memcmp (principal, authuser->value, len));
+    }
+    fclose (xsf);
+  } else {
+    log_error ("Failed to open access control list");
+    free(xsfile);
+    free(disk_path);
+    return NULL;
+  }
+  if (!authorized) {
+    log_error ("Access control list does not contain authorized user");
+    free(xsfile);
+    free(disk_path);
+    return NULL;
+  }
+  log_debug ("xsfile = \"%s\"", xsfile);
+  if(storage_root) {
+    // Cut off .k5remotestorage and reuse for *storage_root
+    xsfile [7 + RS_HOME_SERVE_ROOT_LEN + strlen (dom_user)] = '\0';
+    *storage_root = xsfile;
+    log_debug ("storage_root = \"%s\"", storage_root);
+  } else {
+    free (xsfile);
+    xsfile = NULL;
+  }
+  // remove all /.. segments
+  // (we don't try to resolve them, but instead treat them as garbage)
+  char *pos = NULL;
+  while((pos = strstr(path, "/..")) != NULL) { // FIXME: this would also filter out valid paths like /foo/..bar
+    int restlen = strlen(pos + 3);
+    memmove(pos, pos + 3, restlen);
+    pos[restlen] = 0;
+  }
+  // remove all duplicate slashes (nasty things may be done with them at times)
+  while((pos = strstr(path, "//")) != NULL) {
+    int restlen = strlen(pos + 2);
+    memmove(pos, pos + 2, restlen);
+    pos[restlen] = 0;
+  }
+  // build path
+  sprintf(disk_path, "/home/%s/%s%s", dom_user, RS_HOME_SERVE_ROOT, path);
+  log_debug ("disk_path = \"%s\"", disk_path);
+  return disk_path;
 }
 
-static int handle_get_or_head(struct rs_request *request, int include_body) {
+static evhtp_res handle_get_or_head(evhtp_request_t *request, gss_buffer_t authuser, int include_body) {
+
+  log_debug("HANDLE GET / HEAD (body: %s)", include_body ? "true" : "false");
+
+  char *disk_path = make_disk_path(REQUEST_GET_USER(request),
+                                   REQUEST_GET_PATH(request),
+                                  authuser,
+                                   NULL);
+  if(disk_path == NULL) {
+    return EVHTP_RES_SERVERR;
+  }
+
   // stat
   struct stat stat_buf;
-  if(stat(request->path, &stat_buf) != 0) {
-    if(errno != ENOENT) {
-      log_error("stat() failed for path \"%s\": %s", request->path, strerror(errno));
-      return 500;
+  if(stat(disk_path, &stat_buf) != 0) {
+    if(errno != ENOENT && errno != ENOTDIR) {
+      log_error("stat() failed for path \"%s\": %s", disk_path, strerror(errno));
+      return EVHTP_RES_SERVERR;
     } else {
-      return 404;
+      return EVHTP_RES_NOTFOUND;
     }
   }
   // check for directory
-  if(request->path[request->path_len - 1] == '/') {
+  if(request->uri->path->file == NULL) {
     // directory requested
-    if(! S_ISDIR(stat_buf.st_mode)) {
-      // not a directory.
-      return 404;
-    }
-    // directory found
     if(include_body) {
-      return serve_directory(request, &stat_buf);
+      return serve_directory(request, disk_path, &stat_buf);
     } else {
-      int head_result = serve_file_head(request, &stat_buf, "application/json");
-      if(head_result != 0) {
-        return head_result;
-      }
-      send_response_empty(request);
-      return 0;
+      evhtp_res head_status = serve_file_head(request, disk_path, &stat_buf, "application/json");
+      return head_status != 0 ? head_status : EVHTP_RES_OK;
     }
   } else {
     // file requested
-    if(S_ISDIR(stat_buf.st_mode)) {
-      // found, but is a directory
-      return 404;
-    }
-    // file found
-    int head_result = serve_file_head(request, &stat_buf, NULL);
+    evhtp_res head_result = serve_file_head(request, disk_path, &stat_buf, NULL);
     if(head_result != 0) {
       return head_result;
     }
     if(include_body) {
-      return serve_file(request, &stat_buf);
+      return serve_file(request, disk_path, &stat_buf);
     } else {
-      send_response_empty(request);
-      return 0;
+      return EVHTP_RES_OK;
     }
   }
 }