diff options
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | browse.c | 67 | ||||
| -rw-r--r-- | browse.h | 5 | ||||
| -rw-r--r-- | config.h | 2 | ||||
| -rw-r--r-- | create.c | 238 | ||||
| -rw-r--r-- | create.h | 54 | ||||
| -rw-r--r-- | delete.c | 77 | ||||
| -rw-r--r-- | delete.h | 2 | ||||
| -rw-r--r-- | download.c | 46 | ||||
| -rw-r--r-- | download.h | 2 | ||||
| -rw-r--r-- | http.c | 7 | ||||
| -rw-r--r-- | http.h | 8 | ||||
| -rw-r--r-- | main.c | 48 | ||||
| -rw-r--r-- | mime.c | 1 | ||||
| -rw-r--r-- | template/browse_head.html | 5 | ||||
| -rw-r--r-- | template/create_head.html | 17 | ||||
| -rw-r--r-- | template/head.html | 3 | ||||
| -rw-r--r-- | test.c | 224 | ||||
| -rw-r--r-- | upload.c | 66 | ||||
| -rw-r--r-- | upload.h | 2 |
21 files changed, 599 insertions, 288 deletions
@@ -47,10 +47,10 @@ all: main .c.o: $(CC) -o $@ -c $< $(CFLAGS) -main: str.o config.o mime.o url.o template.o file.o http.o delete.o upload.o download.o browse.o main.o +main: str.o config.o mime.o url.o template.o file.o http.o delete.o upload.o download.o create.o browse.o main.o $(CC) -o $@ $> $(LDFLAGS) -test: str.o config.o mime.o url.o template.o file.o http.o delete.o upload.o download.o browse.o test.o +test: str.o config.o mime.o url.o template.o file.o http.o delete.o upload.o download.o create.o browse.o test.o $(CC) -o $@ $> $(LDFLAGS) env: env.o @@ -68,6 +68,7 @@ install: install -D -o www -g www -m 0440 template/browse_item.html /var/www/usr/share/vault/template/browse_item.html install -D -o www -g www -m 0440 template/upload_head.html /var/www/usr/share/vault/template/upload_head.html install -D -o www -g www -m 0440 template/delete_head.html /var/www/usr/share/vault/template/delete_head.html + install -D -o www -g www -m 0440 template/create_head.html /var/www/usr/share/vault/template/create_head.html install -D -o www -g www -m 0440 fontawesome-6.5.1/css/fontawesome.css /var/www/vault-static/fontawesome-6.5.1/css/fontawesome.css gzip -fk /var/www/vault-static/fontawesome-6.5.1/css/fontawesome.css install -D -o www -g www -m 0440 fontawesome-6.5.1/css/solid.css /var/www/vault-static/fontawesome-6.5.1/css/solid.css @@ -1,8 +1,9 @@ # Vault +Note: This software is WIP. There may be bugs or unintended behavior. + Vault is an opinionated web-based file manager. It is built in C for OpenBSD around the CGI standard, following these main principles: -* __secure__: a great amount of time has been devoted to avoid undefined behaviour and security issues * __simple__: browse, create and delete folders; download and upload files. JavaScript support is optional on the client side and its absence should be handled gracefully. Everything should work from a terminal-based web browser * __fast__: with the server running on an ARM SBC, there should not be any noticeable delay when loading a page @@ -21,9 +22,8 @@ Vault comes with a Makefile: The vault binary will be installed as `/var/www/cgi-bin/vault`. Static resources will be installed in `/var/www/vault-static` and should be served from `/static`. You need to configure your web server accordingly, see below for a sample file. If the log file does not exist, you need to create it. Here is how to do it for a default installation: - # mkdir -p /var/www/var/log - # touch /var/www/var/log/vault.log - # chown www /var/www/var/log/vault.log + # touch /var/www/logs/vault.log + # chown www /var/www/logs/vault.log By default on OpenBSD, the `slowcgi(8)` daemon allows a timeout of 2 minutes for CGI programs. This might not be enough if you want to allow users to download large files. This timeout can be increased by changing the `slowcgi(8)` parameters in `/etc/rc.conf.local`, for example to allow up to 10 minutes : @@ -41,6 +41,7 @@ #include "browse.h" #include "cgi.h" #include "config.h" +#include "create.h" #include "download.h" #include "file.h" #include "http.h" @@ -164,7 +165,7 @@ struct template_data { struct file *f; }; -static const char *const header_template_keys[] = {"upload_url"}; +static const char *const header_template_keys[] = {"upload_url", "create_url"}; static int header_template_callback(size_t index, void *arg) @@ -185,7 +186,21 @@ header_template_callback(size_t index, void *arg) switch (index) { case 0: /* upload_url */ - K_OK(khtml_puts(html, data->f->action_url), r); + if (build_upload_url(r, data->f) > 0) { + K_OK(khtml_puts(html, data->f->action_url), r); + } else + kutil_warnx(r, NULL, + "failed to build upload URL for path %s", + data->f->path); + break; + case 1: + /* create_url */ + if (build_create_url(r, data->f) > 0) { + K_OK(khtml_puts(html, data->f->action_url), r); + } else + kutil_warnx(r, NULL, + "failed to build create URL for path %s", + data->f->path); break; default: kutil_warnx(r, NULL, @@ -241,7 +256,11 @@ file_template_callback(size_t index, void *arg) K_OK(khtml_puts(html, data->f->action_url), r); break; case 3: - /* size, print as human readable */ + /* size, only for files, print as human readable */ + if (data->f->is_dir) { + break; + } + file_size = data->f->size; i = 0; while (file_size > 1024 && i < 3) { @@ -275,12 +294,11 @@ file_template_callback(size_t index, void *arg) return 1; } -struct http_ret +void browse(struct kreq * r) { struct file *file; struct page_template *tmpl; - struct http_ret ret; struct khtmlreq html; struct ktemplate template; struct template_data data; @@ -289,53 +307,26 @@ browse(struct kreq * r) file = NULL; tmpl = NULL; - /* initialize return structure for success */ - ret = (struct http_ret) { - KHTTP_200, "" - }; - /* list requested directory content */ file = file_new(r->path); if (file == NULL) { - ret = (struct http_ret) { - KHTTP_404, - "browse: Unable to build data file" - }; + http_exit(r, KHTTP_404, "browse: Unable to build data file"); goto end; } if (!file->is_dir) { - ret = (struct http_ret) { - KHTTP_404, - "browse: Invalid data file" - }; - goto end; - } - if (build_upload_url(r, file) == 0) { - ret = (struct http_ret) { - KHTTP_500, - "browse: Can't build upload url" - }; + http_exit(r, KHTTP_404, "browse: Invalid data file"); goto end; } - if (build_file_list(r, file) < 0) { - ret = (struct http_ret) { - KHTTP_500, - "browse: Unable to build file list" - }; + http_exit(r, KHTTP_500, "browse: Unable to build file list"); goto end; } - /* read template */ tmpl = page_template_new(BROWSE_URL); if (tmpl == NULL) { - ret = (struct http_ret) { - KHTTP_500, - "browse: Unable to read template" - }; + http_exit(r, KHTTP_500, "browse: Unable to read template"); goto end; } - /* we have all the data we need, we can start to write output page */ http_open(r, KHTTP_200, KMIME_TEXT_HTML); @@ -345,7 +336,7 @@ browse(struct kreq * r) /* print browse header with action buttons for current dir */ template.key = header_template_keys; - template.keysz = 1; + template.keysz = 2; template.cb = header_template_callback; template.arg = &data; data.r = r; @@ -383,6 +374,4 @@ end: file_free(file); file = NULL; } - - return ret; } @@ -43,13 +43,12 @@ * All parameters are required. * Returns the length of the created URL, or 0 in case of failure. */ -size_t -build_browse_url(struct kreq *, struct file *); +size_t build_browse_url(struct kreq *, struct file *); /* * Print a browse page that allows to browse through the folder hierarchy. * The KCGI request is required. */ -struct http_ret browse(struct kreq *); +void browse(struct kreq *); #endif /* BROWSE_H */ @@ -36,7 +36,7 @@ * otherwise it will quit with an error. */ #ifndef LOG_FILE -#define LOG_FILE "/var/log/vault.log" +#define LOG_FILE "/logs/vault.log" #endif /* The directory that contains the HTML template files */ diff --git a/create.c b/create.c new file mode 100644 index 0000000..e5a98b0 --- /dev/null +++ b/create.c @@ -0,0 +1,238 @@ +/* + * Copyright 2025, Vincent Douillet <vincent@vdouillet.fr> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include <stdlib.h> +#include <kcgi.h> +#include <kcgihtml.h> +#include <limits.h> +#include <stdio.h> +#include <string.h> + +#include "browse.h" +#include "cgi.h" +#include "http.h" +#include "template.h" +#include "create.h" +#include "url.h" + +/* + * create url = r->pname / CREATE_URL / file->path + */ +size_t +build_create_url(const struct kreq * r, struct file * file) +{ + char action_url[PATH_MAX]; + size_t action_url_len; + + if (!file->is_dir) + return 0; + + action_url_len = url_build(action_url, PATH_MAX, r->pname, CREATE_URL, + file->path, NULL); + if (action_url_len == 0 || action_url_len >= PATH_MAX) { + kutil_warn(r, NULL, + "create: action URL overflow: %s", action_url); + return 0; + } + file->action_url = strndup(action_url, action_url_len); + if (file->action_url == NULL) { + kutil_warn(r, NULL, + "create: unable to allocate file url buffer: %s", + action_url); + return 0; + } + return action_url_len; +} + +/* + * data required in the template functions + */ +struct template_data { + struct kreq *r; + struct khtmlreq *html; + struct file *f; +}; + +static const char *const header_template_keys[] = {"path", "submit_url"}; + +static int +header_template_callback(size_t index, void *arg) +{ + struct kreq *r; + struct khtmlreq *html; + struct template_data *data; + + if (arg == NULL) { + kutil_warn(NULL, NULL, + "Invalid data for create header template"); + return 0; + } + data = arg; + r = data->r; + html = data->html; + + switch (index) { + case 0: + /* path */ + K_OK(khtml_puts(html, data->f->path), r); + break; + case 1: + /* submit_url */ + K_OK(khtml_puts(html, data->f->action_url), r); + break; + default: + kutil_warnx(r, NULL, + "Invalid key index for create header template: %zd", + index); + return 0; + } + + return 1; +} + +/* + * GET request, we print create form + */ +void +create_get(struct kreq * r, struct file * file) +{ + struct khtmlreq html; + struct ktemplate template; + struct page_template *tmpl; + struct template_data data; + + tmpl = NULL; + + /* action url is form submit url */ + if (build_create_url(r, file) == 0) { + http_exit(r, KHTTP_500, "create: Can't build create url"); + goto end; + } + /* read template */ + tmpl = page_template_new(CREATE_URL); + if (tmpl == NULL) { + http_exit(r, KHTTP_500, "create: Unable to read template"); + goto end; + } + /* print create form */ + http_open(r, KHTTP_200, KMIME_TEXT_HTML); + + K_OK(khttp_puts(r, tmpl->header), r); + K_OK(khtml_open(&html, r, 0), r); + + template.key = header_template_keys; + template.keysz = 2; + template.cb = header_template_callback; + template.arg = &data; + data.r = r; + data.html = &html; + data.f = file; + K_OK(khttp_template_buf(r, &template, tmpl->page_header, + strlen(tmpl->page_header)), r); + + K_OK(khttp_puts(r, tmpl->page_footer), r); + K_OK(khttp_puts(r, tmpl->footer), r); + + K_OK(khtml_close(&html), r); + +end: + page_template_free(tmpl); +} + +/* + * POST request, we handle create form + */ +void +create_post(struct kreq * r, struct file * f) +{ + char path[PATH_MAX]; + size_t path_len, i; + struct kpair *name; + + /* prepare action url for return redirection in case of success */ + if (build_browse_url(r, f) == 0) { + http_exit(r, KHTTP_500, "create: can't build return url"); + return; + } + /* validate input data */ + if (r->fieldsz != 1) { + http_exit(r, KHTTP_400, "create: missing folder name"); + return; + } + name = &(r->fields[0]); + /* + * TODO name->val == NULL is not enough, need better sanitization + * for example, some chars are not allowed in a folder name + * an empty non-NULL string is also not ok + */ + if (strncmp(name->key, "name", 4) || name->val == NULL) { + http_exit(r, KHTTP_400, "create: invalide folder name"); + return; + } + path_len = file_get_data_path(f, path, PATH_MAX, name->val); + if (path_len == 0 || path_len >= PATH_MAX) { + http_exit(r, KHTTP_500, + "create: can't build file path"); + return; + } + /* TODO check if folder already exists */ + if (mkdir(path, 0755)) { + http_exit(r, KHTTP_500, + "create: can't create directory"); + return; + } + + /* on success, redirect to the new folder */ + khttp_head(r, kresps[KRESP_LOCATION], "%s", f->action_url); + http_open(r, KHTTP_303, r->mime); +} + +void +create(struct kreq * r) +{ + struct file *file; + + file = NULL; + + /* build file corresponding to request (ie. create) path */ + file = file_new(r->path); + if (file == NULL) { + http_exit(r, KHTTP_404, "create: Unable to build data file"); + goto end; + } + /* print form or handle submission according to HTTP method */ + if (r->method == KMETHOD_POST) + create_post(r, file); + else + create_get(r, file); + +end: + file_free(file); +} diff --git a/create.h b/create.h new file mode 100644 index 0000000..1626a63 --- /dev/null +++ b/create.h @@ -0,0 +1,54 @@ +/* + * Copyright 2025, Vincent Douillet <vincent@vdouillet.fr> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef CREATE_H +#define CREATE_H + +#include <kcgi.h> + +#include "file.h" + +#define CREATE_URL "create" + +/* + * Build the URL to create a folder and assigns it to the file's action_url. + * All parameters are required + * Returns the length of the created URL, or 0 in case of failure. + */ +size_t +build_create_url(const struct kreq *, struct file *); + +/* + * Print a create form (http GET) or handle a create form submission (http + * POST). + */ +void create(struct kreq *); + +#endif /* CREATE_H */ @@ -122,7 +122,7 @@ header_template_callback(size_t index, void *arg) * GET request, we print delete form */ void -delete_get(struct kreq * r, struct http_ret * ret, struct file * file) +delete_get(struct kreq * r, struct file * file) { struct khtmlreq html; struct ktemplate template; @@ -133,19 +133,13 @@ delete_get(struct kreq * r, struct http_ret * ret, struct file * file) /* action url is form submit url */ if (build_delete_url(r, file) == 0) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: Can't build delete url" - }; + http_exit(r, KHTTP_500, "delete: Can't build delete url"); goto end; } /* read template */ tmpl = page_template_new(DELETE_URL); if (tmpl == NULL) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: Unable to read template" - }; + http_exit(r, KHTTP_500, "delete: Unable to read template"); goto end; } /* print delete form */ @@ -177,7 +171,7 @@ end: * POST request, we handle delete form */ void -delete_post(struct kreq * r, struct http_ret * ret, struct file * f) +delete_post(struct kreq * r, struct file * f) { char path[PATH_MAX]; char *paths[2]; @@ -193,32 +187,22 @@ delete_post(struct kreq * r, struct http_ret * ret, struct file * f) /* prepare action url for return redirection in case of success */ parent = file_get_parent(f); if (parent == NULL || build_browse_url(r, parent) == 0) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: can't build return url" - }; + http_exit(r, KHTTP_500, "delete: can't build return url"); goto end; } - /* build file path on disk */ if (file_get_data_path(f, path, sizeof(path), NULL) <= 0) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: can't build file data path" - }; + http_exit(r, KHTTP_500, "delete: can't build file data path"); goto end; } - if (f->is_dir) { /* don't follow symlinks */ fts_opts = 0x0 | FTS_PHYSICAL; paths[0] = path; paths[1] = NULL; if ((fts = fts_open(paths, fts_opts, NULL)) == NULL) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: can't open file hierarchy" - }; + http_exit(r, KHTTP_500, + "delete: can't open file hierarchy"); goto end; } /* file and directory delete loop */ @@ -227,28 +211,22 @@ delete_post(struct kreq * r, struct http_ret * ret, struct file * f) /* halt on fts errors */ if (fi == FTS_DNR || fi == FTS_ERR || fi == FTS_NS) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: fts_read error" - }; + http_exit(r, KHTTP_500, + "delete: fts_read error"); goto end; } /* delete regular files and empty folders */ else if ((fi == FTS_DP || fi == FTS_F) && remove(ftsent->fts_path) < 0) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: failed to delete file" - }; + http_exit(r, KHTTP_500, + "delete: failed to delete file"); goto end; } } } else { if (remove(path) < 0) { - *ret = (struct http_ret) { - KHTTP_500, - "delete: failed to delete file" - }; + http_exit(r, KHTTP_500, + "delete: failed to delete file"); goto end; } } @@ -257,10 +235,6 @@ delete_post(struct kreq * r, struct http_ret * ret, struct file * f) khttp_head(r, kresps[KRESP_LOCATION], "%s", parent->action_url); http_open(r, KHTTP_303, r->mime); - *ret = (struct http_ret) { - KHTTP_303, - "" - }; end: if (parent != NULL) file_free(parent); @@ -268,28 +242,21 @@ end: fts_close(fts); } - -struct http_ret +void del(struct kreq * r) { char *path; size_t suffix_len; struct file *file; - struct http_ret ret; file = NULL; - ret = (struct http_ret) { - KHTTP_200, "" - }; /* build file corresponding to request (ie. delete) path */ suffix_len = strlen(r->suffix); if (suffix_len > 0) { if (str_concat(&path, r->path, ".", r->suffix, NULL) <= 0) { - ret = (struct http_ret) { - KHTTP_404, - "delete: Unable to build file path" - }; + http_exit(r, KHTTP_404, + "delete: Unable to build file path"); goto end; } } else { @@ -298,21 +265,17 @@ del(struct kreq * r) file = file_new(path); if (file == NULL) { - ret = (struct http_ret) { - KHTTP_404, - "delete: Unable to build data file" - }; + http_exit(r, KHTTP_404, "delete: Unable to build data file"); goto end; } /* print form or handle submission according to HTTP method */ if (r->method == KMETHOD_POST) - delete_post(r, &ret, file); + delete_post(r, file); else - delete_get(r, &ret, file); + delete_get(r, file); end: if (suffix_len > 0) free(path); file_free(file); - return ret; } @@ -49,6 +49,6 @@ build_delete_url(const struct kreq *, struct file *); * Print a delete form (http GET) or handle a delete form submission (http * POST). */ -struct http_ret del(struct kreq *); +void del(struct kreq *); #endif /* DELETE_H */ @@ -76,7 +76,7 @@ build_download_url(const struct kreq * r, struct file * file) return action_url_len; } -struct http_ret +void download(struct kreq * r) { void *buffer; @@ -85,22 +85,16 @@ download(struct kreq * r) char file_path[PATH_MAX]; char *path; size_t path_size, suffix_len; - struct http_ret ret; - /* initialize empty file and return struct to success */ + /* initialize empty file to success */ f = NULL; - ret = (struct http_ret) { - KHTTP_200, "" - }; /* build requested file path, with suffix or without */ suffix_len = strlen(r->suffix); if (suffix_len > 0) { if (str_concat(&path, r->path, ".", r->suffix, NULL) <= 0) { - ret = (struct http_ret) { - KHTTP_414, - "download: unable to build requested file path" - }; + http_exit(r, KHTTP_414, + "download: unable to build requested file path"); goto end; } } else { @@ -109,54 +103,37 @@ download(struct kreq * r) /* build file metadata */ f = file_new(path); if (f == NULL) { - ret = (struct http_ret) { - KHTTP_404, - "download: file metadata failure" - }; + http_exit(r, KHTTP_404, "download: file metadata failure"); goto end; } /* we do not support downloading folders */ if (f->is_dir) { - ret = (struct http_ret) { - KHTTP_400, - "download: can't download folder" - }; + http_exit(r, KHTTP_400, "download: can't download folder"); goto end; } /* memory map the file */ path_size = file_get_data_path(f, file_path, PATH_MAX, NULL); if (path_size == 0 || path_size >= PATH_MAX) { - ret = (struct http_ret) { - KHTTP_404, - "download: unable to build file path" - }; + http_exit(r, KHTTP_404, "download: unable to build file path"); goto end; } fd = open(file_path, O_RDONLY); if (fd < 0) { - ret = (struct http_ret) { - KHTTP_404, - "download: unable to open file" - }; + http_exit(r, KHTTP_404, "download: unable to open file"); goto end; } /* mmap does not work with empty file: st_size = 0 */ if (f->size > 0) { buffer = mmap(NULL, f->size, PROT_READ, MAP_PRIVATE, fd, 0); if (buffer == MAP_FAILED) { - ret = (struct http_ret) { - KHTTP_500, - "download: mmap failed" - }; + http_exit(r, KHTTP_500, "download: mmap failed"); goto end; } } /* write the file */ if (!http_open_file(r, KHTTP_200, f)) { - ret = (struct http_ret) { - KHTTP_500, - "download: filename url encoding failed" - }; + http_exit(r, KHTTP_500, + "download: filename url encoding failed"); goto end; } if (f->size > 0) @@ -169,5 +146,4 @@ end: if (f != NULL && f->size > 0) munmap(buffer, f->size); file_free(f); - return ret; } @@ -49,6 +49,6 @@ build_download_url(const struct kreq *, struct file *); * Return a requested file. * The KCGI request is required. */ -struct http_ret download(struct kreq *); +void download(struct kreq *); #endif /* DOWNLOAD_H */ @@ -53,7 +53,12 @@ http_open_file(struct kreq * r, enum khttp code, const struct file * f) return false; khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[code]); - khttp_head(r, kresps[KRESP_CONTENT_DISPOSITION], + + /* no content-disposition for pictures & videos (open them in browser) */ + if (f->mime != MIME_GIF && f->mime != MIME_JPG && f->mime != MIME_MP4 && + f->mime != MIME_PNG && f->mime != MIME_SVG && f->mime != MIME_WEBM && + f->mime != MIME_WEBP) + khttp_head(r, kresps[KRESP_CONTENT_DISPOSITION], "attachment;filename=\"%s\"", filename); free(filename); khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", mime_str(f->mime)); @@ -36,14 +36,6 @@ #include "file.h" /* - * Return structure for HTTP action methods - */ -struct http_ret { - enum khttp code; - char *message; -}; - -/* * Initialize headers and start the document body with the provided http code. * All parameters are required. */ @@ -36,6 +36,7 @@ #include "browse.h" #include "cgi.h" #include "config.h" +#include "create.h" #include "delete.h" #include "download.h" #include "http.h" @@ -46,6 +47,7 @@ enum page { PAGE_DOWNLOAD, PAGE_UPLOAD, PAGE_DELETE, + PAGE_CREATE, PAGE__MAX }; @@ -54,6 +56,7 @@ static const char *const pages[PAGE__MAX] = { DOWNLOAD_URL, UPLOAD_URL, DELETE_URL, + CREATE_URL, }; int @@ -62,60 +65,75 @@ main(void) char *data_dir; enum kcgi_err parse_err; struct kreq r; - struct http_ret ret; - if (kutil_openlog(LOG_FILE) == 0) + if (kutil_openlog(LOG_FILE) == 0) { http_exit(NULL, KHTTP_500, "Unable to open %s", LOG_FILE); + return EXIT_SUCCESS; + } parse_err = khttp_parse(&r, NULL, 0, pages, PAGE__MAX, PAGE_BROWSE); - if (parse_err != KCGI_OK) + if (parse_err != KCGI_OK) { http_exit(NULL, KHTTP_500, "Unable to parse request: %s", kcgi_strerror(parse_err)); + return EXIT_SUCCESS; + } data_dir = config_data_dir(); - if (data_dir == NULL) + if (data_dir == NULL) { http_exit(NULL, KHTTP_500, "Data dir not configured"); + goto end; + } /* A bit of security cannot hurt */ if (-1 == unveil(data_dir, "rwc") || -1 == unveil(TEMPLATE_DIR, "r") - || -1 == unveil(NULL, NULL)) + || -1 == unveil(NULL, NULL)) { http_exit(&r, KHTTP_500, "Unveil failed: %s", strerror(errno)); - if (-1 == pledge("stdio rpath wpath cpath", NULL)) + goto end; + } + if (-1 == pledge("stdio rpath wpath cpath", NULL)) { http_exit(&r, KHTTP_500, "Pledge failed: %s", strerror(errno)); + goto end; + } /* * Make sure basic request parameters are as expected : GET or POST, * valid page and HTML document */ - if (r.method != KMETHOD_GET && r.method != KMETHOD_POST) + if (r.method != KMETHOD_GET && r.method != KMETHOD_POST) { http_exit(&r, KHTTP_405, NULL); - if (r.page == PAGE__MAX) + goto end; + } + if (r.page == PAGE__MAX) { http_exit(&r, KHTTP_404, NULL); + goto end; + } switch (r.page) { case PAGE_BROWSE: if (r.mime != KMIME_TEXT_HTML) http_exit(&r, KHTTP_406, NULL); /* Not Acceptable */ - ret = browse(&r); + browse(&r); break; case PAGE_DOWNLOAD: - ret = download(&r); + download(&r); break; case PAGE_UPLOAD: - ret = upload(&r); + upload(&r); break; case PAGE_DELETE: - ret = del(&r); + del(&r); + break; + case PAGE_CREATE: + create(&r); break; default: http_exit(&r, KHTTP_404, NULL); + goto end; } +end: khttp_free(&r); - if (ret.code >= KHTTP_400) - http_exit(&r, ret.code, ret.message); - return EXIT_SUCCESS; } @@ -132,7 +132,6 @@ mime_from_ext(const char *ext) return MIME_BIN; } - const char * mime_str(enum mime mime) { diff --git a/template/browse_head.html b/template/browse_head.html index 6775012..8b53948 100644 --- a/template/browse_head.html +++ b/template/browse_head.html @@ -1,5 +1,8 @@ <h1><i class="fa-regular fa-folder-open"></i> Browsing</h1> -<a href="@@upload_url@@">Upload here</a> +<ul id="action-list"> + <li><a href="@@upload_url@@">Upload here</a></li> + <li><a href="@@create_url@@">Create folder here</a></li> +</ul> <hr /> <table id="file-list"> <thead> diff --git a/template/create_head.html b/template/create_head.html new file mode 100644 index 0000000..5b273ce --- /dev/null +++ b/template/create_head.html @@ -0,0 +1,17 @@ +<h1><i class="fa-regular fa-folder-open"></i> New folder</h1> +<p>in</p> +<pre>@@path@@</pre> +<hr /> +<div> +<form action="@@submit_url@@" method="post" enctype="multipart/form-data"> + <div> + <label for="name">Folder name:</label> + </div> + <div> + <input type="text" name="name" id="name" /> + </div> + <div> + <input type="submit" value="Create" /> + </div> +</form> +</div> diff --git a/template/head.html b/template/head.html index a36c475..85f28e3 100644 --- a/template/head.html +++ b/template/head.html @@ -15,6 +15,9 @@ body { font:1.2em/1.62 sans-serif; } a:visited { color:blue; } +ul#action-list { padding:0; } +ul#action-list li { display:inline; } +ul#action-list li:not(:last-child):after { content:' |'; } table#file-list { width:100%; border-collapse:collapse; @@ -1,4 +1,5 @@ #include <stdio.h> +#include <stdlib.h> #include <string.h> #include "browse.h" @@ -18,41 +19,163 @@ int tests_run; static char * -test_browse_invalid_traversal() +test_build_browse_url() { + size_t s; + struct file f; struct kreq r; - struct http_ret ret; - /* attempt to traverse out of DATA_DIR... */ + /* nominal case */ r = (struct kreq) { - .path = "..", + .pname = "vault", + .path = "Alice/Bob", .suffix = "", - .mime = KMIME_TEXT_HTML }; - ret = browse(&r); + f = (struct file) { + .is_dir = true, + .path = "Alice/Bob", + .action_url = NULL + }; + s = build_browse_url(&r, &f); + mu_assert("failed to build browse url", s > 0 && f.action_url != NULL); + + /* refuse to browse files */ + if (f.action_url != NULL) + free(f.action_url); + f.action_url = NULL; + f.is_dir = false; + s = build_browse_url(&r, &f); + mu_assert("allowed to browse a file", s <= 0 && f.action_url == NULL); + + return 0; +} + +static char * +test_build_download_url() +{ + size_t s; + struct file f; + struct kreq r; + + /* nominal case */ + r = (struct kreq) { + .pname = "vault", + .path = "Alice/Bob", + .suffix = "txt", + }; + f = (struct file) { + .is_dir = false, + .path = "Alice/Bob.txt", + .action_url = NULL + }; + s = build_download_url(&r, &f); + mu_assert("failed to build download url", + s > 0 && f.action_url != NULL); + + /* refuse to download directories */ + if (f.action_url != NULL) + free(f.action_url); + f.action_url = NULL; + f.is_dir = true; + s = build_download_url(&r, &f); + mu_assert("allowed to download a directory", + s <= 0 && f.action_url == NULL); + + return 0; +} + +static char * +test_build_upload_url() +{ + size_t s; + struct file f; + struct kreq r; + + /* nominal case */ + r = (struct kreq) { + .pname = "vault", + .path = "Alice/Bob", + .suffix = "", + }; + f = (struct file) { + .is_dir = true, + .path = "Alice/Bob", + .action_url = NULL + }; + s = build_upload_url(&r, &f); + mu_assert("failed to build upload url", s > 0 && f.action_url != NULL); + + /* refuse to upload to a file */ + if (f.action_url != NULL) + free(f.action_url); + f.action_url = NULL; + f.is_dir = false; + s = build_upload_url(&r, &f); + mu_assert("allowed to upload to a file", + s <= 0 && f.action_url == NULL); - /* ...should return an error */ - mu_assert("error, browse allowed invalid traversal!", - ret.code >= KHTTP_400); return 0; } static char * -test_browse_path_too_long() +test_build_delete_url() { + size_t s; + struct file f; struct kreq r; - struct http_ret ret; - /* a lengthy path should cause URL overflow... */ + /* nominal case */ r = (struct kreq) { - .path = "this/is/a/very/very/lengthy/path/that/should/overflow/the/maximum/allowed/path/length/of/PATH_MAX/well/of/course/this/limit/is/platform/dependent", + .pname = "vault", + .path = "Alice/Bob", .suffix = "", - .mime = KMIME_TEXT_HTML }; - ret = browse(&r); + f = (struct file) { + .is_dir = true, + .path = "Alice/Bob", + .action_url = NULL + }; + s = build_delete_url(&r, &f); + mu_assert("failed to build delete url", s > 0 && f.action_url != NULL); + + /* allow also to delete a file */ + if (f.action_url != NULL) + free(f.action_url); + f.action_url = NULL; + f.is_dir = false; + s = build_delete_url(&r, &f); + mu_assert("refused to delete a file", s > 0 && f.action_url != NULL); - mu_assert("error, browse allowed invalid traversal!", - ret.code >= KHTTP_400); + return 0; +} + +static char * +test_url_build() +{ + char dst[BUF_SZ], *expected; + size_t dst_len; + + expected = "/vault/browse"; + dst_len = url_build(dst, BUF_SZ, "/vault/browse", "", NULL); + mu_assert("test_url_build failed", dst_len > 0 && dst_len < BUF_SZ); + mu_assert("test_url_build failed", strncmp(expected, dst, BUF_SZ) == 0); + + expected = "/vault/browse/hello/world"; + dst_len = url_build(dst, BUF_SZ, "/vault/browse", "hello", "world", + NULL); + mu_assert("test_url_build failed", dst_len > 0 && dst_len < BUF_SZ); + mu_assert("test_url_build failed", strncmp(expected, dst, BUF_SZ) == 0); + + expected = "/vault/browse"; + dst_len = url_build(dst, BUF_SZ, "/vault/", "browse/", NULL); + mu_assert("test_url_build failed", dst_len > 0 && dst_len < BUF_SZ); + mu_assert("test_url_build failed", strncmp(expected, dst, BUF_SZ) == 0); + + dst_len = url_build(dst, BUF_SZ, "", "", NULL); + mu_assert("test_url_build failed", dst_len == 0); + + dst_len = url_build(dst, BUF_SZ, NULL); + mu_assert("test_url_build failed", dst_len == 0); return 0; } @@ -61,7 +184,6 @@ test_upload_post() { char *file_content; struct kreq r; - struct http_ret ret; struct kpair fields[2]; file_content = "text file"; @@ -86,9 +208,7 @@ test_upload_post() .fieldsz = 2, .fields = fields }; - ret = upload(&r); - - mu_assert("error, POST upload failed!", ret.code <= KHTTP_400); + upload(&r); return 0; } @@ -96,16 +216,12 @@ static char * test_download() { struct kreq r; - struct http_ret ret; - r = (struct kreq) { .pname = "/vault", .path = "a", .suffix = "txt", }; - ret = download(&r); - - mu_assert("error, download failed!", ret.code <= KHTTP_400); + download(&r); return 0; } @@ -113,17 +229,13 @@ static char * test_delete_get() { struct kreq r; - struct http_ret ret; - r = (struct kreq) { .pname = "/vault", .path = "a", .suffix = "txt", .method = KMETHOD_GET, }; - ret = del(&r); - - mu_assert("error, GET delete failed!", ret.code <= KHTTP_400); + del(&r); return 0; } @@ -131,60 +243,34 @@ static char * test_delete_post() { struct kreq r; - struct http_ret ret; - r = (struct kreq) { .pname = "/vault", .path = "Folder", .suffix = "", .method = KMETHOD_POST, }; - ret = del(&r); - - mu_assert("error, POST delete failed!", ret.code <= KHTTP_400); - return 0; -} - -static char * -test_url_build() -{ - char dst[BUF_SZ], *expected; - size_t dst_len; - - expected = "/vault/browse"; - dst_len = url_build(dst, BUF_SZ, "/vault/browse", "", NULL); - mu_assert("test_url_build failed", dst_len > 0 && dst_len < BUF_SZ); - mu_assert("test_url_build failed", strncmp(expected, dst, BUF_SZ) == 0); - - expected = "/vault/browse/hello/world"; - dst_len = url_build(dst, BUF_SZ, "/vault/browse", "hello", "world", - NULL); - mu_assert("test_url_build failed", dst_len > 0 && dst_len < BUF_SZ); - mu_assert("test_url_build failed", strncmp(expected, dst, BUF_SZ) == 0); - - expected = "/vault/browse"; - dst_len = url_build(dst, BUF_SZ, "/vault/", "browse/", NULL); - mu_assert("test_url_build failed", dst_len > 0 && dst_len < BUF_SZ); - mu_assert("test_url_build failed", strncmp(expected, dst, BUF_SZ) == 0); - - dst_len = url_build(dst, BUF_SZ, "", "", NULL); - mu_assert("test_url_build failed", dst_len == 0); - - dst_len = url_build(dst, BUF_SZ, NULL); - mu_assert("test_url_build failed", dst_len == 0); + del(&r); return 0; } static char * all_tests() { - mu_run_test(test_browse_invalid_traversal); - mu_run_test(test_browse_path_too_long); - mu_run_test(test_url_build); + /* the following tests are for debug only */ + /* mu_run_test(test_download); mu_run_test(test_upload_post); mu_run_test(test_delete_get); mu_run_test(test_delete_post); + */ + + /* url building */ + mu_run_test(test_url_build); + mu_run_test(test_build_browse_url); + mu_run_test(test_build_download_url); + mu_run_test(test_build_upload_url); + mu_run_test(test_build_delete_url); + return 0; } @@ -121,7 +121,7 @@ header_template_callback(size_t index, void *arg) * GET request, we print upload form */ void -upload_get(struct kreq * r, struct http_ret * ret, struct file * file) +upload_get(struct kreq * r, struct file * file) { struct khtmlreq html; struct ktemplate template; @@ -132,19 +132,13 @@ upload_get(struct kreq * r, struct http_ret * ret, struct file * file) /* action url is form submit url */ if (build_upload_url(r, file) == 0) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: Can't build upload url" - }; + http_exit(r, KHTTP_500, "upload: Can't build upload url"); goto end; } /* read template */ tmpl = page_template_new(UPLOAD_URL); if (tmpl == NULL) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: Unable to read template" - }; + http_exit(r, KHTTP_500, "upload: Unable to read template"); goto end; } /* print upload form */ @@ -176,7 +170,7 @@ end: * POST request, we handle upload form */ void -upload_post(struct kreq * r, struct http_ret * ret, struct file * f) +upload_post(struct kreq * r, struct file * f) { char path[PATH_MAX]; FILE *to_write; @@ -184,13 +178,9 @@ upload_post(struct kreq * r, struct http_ret * ret, struct file * f) /* prepare action url for return redirection in case of success */ if (build_browse_url(r, f) == 0) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: can't build return url" - }; + http_exit(r, KHTTP_500, "upload: can't build return url"); return; } - /* * data should have been validated prior to handling the request, so * here we should have an array of fields named "file", each field @@ -200,33 +190,25 @@ upload_post(struct kreq * r, struct http_ret * ret, struct file * f) struct kpair *upl = &(r->fields[i]); path_len = file_get_data_path(f, path, PATH_MAX, upl->file); if (path_len == 0 || path_len >= PATH_MAX) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: can't build file path" - }; + http_exit(r, KHTTP_500, + "upload: can't build file path"); return; } to_write = fopen(path, "wb"); if (to_write == NULL) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: can't open file for writing" - }; + http_exit(r, KHTTP_500, + "upload: can't open file for writing"); return; } if (fwrite(upl->val, sizeof(char), upl->valsz, to_write) != upl->valsz) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: error while writing file" - }; + http_exit(r, KHTTP_500, + "upload: error while writing file"); return; } if (fclose(to_write) != 0) { - *ret = (struct http_ret) { - KHTTP_500, - "upload: error while closing file" - }; + http_exit(r, KHTTP_500, + "upload: error while closing file"); return; } } @@ -234,41 +216,27 @@ upload_post(struct kreq * r, struct http_ret * ret, struct file * f) /* on success, redirect to the directory where new files were created */ khttp_head(r, kresps[KRESP_LOCATION], "%s", f->action_url); http_open(r, KHTTP_303, r->mime); - - *ret = (struct http_ret) { - KHTTP_303, - "" - }; } - -struct http_ret +void upload(struct kreq * r) { struct file *file; - struct http_ret ret; file = NULL; - ret = (struct http_ret) { - KHTTP_200, "" - }; /* build file corresponding to request (ie. upload) path */ file = file_new(r->path); if (file == NULL) { - ret = (struct http_ret) { - KHTTP_404, - "upload: Unable to build data file" - }; + http_exit(r, KHTTP_404, "upload: Unable to build data file"); goto end; } /* print form or handle submission according to HTTP method */ if (r->method == KMETHOD_POST) - upload_post(r, &ret, file); + upload_post(r, file); else - upload_get(r, &ret, file); + upload_get(r, file); end: file_free(file); - return ret; } @@ -49,6 +49,6 @@ build_upload_url(const struct kreq *, struct file *); * Print an upload form (http GET) or handle an upload form submission (http * POST). */ -struct http_ret upload(struct kreq *); +void upload(struct kreq *); #endif /* UPLOAD_H */ |
