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 *escape_name(const char *name);
25 static char *make_etag(struct stat *stat_buf);
26 static char *make_disk_path(char *user, char *path, char **storage_root);
27 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path,
28 struct stat *stat_buf);
29 static int serve_file_head(evhtp_request_t *request_t, char *disk_path,
30 struct stat *stat_buf,const char *mime_type);
31 static int serve_file(evhtp_request_t *request, const char *disk_path,
32 struct stat *stat_buf);
33 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body);
34 static int content_type_to_xattr(int fd, const char *content_type);
37 evhtp_res storage_handle_head(evhtp_request_t *request) {
38 return handle_get_or_head(request, 0);
41 evhtp_res storage_handle_get(evhtp_request_t *request) {
42 return handle_get_or_head(request, 1);
45 /* struct rs_put_context { */
47 /* struct evbuffer *buf; */
48 /* evhtp_request_t *request; */
51 /* static void finalize_put(struct rs_put_context *context) { */
52 /* close(context->fd); */
53 /* evbuffer_free(context->buf); */
54 /* evhtp_send_reply(context->request, 200); */
58 /* static void write_from_put(struct bufferevent *bev, void *arg) { */
59 /* log_debug("write_from_put()"); */
60 /* struct rs_put_context *context = arg; */
61 /* bufferevent_read_buffer(bev, context->buf); */
62 /* size_t count = evbuffer_get_length(context->buf); */
63 /* if(count == 0) { */
64 /* log_debug("got 0 bytes, finalizing"); */
65 /* finalize_put(context); */
67 /* log_debug("got %d bytes, writing", count); */
68 /* evbuffer_write(context->buf, context->fd); */
72 /* static void put_event(struct bufferevent *bev, short what, void *arg) { */
73 /* log_debug("PUT event:"); */
74 /* log_debug(" - BEV_EVENT_READING: %d", (what & BEV_EVENT_READING) ? 1 : 0); */
75 /* log_debug(" - BEV_EVENT_WRITING: %d", (what & BEV_EVENT_WRITING) ? 1 : 0); */
76 /* log_debug(" - BEV_EVENT_EOF: %d", (what & BEV_EVENT_EOF) ? 1 : 0); */
77 /* log_debug(" - BEV_EVENT_ERROR: %d", (what & BEV_EVENT_ERROR) ? 1 : 0); */
78 /* log_debug(" - BEV_EVENT_TIMEOUT: %d", (what & BEV_EVENT_TIMEOUT) ? 1 : 0); */
79 /* log_debug(" - BEV_EVENT_CONNECTED: %d", (what & BEV_EVENT_CONNECTED) ? 1 : 0); */
82 evhtp_res storage_handle_put(evhtp_request_t *request) {
83 char *storage_root = NULL;
84 char *disk_path = make_disk_path(request->uri->path->match_start,
85 request->uri->path->match_end,
87 if(disk_path == NULL) {
90 char *disk_path_copy = strdup(disk_path);
91 if(disk_path_copy == NULL) {
92 log_error("strdup() failed: %s", strerror(errno));
97 char *dir_path = dirname(disk_path_copy);
100 int dirfd = open(storage_root, O_RDONLY), prevfd;
102 log_error("failed to open() storage path (\"%s\"): %s", storage_root, strerror(errno));
104 free(disk_path_copy);
108 struct stat dir_stat;
109 for(dir_name = strtok_r(dir_path, "/", &saveptr);
111 dir_name = strtok_r(NULL, "/", &saveptr)) {
112 if(fstatat(dirfd, dir_name, &dir_stat, 0) == 0) {
113 if(! S_ISDIR(dir_stat.st_mode)) {
114 // exists, but not a directory
115 log_error("Can't PUT to %s, found a non-directory parent.", request->uri->path->full);
118 free(disk_path_copy);
125 if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
126 log_error("mkdirat() failed: %s", strerror(errno));
129 free(disk_path_copy);
135 dirfd = openat(prevfd, dir_name, O_RDONLY);
138 log_error("failed to openat() next directory (\"%s\"): %s",
139 dir_name, strerror(errno));
141 free(disk_path_copy);
147 free(disk_path_copy);
151 // open (and possibly create) file
152 int fd = open(disk_path, O_NONBLOCK | O_CREAT | O_WRONLY | O_TRUNC,
153 RS_FILE_CREATE_MODE);
156 log_error("open() failed to open file \"%s\": %s", disk_path, strerror(errno));
161 /* struct rs_put_context *context = malloc(sizeof(struct rs_put_context)); */
162 /* if(context == NULL) { */
163 /* log_error("malloc() failed: %s", strerror(errno)); */
167 evbuffer_write(request->buffer_in, fd);
169 char *content_type = "application/octet-stream; charset=binary";
170 evhtp_kv_t *content_type_header = evhtp_headers_find_header(request->headers_in, "Content-Type");
172 if(content_type_header != NULL) {
173 content_type = content_type_header->val;
176 if(content_type_to_xattr(fd, content_type) != 0) {
177 log_error("Setting xattr for content type failed. Ignoring.");
182 struct stat stat_buf;
183 memset(&stat_buf, 0, sizeof(struct stat));
184 if(stat(disk_path, &stat_buf) != 0) {
185 log_error("failed to stat() file after writing: %s", strerror(errno));
190 char *etag_string = make_etag(&stat_buf);
192 ADD_RESP_HEADER_CP(request, "Content-Type", content_type);
193 ADD_RESP_HEADER_CP(request, "ETag", etag_string);
201 int storage_handle_delete(struct rs_request *request) {
205 static char *make_etag(struct stat *stat_buf) {
206 char *etag = malloc(21);
208 log_error("malloc() failed: %s", strerror(errno));
211 snprintf(etag, 20, "%lld", ((long long int)stat_buf->st_mtime) * 1000);
215 // escape backslashes (/) and double quotes (") to put the given string
216 // in quoted JSON strings.
217 static char *escape_name(const char *name) {
218 int max_len = strlen(name) * 2, i = 0;
219 char *escaped = malloc(max_len + 1);
220 if(escaped == NULL) {
221 perror("malloc() failed");
225 for(name_p = name; *name_p != 0; name_p++) {
226 if(*name_p == '"' || *name_p == '\\') {
229 escaped[i++] = *name_p;
232 escaped = realloc(escaped, i);
236 // serve a directory response for the given request
237 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path, struct stat *stat_buf) {
238 size_t disk_path_len = strlen(disk_path);
239 struct evbuffer *buf = evbuffer_new();
241 log_error("evbuffer_new() failed: %s", strerror(errno));
244 DIR *dir = opendir(disk_path);
246 log_error("opendir() failed: %s", strerror(errno));
249 struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
250 pathconf(disk_path, _PC_NAME_MAX) + 1);
251 struct dirent *resultp = NULL;
253 log_error("malloc() failed while creating directory pointer: %s",
257 struct stat file_stat_buf;
261 readdir_r(dir, entryp, &resultp);
262 if(resultp == NULL) {
265 if(strcmp(entryp->d_name, ".") == 0 ||
266 strcmp(entryp->d_name, "..") == 0) {
271 evbuffer_add(buf, "{", 1);
274 evbuffer_add(buf, ",", 1);
276 entry_len = strlen(entryp->d_name);
277 char full_path[disk_path_len + entry_len + 1];
278 sprintf(full_path, "%s%s", disk_path, entryp->d_name);
279 stat(full_path, &file_stat_buf);
281 log_debug("Listing add entry %s", entryp->d_name);
283 char *escaped_name = escape_name(entryp->d_name);
285 // failed to allocate name
290 evbuffer_add_printf(buf, "\"%s%s\":%lld", escaped_name,
291 S_ISDIR(file_stat_buf.st_mode) ? "/" : "",
292 ((long long)file_stat_buf.st_mtime) * 1000);
297 evbuffer_add(buf, "{", 1);
299 evbuffer_add(buf, "}\n", 2);
301 char *etag = make_etag(stat_buf);
303 log_error("make_etag() failed");
309 ADD_RESP_HEADER(request, "Content-Type", "application/json; charset=UTF-8");
310 ADD_RESP_HEADER_CP(request, "ETag", etag);
312 // FIXME: why do I need to use *_chunk_* here???
313 evhtp_send_reply_chunk_start(request, EVHTP_RES_OK);
314 evhtp_send_reply_chunk(request, buf);
315 evhtp_send_reply_chunk_end(request);
322 static char *get_xattr(const char *path, const char *key, int maxlen) {
324 char *value = malloc(32);
325 for(value = malloc(len);len<=maxlen;value = realloc(value, len+=16)) {
327 log_error("malloc() / realloc() failed: %s", strerror(errno));
330 int actual_len = getxattr(path, key, value, len);
331 log_debug("getxattr() for key %s returned actual length of %d bytes", key, actual_len);
335 if(errno == ERANGE) {
338 } else if(errno == ENOATTR) {
342 } else if(errno == ENOTSUP) {
343 // xattr not supported
344 log_error("File system doesn't support extended attributes! You may want to use another one.");
348 log_error("Unexpected error while getting %s attribute: %s", key, strerror(errno));
354 log_error("%s attribute seems to be longer than %d bytes. That is simply unreasonable.", key, maxlen);
359 static char *content_type_from_xattr(const char *path) {
360 char *mime_type = get_xattr(path, "user.mime_type", 128);
361 if(mime_type == NULL) {
364 char *charset = get_xattr(path, "user.charset", 64);
365 if(charset == NULL) {
368 int mt_len = strlen(mime_type);
369 char *content_type = realloc(mime_type, mt_len + strlen(charset) + 10 + 1);
370 if(content_type == NULL) {
371 log_error("realloc() failed: %s", strerror(errno));
375 log_debug("content_type now: %s (also charset is \"%s\")", content_type, charset);
376 sprintf(content_type + mt_len, "; charset=%s", charset);
381 static int content_type_to_xattr(int fd, const char *content_type) {
382 char *content_type_copy = strdup(content_type), *saveptr = NULL;
383 if(content_type_copy == NULL) {
384 log_error("strdup() failed: %s", strerror(errno));
387 char *mime_type = strtok_r(content_type_copy, ";", &saveptr);
388 log_debug("extracted mime type: %s", mime_type);
389 char *rest = strtok_r(NULL, "", &saveptr);
390 char *charset_begin = NULL, *charset = NULL;
392 charset_begin = strstr(rest, "charset=");
394 charset = charset_begin + 8;
395 log_debug("extracted charset: %s", charset);
398 if(charset == NULL) {
399 // FIXME: should this rather be binary or us-ascii?
401 log_debug("guessed charset: %s", charset);
403 if(fsetxattr(fd, "user.mime_type", mime_type, strlen(mime_type) + 1, 0) != 0) {
404 log_error("fsetxattr() failed: %s", strerror(errno));
405 free(content_type_copy);
408 log_debug("fsetxattr (mime_type) done");
409 if(fsetxattr(fd, "user.charset", mime_type, strlen(charset) + 1, 0) != 0) {
410 log_error("fsetxattr() failed: %s", strerror(errno));
411 free(content_type_copy);
414 log_debug("fsetxattr (charset) done");
415 free(content_type_copy);
420 static int serve_file_head(evhtp_request_t *request, char *disk_path, struct stat *stat_buf, const char *mime_type) {
421 log_debug("serve_file_head for path %s", disk_path);
422 int free_mime_type = 0;
423 // mime type is either passed in ... (such as for directory listings)
424 if(mime_type == NULL) {
425 // ... or detected based on xattr
426 mime_type = content_type_from_xattr(disk_path);
427 if(mime_type == NULL) {
428 // ... or guessed by libmagic
429 log_debug("mime type not given, detecting...");
430 mime_type = magic_file(magic_cookie, disk_path);
431 if(mime_type == NULL) {
432 // ... or defaulted to "application/octet-stream"
433 log_error("magic failed: %s", magic_error(magic_cookie));
434 mime_type = "application/octet-stream; charset=binary";
437 // xattr detected mime type and allocated memory for it
441 struct rs_header content_type_header = {
442 .key = "Content-Type",
443 // header won't be freed (it's completely stack based / constant),
444 // so cast is valid here to satisfy type check.
445 // (struct rs_header.value cannot be constant, because in the case of a request
446 // header it will be free()'d at some point)
447 .value = (char*)mime_type,
448 .next = &RS_DEFAULT_HEADERS
450 char *length_string = malloc(24);
451 if(length_string == NULL) {
452 log_error("malloc() failed: %s", strerror(errno));
455 snprintf(length_string, 24, "%ld", stat_buf->st_size);
456 char *etag_string = make_etag(stat_buf);
457 if(etag_string == NULL) {
458 log_error("make_etag() failed");
463 ADD_RESP_HEADER_CP(request, "Content-Length", length_string);
464 ADD_RESP_HEADER_CP(request, "ETag", etag_string);
466 log_debug("HAVE SET CONTENT LENGTH: %s", length_string);
468 evhtp_send_reply_chunk_start(request, 200);
473 free((char*)mime_type);
478 // serve a file body for the given request
479 static int serve_file(evhtp_request_t *request, const char *disk_path, struct stat *stat_buf) {
480 int fd = open(disk_path, O_RDONLY | O_NONBLOCK);
482 log_error("open() failed: %s", strerror(errno));
485 struct evbuffer *buf = evbuffer_new();
486 while(evbuffer_read(buf, fd, 4096) != 0) {
487 evhtp_send_reply_chunk(request, buf);
490 evhtp_send_reply_chunk_end(request);
494 static char *make_disk_path(char *user, char *path, char **storage_root) {
496 // FIXME: use passwd->pwdir instead of /home/{user}/
498 // calculate maximum length of path
499 int pathlen = ( strlen(user) + strlen(path) +
502 RS_HOME_SERVE_ROOT_LEN );
503 char *disk_path = malloc(pathlen);
504 if(disk_path == NULL) {
505 log_error("malloc() failed: %s", strerror(errno));
509 *storage_root = malloc( 6 + RS_HOME_SERVE_ROOT_LEN + 1 + strlen(user) );
510 if(*storage_root == NULL) {
511 log_error("malloc() failed: %s", strerror(errno));
515 sprintf(*storage_root, "/home/%s/%s", user, RS_HOME_SERVE_ROOT);
517 // remove all /.. segments
518 // (we don't try to resolve them, but instead treat them as garbage)
519 char *traverse_pos = NULL;
520 while((traverse_pos = strstr(path, "/..")) != NULL) {
521 int restlen = strlen(traverse_pos + 3);
522 memmove(traverse_pos, traverse_pos + 3, restlen);
523 traverse_pos[restlen] = 0;
526 sprintf(disk_path, "/home/%s/%s%s", user, RS_HOME_SERVE_ROOT, path);
531 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body) {
532 char *disk_path = make_disk_path(request->uri->path->match_start,
533 request->uri->path->match_end, NULL);
534 if(disk_path == NULL) {
538 log_debug("HANDLE GET OR HEAD, DISK PATH: %s", disk_path);
541 struct stat stat_buf;
542 if(stat(disk_path, &stat_buf) != 0) {
543 if(errno != ENOENT) {
544 log_error("stat() failed for path \"%s\": %s", disk_path, strerror(errno));
550 // check for directory
551 if(request->uri->path->file == NULL) {
552 // directory requested
553 if(! S_ISDIR(stat_buf.st_mode)) {
559 return serve_directory(request, disk_path, &stat_buf);
561 return serve_file_head(request, disk_path, &stat_buf, "application/json");
565 if(S_ISDIR(stat_buf.st_mode)) {
566 // found, but is a directory
570 int head_result = serve_file_head(request, disk_path, &stat_buf, NULL);
571 if(head_result != 0) {
575 return serve_file(request, disk_path, &stat_buf);