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
22 * These functions are only called from storage processes.
25 static char *escape_name(const char *name);
26 static char *make_etag(struct stat *stat_buf);
27 static char *make_disk_path(char *user, char *path);
28 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path,
29 struct stat *stat_buf);
30 static int serve_file_head(evhtp_request_t *request_t, char *disk_path,
31 struct stat *stat_buf,const char *mime_type);
32 static int serve_file(evhtp_request_t *request, const char *disk_path,
33 struct stat *stat_buf);
34 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body);
35 static int content_type_to_xattr(int fd, const char *content_type);
38 evhtp_res storage_handle_head(evhtp_request_t *request) {
39 return handle_get_or_head(request, 0);
42 evhtp_res storage_handle_get(evhtp_request_t *request) {
43 return handle_get_or_head(request, 1);
46 int storage_begin_put(struct rs_request *request) {
47 char *path_copy = strdup(request->path);
48 if(path_copy == NULL) {
49 log_error("strdup() failed: %s", strerror(errno));
52 char *dir_path = dirname(path_copy);
55 int dirfd = open("/", O_RDONLY), prevfd;
57 log_error("failed to open() root directory: %s", strerror(errno));
62 for(dir_name = strtok_r(dir_path, "/", &saveptr);
64 dir_name = strtok_r(NULL, "/", &saveptr)) {
65 if(fstatat(dirfd, dir_name, &dir_stat, 0) == 0) {
66 if(! S_ISDIR(dir_stat.st_mode)) {
67 // exists, but not a directory
68 log_error("Can't PUT to %s, found a non-directory parent.", request->path);
76 if(mkdirat(dirfd, dir_name, S_IRWXU | S_IRWXG) != 0) {
77 log_error("mkdirat() failed: %s", strerror(errno));
84 dirfd = openat(prevfd, dir_name, O_RDONLY);
87 log_error("failed to openat() next directory (\"%s\"): %s",
88 dir_name, strerror(errno));
94 char *expectation = NULL;
95 struct rs_header *header;
96 for(header = request->headers;
98 header = header->next) {
99 if(strcmp(header->key, "Expect") == 0) {
100 expectation = header->value;
105 // dirname() possibly made previous copy unusable
106 strcpy(path_copy, request->path);
107 char *file_name = basename(path_copy);
109 // open (and possibly create) file
110 int fd = openat(dirfd, file_name,
111 O_NONBLOCK | O_CREAT | O_WRONLY | O_TRUNC,
112 RS_FILE_CREATE_MODE);
117 log_error("openat() failed to open file \"%s\": %s", file_name, strerror(errno));
121 request->file_fd = fd;
123 if(expectation != NULL) {
124 if(strcasecmp(expectation, "100-continue") == 0) {
125 send_response_head(request, 100, NULL);
127 log_error("Expectation \"%s\" cannot be met.", expectation);
135 int storage_end_put(struct rs_request *request) {
136 struct stat stat_buf;
137 log_debug("fstatting now");
138 if(fstat(request->file_fd, &stat_buf) != 0) {
139 log_error("fstat() after PUT failed: %s", strerror(errno));
142 log_debug("trying to find content-type header");
143 struct rs_header *header;
144 char *content_type = "application/octet-stream; charset=binary";
145 for(header = request->headers;
147 header = header->next) {
148 if(strcmp(header->key, "Content-Type") == 0) {
149 log_debug("got content type header value: %s", header->value);
150 content_type = header->value;
154 if(content_type_to_xattr(request->file_fd, content_type) != 0) {
155 log_error("Setting xattr for content type failed. Ignoring.");
157 close(request->file_fd);
158 int head_result = serve_file_head(request, request->path, &stat_buf, content_type);
159 if(head_result != 0) {
162 send_response_empty(request);
166 int storage_handle_delete(struct rs_request *request) {
170 static char *make_etag(struct stat *stat_buf) {
171 char *etag = malloc(21);
173 log_error("malloc() failed: %s", strerror(errno));
176 snprintf(etag, 20, "%lld", ((long long int)stat_buf->st_mtime) * 1000);
180 // escape backslashes (/) and double quotes (") to put the given string
181 // in quoted JSON strings.
182 static char *escape_name(const char *name) {
183 int max_len = strlen(name) * 2, i = 0;
184 char *escaped = malloc(max_len + 1);
185 if(escaped == NULL) {
186 perror("malloc() failed");
190 for(name_p = name; *name_p != 0; name_p++) {
191 if(*name_p == '"' || *name_p == '\\') {
194 escaped[i++] = *name_p;
197 escaped = realloc(escaped, i);
201 // serve a directory response for the given request
202 static evhtp_res serve_directory(evhtp_request_t *request, char *disk_path, struct stat *stat_buf) {
203 size_t disk_path_len = strlen(disk_path);
204 struct evbuffer *buf = evbuffer_new();
206 log_error("evbuffer_new() failed: %s", strerror(errno));
209 DIR *dir = opendir(disk_path);
211 log_error("opendir() failed: %s", strerror(errno));
214 struct dirent *entryp = malloc(offsetof(struct dirent, d_name) +
215 pathconf(disk_path, _PC_NAME_MAX) + 1);
216 struct dirent *resultp = NULL;
218 log_error("malloc() failed while creating directory pointer: %s",
222 struct stat file_stat_buf;
226 readdir_r(dir, entryp, &resultp);
227 if(resultp == NULL) {
230 if(strcmp(entryp->d_name, ".") == 0 ||
231 strcmp(entryp->d_name, "..") == 0) {
236 evbuffer_add(buf, "{", 1);
239 evbuffer_add(buf, ",", 1);
241 entry_len = strlen(entryp->d_name);
242 char full_path[disk_path_len + entry_len + 1];
243 sprintf(full_path, "%s%s", disk_path, entryp->d_name);
244 stat(full_path, &file_stat_buf);
246 log_debug("Listing add entry %s", entryp->d_name);
248 char *escaped_name = escape_name(entryp->d_name);
250 // failed to allocate name
255 evbuffer_add_printf(buf, "\"%s%s\":%lld", escaped_name,
256 S_ISDIR(file_stat_buf.st_mode) ? "/" : "",
257 ((long long)file_stat_buf.st_mtime) * 1000);
262 evbuffer_add(buf, "{", 1);
264 evbuffer_add(buf, "}\n", 2);
266 char *etag = make_etag(stat_buf);
268 log_error("make_etag() failed");
274 ADD_RESP_HEADER(request, "Content-Type", "application/json; charset=UTF-8");
275 ADD_RESP_HEADER_CP(request, "ETag", etag);
277 // FIXME: why do I need to use *_chunk_* here???
278 evhtp_send_reply_chunk_start(request, EVHTP_RES_OK);
279 evhtp_send_reply_chunk(request, buf);
280 evhtp_send_reply_chunk_end(request);
287 static char *get_xattr(const char *path, const char *key, int maxlen) {
289 char *value = malloc(32);
290 for(value = malloc(len);len<=maxlen;value = realloc(value, len+=16)) {
292 log_error("malloc() / realloc() failed: %s", strerror(errno));
295 int actual_len = getxattr(path, key, value, len);
296 log_debug("getxattr() for key %s returned actual length of %d bytes", key, actual_len);
300 if(errno == ERANGE) {
303 } else if(errno == ENOATTR) {
307 } else if(errno == ENOTSUP) {
308 // xattr not supported
309 log_error("File system doesn't support extended attributes! You may want to use another one.");
313 log_error("Unexpected error while getting %s attribute: %s", key, strerror(errno));
319 log_error("%s attribute seems to be longer than %d bytes. That is simply unreasonable.", key, maxlen);
324 static char *content_type_from_xattr(const char *path) {
325 char *mime_type = get_xattr(path, "user.mime_type", 128);
326 if(mime_type == NULL) {
329 char *charset = get_xattr(path, "user.charset", 64);
330 if(charset == NULL) {
333 int mt_len = strlen(mime_type);
334 char *content_type = realloc(mime_type, mt_len + strlen(charset) + 10 + 1);
335 if(content_type == NULL) {
336 log_error("realloc() failed: %s", strerror(errno));
340 log_debug("content_type now: %s (also charset is \"%s\")", content_type, charset);
341 sprintf(content_type + mt_len, "; charset=%s", charset);
346 static int content_type_to_xattr(int fd, const char *content_type) {
347 char *content_type_copy = strdup(content_type), *saveptr = NULL;
348 if(content_type_copy == NULL) {
349 log_error("strdup() failed: %s", strerror(errno));
352 char *mime_type = strtok_r(content_type_copy, ";", &saveptr);
353 log_debug("extracted mime type: %s", mime_type);
354 char *rest = strtok_r(NULL, "", &saveptr);
355 char *charset_begin = NULL, *charset = NULL;
357 charset_begin = strstr(rest, "charset=");
359 charset = charset_begin + 8;
360 log_debug("extracted charset: %s", charset);
363 if(charset == NULL) {
364 // FIXME: should this rather be binary or us-ascii?
366 log_debug("guessed charset: %s", charset);
368 if(fsetxattr(fd, "user.mime_type", mime_type, strlen(mime_type) + 1, 0) != 0) {
369 log_error("fsetxattr() failed: %s", strerror(errno));
370 free(content_type_copy);
373 log_debug("fsetxattr (mime_type) done");
374 if(fsetxattr(fd, "user.charset", mime_type, strlen(charset) + 1, 0) != 0) {
375 log_error("fsetxattr() failed: %s", strerror(errno));
376 free(content_type_copy);
379 log_debug("fsetxattr (charset) done");
380 free(content_type_copy);
385 static int serve_file_head(evhtp_request_t *request, char *disk_path, struct stat *stat_buf, const char *mime_type) {
386 log_debug("serve_file_head for path %s", disk_path);
387 int free_mime_type = 0;
388 // mime type is either passed in ... (such as for directory listings)
389 if(mime_type == NULL) {
390 // ... or detected based on xattr
391 mime_type = content_type_from_xattr(disk_path);
392 if(mime_type == NULL) {
393 // ... or guessed by libmagic
394 log_debug("mime type not given, detecting...");
395 mime_type = magic_file(magic_cookie, disk_path);
396 if(mime_type == NULL) {
397 // ... or defaulted to "application/octet-stream"
398 log_error("magic failed: %s", magic_error(magic_cookie));
399 mime_type = "application/octet-stream; charset=binary";
402 // xattr detected mime type and allocated memory for it
406 struct rs_header content_type_header = {
407 .key = "Content-Type",
408 // header won't be freed (it's completely stack based / constant),
409 // so cast is valid here to satisfy type check.
410 // (struct rs_header.value cannot be constant, because in the case of a request
411 // header it will be free()'d at some point)
412 .value = (char*)mime_type,
413 .next = &RS_DEFAULT_HEADERS
415 char *length_string = malloc(24);
416 if(length_string == NULL) {
417 log_error("malloc() failed: %s", strerror(errno));
420 snprintf(length_string, 24, "%ld", stat_buf->st_size);
421 char *etag_string = make_etag(stat_buf);
422 if(etag_string == NULL) {
423 log_error("make_etag() failed");
428 ADD_RESP_HEADER_CP(request, "Content-Length", length_string);
429 ADD_RESP_HEADER_CP(request, "ETag", etag_string);
431 log_debug("HAVE SET CONTENT LENGTH: %s", length_string);
433 evhtp_send_reply_chunk_start(request, 200);
438 free((char*)mime_type);
443 // serve a file body for the given request
444 static int serve_file(evhtp_request_t *request, const char *disk_path, struct stat *stat_buf) {
445 int fd = open(disk_path, O_RDONLY | O_NONBLOCK);
447 log_error("open() failed: %s", strerror(errno));
450 struct evbuffer *buf = evbuffer_new();
451 while(evbuffer_read(buf, fd, 4096) != 0) {
452 evhtp_send_reply_chunk(request, buf);
455 evhtp_send_reply_chunk_end(request);
459 static char *make_disk_path(char *user, char *path) {
461 // FIXME: use passwd->pwdir instead of /home/{user}/
463 // calculate maximum length of path
464 int pathlen = ( strlen(user) + strlen(path) +
467 RS_HOME_SERVE_ROOT_LEN );
468 char *disk_path = malloc(pathlen);
469 if(disk_path == NULL) {
470 log_error("malloc() failed: %s", strerror(errno));
473 // remove all /.. segments
474 // (we don't try to resolve them, but instead treat them as garbage)
475 char *traverse_pos = NULL;
476 while((traverse_pos = strstr(path, "/..")) != NULL) {
477 int restlen = strlen(traverse_pos + 3);
478 memmove(traverse_pos, traverse_pos + 3, restlen);
479 traverse_pos[restlen] = 0;
482 sprintf(disk_path, "/home/%s/%s%s", user, RS_HOME_SERVE_ROOT, path);
487 static evhtp_res handle_get_or_head(evhtp_request_t *request, int include_body) {
488 char *disk_path = make_disk_path(request->uri->path->match_start,
489 request->uri->path->match_end);
490 if(disk_path == NULL) {
494 log_debug("HANDLE GET OR HEAD, DISK PATH: %s", disk_path);
497 struct stat stat_buf;
498 if(stat(disk_path, &stat_buf) != 0) {
499 if(errno != ENOENT) {
500 log_error("stat() failed for path \"%s\": %s", disk_path, strerror(errno));
506 // check for directory
507 if(request->uri->path->file == NULL) {
508 // directory requested
509 if(! S_ISDIR(stat_buf.st_mode)) {
515 return serve_directory(request, disk_path, &stat_buf);
517 return serve_file_head(request, disk_path, &stat_buf, "application/json");
521 if(S_ISDIR(stat_buf.st_mode)) {
522 // found, but is a directory
526 int head_result = serve_file_head(request, disk_path, &stat_buf, NULL);
527 if(head_result != 0) {
531 return serve_file(request, disk_path, &stat_buf);