DELETE works again as well!
author() <nilclass@riseup.net>
Sun, 16 Jun 2013 20:53:17 +0000 (22:53 +0200)
committer() <nilclass@riseup.net>
Sun, 16 Jun 2013 20:53:17 +0000 (22:53 +0200)
src/handler/storage.c
src/process/main.c
src/storage.c [new file with mode: 0644]

index 20f7fe7..350898d 100644 (file)
@@ -43,28 +43,36 @@ evhtp_res storage_handle_get(evhtp_request_t *request) {
 }
 
 evhtp_res storage_handle_put(evhtp_request_t *request) {
+
+  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->uri->path->match_start,
                                    request->uri->path->match_end,
                                    &storage_root);
+  log_debug("PUT to %s, resolved to %s on filesystem (storage root: %s)",
+            request->uri->path->match_end, disk_path, storage_root);
   if(disk_path == NULL) {
     return 500;
   }
-  char *disk_path_copy = strdup(disk_path);
-  if(disk_path_copy == NULL) {
+  char *path_copy = strdup(request->uri->path->match_end);
+  if(path_copy == NULL) {
     log_error("strdup() failed: %s", strerror(errno));
     free(disk_path);
     free(storage_root);
     return 500;
   }
-  char *dir_path = dirname(disk_path_copy);
+  char *dir_path = dirname(path_copy);
   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(disk_path_copy);
+    free(path_copy);
     free(storage_root);
     return 500;
   }
@@ -78,7 +86,7 @@ evhtp_res storage_handle_put(evhtp_request_t *request) {
         log_error("Can't PUT to %s, found a non-directory parent.", request->uri->path->full);
         close(dirfd);
         free(disk_path);
-        free(disk_path_copy);
+        free(path_copy);
         free(storage_root);
         return 400;
       } else {
@@ -89,7 +97,7 @@ evhtp_res storage_handle_put(evhtp_request_t *request) {
         log_error("mkdirat() failed: %s", strerror(errno));
         close(dirfd);
         free(disk_path);
-        free(disk_path_copy);
+        free(path_copy);
         free(storage_root);
         return 500;
       }
@@ -101,13 +109,13 @@ evhtp_res storage_handle_put(evhtp_request_t *request) {
       log_error("failed to openat() next directory (\"%s\"): %s",
                 dir_name, strerror(errno));
       free(disk_path);
-      free(disk_path_copy);
+      free(path_copy);
       free(storage_root);
       return 500;
     }
   }
 
-  free(disk_path_copy);
+  free(path_copy);
   free(storage_root);
   close(dirfd);
 
@@ -156,7 +164,83 @@ evhtp_res storage_handle_put(evhtp_request_t *request) {
 }
 
 evhtp_res storage_handle_delete(evhtp_request_t *request) {
-  return 501;
+
+  if(request->uri->path->file == NULL) {
+    // DELETE to directories aren't allowed
+    return 400;
+  }
+
+  char *storage_root = NULL;
+  char *disk_path = make_disk_path(request->uri->path->match_start,
+                                   request->uri->path->match_end,
+                                   &storage_root);
+  if(disk_path == NULL) {
+    return 500;
+  }
+
+  struct stat stat_buf;
+  if(stat(disk_path, &stat_buf) == 0) {
+
+    if(S_ISDIR(stat_buf.st_mode)) {
+      return 400;
+    }
+
+    // file exists, delete it.
+    if(unlink(disk_path) == -1) {
+      log_error("unlink() failed: %s", strerror(errno));
+      return 500;
+    }
+
+    log_debug("match_end=%s", request->uri->path->match_end);
+    
+    /* 
+     * remove empty parents
+     */
+    char *path_copy = strdup(request->uri->path->match_end);
+    if(path_copy == NULL) {
+      log_error("strdup() failed to copy path: %s", strerror(errno));
+      free(disk_path);
+      return 500;
+    }
+    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 500;
+    }
+    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 500;
+        }
+      }
+    }
+    close(rootdirfd);
+    free(path_copy);
+  } else {
+    // file doesn't exist, ignore it.
+  }
+
+  free(storage_root);
+
+  return 200;
 }
 
 static char *make_etag(struct stat *stat_buf) {
@@ -449,27 +533,35 @@ static char *make_disk_path(char *user, char *path, char **storage_root) {
                   6 + // "/home/"
                   1 + // another slash
                   RS_HOME_SERVE_ROOT_LEN );
-  char *disk_path = malloc(pathlen);
+  char *disk_path = malloc(pathlen + 1);
   if(disk_path == NULL) {
     log_error("malloc() failed: %s", strerror(errno));
     return NULL;
   }
   if(storage_root) {
-    *storage_root = malloc( 6 + RS_HOME_SERVE_ROOT_LEN + 1 + strlen(user) );
+    *storage_root = malloc( 7 + RS_HOME_SERVE_ROOT_LEN + strlen(user) + 1);
     if(*storage_root == NULL) {
       log_error("malloc() failed: %s", strerror(errno));
       free(disk_path);
       return NULL;
     }
     sprintf(*storage_root, "/home/%s/%s", user, RS_HOME_SERVE_ROOT);
+
+    log_debug("have built storage_root, it's: %p", *storage_root);
   }
   // remove all /.. segments
   // (we don't try to resolve them, but instead treat them as garbage)
-  char *traverse_pos = NULL;
-  while((traverse_pos = strstr(path, "/..")) != NULL) {
-    int restlen = strlen(traverse_pos + 3);
-    memmove(traverse_pos, traverse_pos + 3, restlen);
-    traverse_pos[restlen] = 0;
+  char *pos = NULL;
+  while((pos = strstr(path, "/..")) != NULL) {
+    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", user, RS_HOME_SERVE_ROOT, path);
@@ -478,13 +570,20 @@ static char *make_disk_path(char *user, char *path, char **storage_root) {
                       
 
 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body) {
+  char *storage_root = NULL;
   char *disk_path = make_disk_path(request->uri->path->match_start,
-                                   request->uri->path->match_end, NULL);
+                                   request->uri->path->match_end,
+                                   &storage_root);
   if(disk_path == NULL) {
     return 500;
   }
 
-  log_debug("HANDLE GET OR HEAD, DISK PATH: %s", disk_path);
+  log_debug("GET/HEAD to %s, resolved to %s on filesystem (storage root: %s)",
+            request->uri->path->match_end, disk_path, storage_root);
+
+  log_debug("storage root is %p, disk_path is %p", storage_root, disk_path);
+
+  free(storage_root);
 
   // stat
   struct stat stat_buf;
index 1ee3e4e..01ced66 100644 (file)
@@ -145,10 +145,10 @@ static void handle_storage(evhtp_request_t *req, void *arg) {
     break;
   case htp_method_PUT:
     req->status = storage_handle_put(req);
-   break;
-  //case htp_method_DELETE:
-  //  req->status = storage_handle_delete(req);
-  //  break;
+    break;
+  case htp_method_DELETE:
+    req->status = storage_handle_delete(req);
+    break;
   default:
     req->status = EVHTP_RES_METHNALLOWED;
   }
diff --git a/src/storage.c b/src/storage.c
new file mode 100644 (file)
index 0000000..42d6799
--- /dev/null
@@ -0,0 +1,431 @@
+/*
+ * rs-serve - (c) 2013 Niklas E. Cathor
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "rs-serve.h"
+
+#define EXTRACT_PATH(request) (evhttp_request_get_uri(request) + RS_STORAGE_PATH_LEN)
+
+static char *make_disk_path(struct parsed_path *parsed_path, int *disk_path_len) {
+  char *disk_path = NULL;
+  if(RS_SERVE_HOMES) {
+    // TODO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
+    // 1) get user home
+    // 2) append RS_SERVE_HOMES_DIR
+    // 3) append scope
+    // 4) append rest
+  } else {
+    // TODO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
+    // 1) start with RS_REAL_STORAGE_ROOT
+    // 2) append scope
+    // 3) append rest
+    /*    *disk_path_len = path_len + RS_REAL_STORAGE_ROOT_LEN;
+    disk_path = malloc(*disk_path_len + 1);
+    if(disk_path == NULL) {
+      perror("malloc() failed while allocating disk path");
+      return NULL;
+    }
+    sprintf(disk_path, "%s%s", RS_REAL_STORAGE_ROOT, path);*/
+  }
+  return disk_path;
+}
+
+/**
+ * escape_name(name)
+ *
+ * Escape all occurances of quotes ('"') and backslashes ('\') to make the
+ * resulted string usable in a directory listing.
+ *
+ * Allocates a new string that needs to be free()d by the caller.
+ */
+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;
+  }
+  const char *name_p;
+  for(name_p = name; *name_p != 0; name_p++) {
+    if(*name_p == '"' || *name_p == '\\') {
+      escaped[i++] = '\\';
+    }
+    escaped[i++] = *name_p;
+  }
+  escaped[i++] = 0;
+  escaped = realloc(escaped, i);
+  return escaped;
+}
+
+/**
+ * storage_options(request)
+ *
+ * Handle a OPTIONS request by sending an empty response with appropriate
+ * CORS headers.
+ */
+void storage_options(struct evhttp_request *request) {
+  struct evkeyvalq *headers = evhttp_request_get_output_headers(request);
+  add_cors_headers(headers);
+
+  evhttp_send_reply(request, HTTP_OK, NULL, NULL);
+}
+
+void storage_get(struct evhttp_request *request, int sendbody) {
+  struct evkeyvalq *headers = evhttp_request_get_output_headers(request);
+  add_cors_headers(headers);
+
+  int error_response;
+  struct parsed_path *parsed_path = parse_path(EXTRACT_PATH(request),
+                                               &error_response);
+  if(parsed_path == NULL) {
+    evhttp_send_error(request, error_response, NULL);
+    return;
+  }
+
+  if(authorize_request(request, parsed_path, 0) != 0) {
+    free_parsed_path(parsed_path);
+    return;
+  }
+
+  int disk_path_len;
+  char *disk_path = make_disk_path(parsed_path, &disk_path_len);
+  if(disk_path == NULL) {
+    // failed to allocate disk path
+    evhttp_send_error(request, HTTP_INTERNAL, NULL);
+    return;
+  }
+
+  free_parsed_path(parsed_path);
+
+  struct stat stat_buf;
+  struct evbuffer *buf = evbuffer_new();
+
+  if(buf == NULL) {
+    perror("evbuffer_new() failed");
+    free(disk_path);
+    evhttp_send_error(request, HTTP_INTERNAL, NULL);
+    return;
+  }
+
+  if(stat(disk_path, &stat_buf) != 0) {
+    // stat failed
+    if(errno != ENOENT) {
+      fprintf(stderr, "while getting %s, ", disk_path);
+      perror("stat() failed");
+    }
+    if(path[path_len - 1] == '/') {
+      // empty dir listing.
+      evhttp_add_header(headers, "Content-Type", "application/json");
+      evbuffer_add(buf, "{}", 2);
+      evhttp_send_reply(request, HTTP_OK, NULL, buf);
+    } else {
+      evhttp_send_error(request, HTTP_NOTFOUND, NULL);
+    }
+  } else if(path[path_len - 1] == '/') {
+    // directory requested
+    if(S_ISDIR(stat_buf.st_mode)) {
+      // directory found
+      evhttp_add_header(headers, "Content-Type", "application/json");
+      if(sendbody) {
+        // GET response
+        DIR *dir = opendir(disk_path);
+        if(dir == NULL) {
+          perror("opendir() failed");
+          evhttp_send_error(request, HTTP_INTERNAL, NULL);
+        } else {
+          struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
+                                         pathconf(disk_path, _PC_NAME_MAX) + 1);
+          struct dirent *resultp = NULL;
+          if(entryp == NULL) {
+            perror("malloc() failed while creating directory pointer");
+            evhttp_send_error(request, HTTP_INTERNAL, NULL);
+          } else {
+            struct stat file_stat_buf;
+            int entry_len;
+            int first = 1;
+            for(;;) {
+              readdir_r(dir, entryp, &resultp);
+              if(resultp == NULL) {
+                break;
+              }
+              if(strcmp(entryp->d_name, ".") == 0 ||
+                 strcmp(entryp->d_name, "..") == 0) {
+                // skip.
+                continue;
+              }
+              if(first) {
+                evbuffer_add(buf, "{", 1);
+                first = 0;
+              } else {
+                evbuffer_add(buf, ",", 1);
+              }
+              entry_len = strlen(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
+                evhttp_send_error(request, HTTP_INTERNAL, NULL);
+                free(entryp);
+                free(dir);
+                free(disk_path);
+                return;
+              }
+              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);
+            evhttp_send_reply(request, HTTP_OK, NULL, buf);
+          }
+          free(entryp);
+          closedir(dir);
+        }
+      } else {
+        // HEAD response
+        evhttp_send_reply(request, HTTP_OK, NULL, NULL);
+      }
+    } else {
+      // found, but not a directory (FIXME!)
+      evhttp_send_error(request, HTTP_NOTFOUND, NULL);
+    }
+  } else {
+    // file requested
+    if(S_ISREG(stat_buf.st_mode)) {
+      const char *mime_type = magic_file(magic_cookie, disk_path);
+      if(mime_type == NULL) {
+        fprintf(stderr, "magic failed: %s\n", magic_error(magic_cookie));
+      } else {
+        evhttp_add_header(headers, "Content-Type", mime_type);
+      }
+      // file found
+      if(sendbody) {
+        // GET response
+        evbuffer_add_file(buf, open(disk_path, O_NONBLOCK), 0, stat_buf.st_size);
+        evhttp_send_reply(request, HTTP_OK, NULL, buf);
+      } else {
+        // HEAD response
+        char len[100];
+        // not sure how to format off_t correctly, so casting to longest int.
+        snprintf(len, 99, "%lld", (long long int)stat_buf.st_size);
+        evhttp_add_header(headers, "Content-Length", len);
+        // TODO: add ETag
+        evhttp_send_reply(request, HTTP_OK, NULL, NULL);
+      }
+    } else {
+      // found, but not a regular file (FIXME!)
+      evhttp_send_error(request, HTTP_NOTFOUND, NULL);
+    }
+  }
+
+  free(disk_path);
+  evbuffer_free(buf);
+}
+
+/**
+ * storage_put(request)
+ *
+ * Handle a PUT request. If the request URI carries a trailing slash, the path
+ * is interpreted as a directory and thus BADREQUEST is sent.
+ * Otherwise a file and possibly it's parent directories are created and the
+ * request body is written there.
+ *
+ * The mode of newly created files depends on RS_FILE_CREATE_MODE (and the
+ * current umask).
+ */
+void storage_put(struct evhttp_request *request) {
+  struct evkeyvalq *headers = evhttp_request_get_output_headers(request);
+  add_cors_headers(headers);
+
+  const char *path = EXTRACT_PATH(request);
+  int path_len = strlen(path);
+
+  if(validate_path(request, path) != 0) {
+    return;
+  }
+
+  char *scope = extract_scope(path);
+  if(scope == NULL) {
+    evhttp_send_error(request, HTTP_INTERNAL, NULL);
+    return;
+  }
+
+  if(authorize_request(request, scope, 1) != 0) {
+    free(scope);
+    return;
+  }
+  free(scope);
+
+  if(path[path_len - 1] == '/') {
+    // no PUT on directories.
+    evhttp_send_error(request, HTTP_BADREQUEST, NULL);
+  } else {
+    struct evbuffer *buf = evhttp_request_get_input_buffer(request);
+
+    // create parent(s)
+    char *path_copy = strdup(path);
+    char *dir_path = dirname(path_copy);
+    char *saveptr = NULL;
+    char *dir_name;
+    // directory fd for reference
+    int dirfd = open(RS_REAL_STORAGE_ROOT, O_RDONLY), prevfd;
+    if(dirfd == -1) {
+      perror("failed to open() storage root");
+      evhttp_send_error(request, HTTP_BADREQUEST, NULL);
+      free(path_copy);
+      return;
+    }
+    struct stat dir_stat;
+    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.
+          fprintf(stderr, "Can't PUT to %s, found a non-directory parent.\n", path);
+          evhttp_send_error(request, HTTP_BADREQUEST, NULL);
+          free(path_copy);
+          return;
+        }
+      } else {
+        // directory doesn't exist, create it.
+        if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
+          perror("mkdirat() failed");
+          evhttp_send_error(request, HTTP_INTERNAL, NULL);
+          free(path_copy);
+          return;
+        }
+      }
+      prevfd = dirfd;
+      dirfd = openat(prevfd, dir_name, O_RDONLY);
+      close(prevfd);
+    }
+
+    // dirname() possibly made previous copy unusable
+    strcpy(path_copy, path);
+    char *file_name = basename(path_copy);
+
+    // open (and possibly create) file
+    int fd = openat(dirfd, file_name,
+                    O_NONBLOCK | O_CREAT | O_WRONLY | O_TRUNC,
+                    RS_FILE_CREATE_MODE);
+    free(path_copy);
+    close(dirfd);
+    if(fd == -1) {
+      perror("openat() failed");
+      evhttp_send_error(request, HTTP_INTERNAL, NULL);
+    } else if(evbuffer_write(buf, fd) == -1) {
+      perror("evbuffer_write() failed");
+      evhttp_send_error(request, HTTP_INTERNAL, NULL);
+    } else {
+      // writing succeeded
+      // TODO: add ETag
+      evhttp_send_reply(request, HTTP_OK, NULL, NULL);
+    }
+  }
+}
+
+void storage_delete(struct evhttp_request *request) {
+  /* struct evkeyvalq *headers = evhttp_request_get_output_headers(request); */
+  /* add_cors_headers(headers); */
+
+  /* const char *path = EXTRACT_PATH(request); */
+  /* int path_len = strlen(path); */
+
+  /* if(validate_path(request, path) != 0) { */
+  /*   return; */
+  /* } */
+
+  /* char *scope = extract_scope(path); */
+  /* if(scope == NULL) { */
+  /*   evhttp_send_error(request, HTTP_INTERNAL, NULL); */
+  /*   return; */
+  /* } */
+
+  /* if(authorize_request(request, scope, 1) != 0) { */
+  /*   free(scope); */
+  /*   return; */
+  /* } */
+  /* free(scope); */
+
+  /* if(path[path_len - 1] == '/') { */
+  /*   // no DELETE on directories. */
+  /*   evhttp_send_error(request, HTTP_BADREQUEST, NULL); */
+  /*   return; */
+  /* } */
+
+  /* int disk_path_len; */
+  /* char *disk_path = make_disk_path(path, path_len, &disk_path_len); */
+
+  /* if(disk_path == NULL) { */
+  /*   evhttp_send_error(request, HTTP_INTERNAL, NULL); */
+  /*   return; */
+  /* } */
+
+  /* struct stat stat_buf; */
+  /* if(stat(disk_path, &stat_buf) == 0) { */
+  /*   // file exists, delete it. */
+  /*   unlink(disk_path); */
+
+  /*   // remove empty parents */
+  /*   char *path_copy = strdup(path); */
+  /*   if(path_copy == NULL) { */
+  /*     perror("strdup() failed to copy path"); */
+  /*     evhttp_send_error(request, HTTP_INTERNAL, NULL); */
+  /*     free(disk_path); */
+  /*     return; */
+  /*   } */
+  /*   char *dir_path; */
+  /*   int rootdirfd = open(RS_REAL_STORAGE_ROOT, O_RDONLY); */
+  /*   if(rootdirfd == -1) { */
+  /*     perror("failed to open() storage root"); */
+  /*     evhttp_send_error(request, HTTP_INTERNAL, NULL); */
+  /*     free(path_copy); */
+  /*     free(disk_path); */
+  /*     return; */
+  /*   } */
+  /*   int result; */
+  /*   // skip leading slash */
+  /*   char *relative_path = path_copy + 1; */
+  /*   for(dir_path = dirname(relative_path); */
+  /*       dir_path[1] != 0; // reached root */
+  /*       dir_path = dirname(dir_path)) { */
+  /*     result = unlinkat(rootdirfd, dir_path, AT_REMOVEDIR); */
+  /*     if(result != 0) { */
+  /*       if(errno == ENOTEMPTY || errno == EEXIST) { */
+  /*         // non-empty directory reached */
+  /*         break; */
+  /*       } else { */
+  /*         // other error occured */
+  /*         fprintf(stderr, "(while trying to remove %s)\n", dir_path); */
+  /*         perror("unlinkat() failed to remove parent directory"); */
+  /*         evhttp_send_error(request, HTTP_INTERNAL, NULL); */
+  /*         free(path_copy); */
+  /*         free(disk_path); */
+  /*         return; */
+  /*       } */
+  /*     } */
+  /*   } */
+  /*   close(rootdirfd); */
+  /*   free(path_copy); */
+  /* } else { */
+  /*   // file doesn't exist, ignore it. */
+  /* } */
+  /* evhttp_send_reply(request, HTTP_OK, NULL, NULL); */
+  /* free(disk_path); */
+}