diff --git a/CMakeLists.txt b/CMakeLists.txt index de2c9ac..2578291 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(${PROJECT_NAME} server.c ) target_compile_options(${PROJECT_NAME} PRIVATE -Wall) +target_compile_definitions(${PROJECT_NAME} PRIVATE _FILE_OFFSET_BITS=64) add_subdirectory(dynstr) find_package(cJSON 1.0 REQUIRED) find_package(OpenSSL 3.0 REQUIRED) diff --git a/Makefile b/Makefile index e29c8ac..fc00b3a 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,8 @@ CC = cc # c99 (default value) does not allow POSIX extensions. PROJECT = slcl O = -Og -CFLAGS = $(O) -g -Wall -Idynstr/include -MD -MF - +CDEFS = -D_FILE_OFFSET_BITS=64 # Required for large file support on 32-bit. +CFLAGS = $(O) $(CDEFS) -g -Wall -Idynstr/include -MD -MF - LIBS = -lcjson -lssl -lm -lcrypto LDFLAGS = $(LIBS) DEPS = $(OBJECTS:.o=.d) diff --git a/README.md b/README.md index 03309de..241dd8b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ simplicity and efficiency. ## Features -- Private access directory with file uploading. +- Private access directory with file uploading, with configurable quota. - Read-only public file sharing. - Its own, tiny HTTP/1.0 and 1.1-compatible server. - A simple JSON file as the credentials database. @@ -128,15 +128,17 @@ schema: "name": "...", "password": "...", "salt": "...", - "key": "..." + "key": "...", + "quota": "..." }] } ``` -[`usergen`](usergen) is an interactive script that consumes a username and -password, and writes a JSON object that can be appended to the `users` JSON -array in `db.json`. A salt is randomly generated using `openssl` and passwords -are hashed multiple times beforehand - see [`usergen`](usergen) and +[`usergen`](usergen) is an interactive script that consumes a username, a +password and, optionally, a user quota in MiB. Then, [`usergen`](usergen) +writes a JSON object that can be appended to the `users` JSON array in +`db.json`. A salt is randomly generated using `openssl` and passwords are +hashed multiple times beforehand - see [`usergen`](usergen) and [`auth.c`](/auth.c) for further reference. Also, a random key is generated that is later used to sign HTTP cookies. diff --git a/auth.c b/auth.c index 630bb0a..b3ed872 100644 --- a/auth.c +++ b/auth.c @@ -372,6 +372,97 @@ const char *auth_dir(const struct auth *const a) return a->dir.str; } +int auth_quota(const struct auth *const a, const char *const user, + bool *const available, unsigned long long *const quota) +{ + int ret = -1; + const char *const path = a->db.str; + char *const db = dump_db(path); + cJSON *json = NULL; + + if (!db) + { + fprintf(stderr, "%s: dump_db failed\n", __func__); + goto end; + } + else if (!(json = cJSON_Parse(db))) + { + fprintf(stderr, "%s: cJSON_Parse failed\n", __func__); + goto end; + } + + const cJSON *const users = cJSON_GetObjectItem(json, "users"); + + if (!users) + { + fprintf(stderr, "%s: could not find users\n", __func__); + goto end; + } + else if (!cJSON_IsArray(users)) + { + fprintf(stderr, "%s: expected JSON array for users\n", __func__); + goto end; + } + + *available = false; + + const cJSON *u; + + cJSON_ArrayForEach(u, users) + { + const cJSON *const n = cJSON_GetObjectItem(u, "name"), + *const q = cJSON_GetObjectItem(u, "quota"); + const char *name; + + if (!n || !(name = cJSON_GetStringValue(n))) + { + fprintf(stderr, "%s: missing username\n", __func__); + goto end; + } + else if (!strcmp(name, user)) + { + const char *qs; + + if (!q || !(qs = cJSON_GetStringValue(q)) || !*qs) + { + /* Unlimited quota. */ + ret = 0; + goto end; + } + + char *end; + + errno = 0; + *available = true; + *quota = strtoull(qs, &end, 10); + + const unsigned long long mul = 1024 * 1024; + + if (errno || *end != '\0') + { + fprintf(stderr, "%s: invalid quota %s: %s\n", + __func__, qs, strerror(errno)); + goto end; + } + else if (*quota >= ULLONG_MAX / mul) + { + fprintf(stderr, "%s: quota %s too large\n", __func__, qs); + goto end; + } + + *quota *= 1024 * 1024; + break; + } + } + + ret = 0; + +end: + free(db); + cJSON_Delete(json); + return ret; +} + static int create_db(const char *const path) { int ret = -1; diff --git a/auth.h b/auth.h index a4145aa..ad46585 100644 --- a/auth.h +++ b/auth.h @@ -2,6 +2,7 @@ #define AUTH_H #include "http.h" +#include struct auth *auth_alloc(const char *dir); void auth_free(struct auth *a); @@ -9,5 +10,7 @@ int auth_cookie(const struct auth *a, const struct http_cookie *c); int auth_login(const struct auth *a, const char *user, const char *password, char **cookie); const char *auth_dir(const struct auth *a); +int auth_quota(const struct auth *a, const char *user, bool *available, + unsigned long long *quota); #endif /* AUTH_H */ diff --git a/doc/man1/slcl.1 b/doc/man1/slcl.1 index e48d800..174d18e 100644 --- a/doc/man1/slcl.1 +++ b/doc/man1/slcl.1 @@ -68,7 +68,8 @@ The following schema is expected: "name": "...", "password": "...", "salt": "...", - "key": "..." + "key": "...", + "quota": "..." }] } .EE diff --git a/handler.c b/handler.c index 6a3d626..05650ca 100644 --- a/handler.c +++ b/handler.c @@ -10,15 +10,14 @@ struct handler { - const char *tmpdir; - - struct handler_cfg + struct handler_cfg cfg; + struct elem { char *url; enum http_op op; handler_fn f; void *user; - } *cfg; + } *elem; struct server *server; struct client @@ -96,10 +95,10 @@ static int on_payload(const struct http_payload *const p, for (size_t i = 0; i < h->n_cfg; i++) { - const struct handler_cfg *const cfg = &h->cfg[i]; + const struct elem *const e = &h->elem[i]; - if (cfg->op == p->op && !wildcard_cmp(p->resource, cfg->url)) - return cfg->f(p, r, cfg->user); + if (e->op == p->op && !wildcard_cmp(p->resource, e->url)) + return e->f(p, r, e->user); } fprintf(stderr, "Not found: %s\n", p->resource); @@ -112,6 +111,18 @@ static int on_payload(const struct http_payload *const p, return 0; } +static int on_length(const unsigned long long len, + const struct http_cookie *const c, void *const user) +{ + struct client *const cl = user; + struct handler *const h = cl->h; + + if (h->cfg.length) + return h->cfg.length(len, c, h->cfg.user); + + return 0; +} + static struct client *find_or_alloc_client(struct handler *const h, struct server_client *const c) { @@ -134,8 +145,9 @@ static struct client *find_or_alloc_client(struct handler *const h, .read = on_read, .write = on_write, .payload = on_payload, + .length = on_length, .user = ret, - .tmpdir = h->tmpdir + .tmpdir = h->cfg.tmpdir }; *ret = (const struct client) @@ -282,9 +294,9 @@ void handler_free(struct handler *const h) if (h) { for (size_t i = 0; i < h->n_cfg; i++) - free(h->cfg[i].url); + free(h->elem[i].url); - free(h->cfg); + free(h->elem); free_clients(h); server_close(h->server); } @@ -292,7 +304,7 @@ void handler_free(struct handler *const h) free(h); } -struct handler *handler_alloc(const char *const tmpdir) +struct handler *handler_alloc(const struct handler_cfg *const cfg) { struct handler *const h = malloc(sizeof *h); @@ -303,7 +315,7 @@ struct handler *handler_alloc(const char *const tmpdir) return NULL; } - *h = (const struct handler){.tmpdir = tmpdir}; + *h = (const struct handler){.cfg = *cfg}; return h; } @@ -311,17 +323,17 @@ int handler_add(struct handler *const h, const char *url, const enum http_op op, const handler_fn f, void *const user) { const size_t n = h->n_cfg + 1; - struct handler_cfg *const cfgs = realloc(h->cfg, n * sizeof *h->cfg); + struct elem *const elem = realloc(h->elem, n * sizeof *h->elem); - if (!cfgs) + if (!elem) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } - struct handler_cfg *const c = &cfgs[h->n_cfg]; + struct elem *const e = &elem[h->n_cfg]; - *c = (const struct handler_cfg) + *e = (const struct elem) { .url = strdup(url), .op = op, @@ -329,13 +341,13 @@ int handler_add(struct handler *const h, const char *url, .user = user }; - if (!c->url) + if (!e->url) { fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno)); return -1; } - h->cfg = cfgs; + h->elem = elem; h->n_cfg = n; return 0; } diff --git a/handler.h b/handler.h index 69f8cbf..d851f65 100644 --- a/handler.h +++ b/handler.h @@ -7,7 +7,15 @@ typedef int (*handler_fn)(const struct http_payload *p, struct http_response *r, void *user); -struct handler *handler_alloc(const char *tmpdir); +struct handler_cfg +{ + const char *tmpdir; + int (*length)(unsigned long long len, const struct http_cookie *c, + void *user); + void *user; +}; + +struct handler *handler_alloc(const struct handler_cfg *cfg); void handler_free(struct handler *h); int handler_add(struct handler *h, const char *url, enum http_op op, handler_fn f, void *user); diff --git a/http.c b/http.c index 5c4626b..a06f29d 100644 --- a/http.c +++ b/http.c @@ -797,6 +797,18 @@ static int process_header(struct http_ctx *const h, const char *const line, return 0; } +static int check_length(struct http_ctx *const h) +{ + struct ctx *const c = &h->ctx; + const struct http_cookie cookie = + { + .field = c->field, + .value = c->value + }; + + return h->cfg.length(c->post.len, &cookie, h->cfg.user); +} + static int header_cr_line(struct http_ctx *const h) { const char *const line = (const char *)h->line; @@ -810,11 +822,20 @@ static int header_cr_line(struct http_ctx *const h) return payload_get(h, line); case HTTP_OP_POST: + { if (!c->post.len) return payload_post(h, line); + else if (c->boundary) + { + const int res = check_length(h); + + if (res) + return res; + } c->state = BODY_LINE; return 0; + } } } diff --git a/http.h b/http.h index 3390e49..33f42c6 100644 --- a/http.h +++ b/http.h @@ -79,6 +79,8 @@ struct http_cfg int (*write)(const void *buf, size_t n, void *user); int (*payload)(const struct http_payload *p, struct http_response *r, void *user); + int (*length)(unsigned long long len, const struct http_cookie *c, + void *user); const char *tmpdir; void *user; }; diff --git a/main.c b/main.c index 6bda8c4..5175f00 100644 --- a/main.c +++ b/main.c @@ -1,4 +1,5 @@ #include "auth.h" +#include "cftw.h" #include "handler.h" #include "http.h" #include "page.h" @@ -351,6 +352,84 @@ static int search(const struct http_payload *const p, return -1; } + +static int add_length(const char *const fpath, const struct stat *const sb, + void *const user) +{ + unsigned long long *const l = user; + + *l += sb->st_size; + return 0; +} + +static int quota_current(const struct auth *const a, + const char *const username, unsigned long long *const cur) +{ + int ret = -1; + const char *const adir = auth_dir(a); + struct dynstr d; + + dynstr_init(&d); + + if (!adir) + { + fprintf(stderr, "%s: auth_dir failed\n", __func__); + goto end; + } + else if (dynstr_append(&d, "%s/user/%s", adir, username)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + + *cur = 0; + + if (cftw(d.str, add_length, cur)) + { + fprintf(stderr, "%s: cftw: %s\n", __func__, strerror(errno)); + goto end; + } + + ret = 0; + +end: + dynstr_free(&d); + return ret; +} + +static int check_quota(const struct auth *const a, const char *const username, + const unsigned long long len, const unsigned long long quota) +{ + unsigned long long total; + + if (quota_current(a, username, &total)) + { + fprintf(stderr, "%s: quota_current failed\n", __func__); + return -1; + } + + return total + len > quota ? 1 : 0; +} + +static int check_length(const unsigned long long len, + const struct http_cookie *const c, void *const user) +{ + struct auth *const a = user; + const char *const username = c->field; + bool has_quota; + unsigned long long quota; + + if (auth_quota(a, username, &has_quota, "a)) + { + fprintf(stderr, "%s: auth_quota failed\n", __func__); + return -1; + } + else if (has_quota) + return check_quota(a, username, len, quota); + + return 0; +} + static bool path_isrel(const char *const path) { if (!strcmp(path, "..") || !strcmp(path, ".") || strstr(path, "/../")) @@ -458,7 +537,27 @@ static int getnode(const struct http_payload *const p, goto end; } - ret = page_resource(r, dir, root.str, d.str); + bool available; + unsigned long long cur, max; + + if (auth_quota(a, username, &available, &max)) + { + fprintf(stderr, "%s: quota_available failed\n", __func__); + goto end; + } + else if (available && quota_current(a, username, &cur)) + { + fprintf(stderr, "%s: quota_current failed\n", __func__); + goto end; + } + + const struct page_quota pq = + { + .cur = cur, + .max = max + }, *const ppq = available ? &pq : NULL; + + ret = page_resource(r, dir, root.str, d.str, ppq); end: dynstr_free(&d); @@ -899,8 +998,17 @@ int main(const int argc, char *const argv[]) unsigned short port; if (parse_args(argc, argv, &dir, &port, &tmpdir) - || !(a = auth_alloc(dir)) - || !(h = handler_alloc(tmpdir)) + || !(a = auth_alloc(dir))) + goto end; + + const struct handler_cfg cfg = + { + .length = check_length, + .tmpdir = tmpdir, + .user = a + }; + + if (!(h = handler_alloc(&cfg)) || handler_add(h, "/", HTTP_OP_GET, serve_index, a) || handler_add(h, "/index.html", HTTP_OP_GET, serve_index, a) || handler_add(h, "/style.css", HTTP_OP_GET, serve_style, NULL) diff --git a/page.c b/page.c index 87486ae..4471cd2 100644 --- a/page.c +++ b/page.c @@ -374,6 +374,97 @@ static int prepare_mkdir_form(struct html_node *const n, const char *const dir) return 0; } +static int prepare_quota_form(struct html_node *const n, + const struct page_quota *const q) +{ + int ret = -1; + struct html_node *progress = NULL, *div, *label; + char cur[sizeof "18446744073709551615"], max[sizeof cur]; + int res = snprintf(cur, sizeof cur, "%llu", q->cur); + struct dynstr d, pd; + + dynstr_init(&d); + dynstr_init(&pd); + + if (res < 0 || res >= sizeof cur) + { + fprintf(stderr, "%s: snprintf(3) cur failed\n", __func__); + goto end; + } + + res = snprintf(max, sizeof max, "%llu", q->max); + + if (res < 0 || res >= sizeof cur) + { + fprintf(stderr, "%s: snprintf(3) max failed\n", __func__); + goto end; + } + else if (!(div = html_node_add_child(n, "div"))) + { + fprintf(stderr, "%s: html_node_add_child div failed\n", __func__); + goto end; + } + else if (!(label = html_node_add_child(div, "label"))) + { + fprintf(stderr, "%s: html_node_add_child label failed\n", __func__); + goto end; + } + else if (!(progress = html_node_alloc("progress"))) + { + fprintf(stderr, "%s: html_node_alloc progress failed\n", __func__); + goto end; + } + else if (html_node_add_attr(progress, "value", cur)) + { + fprintf(stderr, "%s: html_node_add_attr value failed\n", __func__); + goto end; + } + else if (html_node_add_attr(progress, "max", max)) + { + fprintf(stderr, "%s: html_node_add_attr max failed\n", __func__); + goto end; + } + else if (dynstr_append(&pd, "%llu/%llu bytes", q->cur, q->max)) + { + fprintf(stderr, "%s: dynstr_append pd failed\n", __func__); + goto end; + } + else if (html_node_set_value(progress, pd.str)) + { + fprintf(stderr, "%s: html_node_set_value progress failed\n", __func__); + goto end; + } + else if (dynstr_append(&d, "File quota: ")) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (html_serialize(progress, &d)) + { + fprintf(stderr, "%s: html_serialize failed\n", __func__); + goto end; + } + else if (dynstr_append(&d, "%s", pd.str)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (html_node_set_value_unescaped(label, d.str)) + { + fprintf(stderr, "%s: html_node_set_value_unescaped label failed\n", + __func__); + goto end; + } + + ret = 0; + +end: + html_node_free(progress); + dynstr_free(&d); + dynstr_free(&pd); + return ret; +} + static int prepare_logout_form(struct html_node *const n) { struct html_node *div, *form, *input; @@ -470,7 +561,8 @@ end: } static int list_dir(struct http_response *const r, const char *const dir, - const char *const root, const char *const res) + const char *const root, const char *const res, + const struct page_quota *const q) { int ret = -1; DIR *const d = opendir(res); @@ -560,6 +652,11 @@ static int list_dir(struct http_response *const r, const char *const dir, fprintf(stderr, "%s: prepare_upload_form failed\n", __func__); goto end; } + else if (q && prepare_quota_form(body, q)) + { + fprintf(stderr, "%s: prepare_quota_form failed\n", __func__); + goto end; + } else if (prepare_logout_form(body)) { fprintf(stderr, "%s: prepare_logout_form failed\n", __func__); @@ -652,7 +749,8 @@ static int serve_file(struct http_response *const r, } int page_resource(struct http_response *const r, const char *const dir, - const char *const root, const char *const res) + const char *const root, const char *const res, + const struct page_quota *const q) { struct stat sb; @@ -677,7 +775,7 @@ int page_resource(struct http_response *const r, const char *const dir, const mode_t m = sb.st_mode; if (S_ISDIR(m)) - return list_dir(r, dir, root, res); + return list_dir(r, dir, root, res, q); else if (S_ISREG(m)) return serve_file(r, &sb, res); diff --git a/page.h b/page.h index 9743556..7ddce40 100644 --- a/page.h +++ b/page.h @@ -3,12 +3,17 @@ #include "http.h" +struct page_quota +{ + unsigned long long cur, max; +}; + int page_login(struct http_response *r); int page_style(struct http_response *r); int page_failed_login(struct http_response *r); int page_forbidden(struct http_response *r); int page_bad_request(struct http_response *r); int page_resource(struct http_response *r, const char *dir, const char *root, - const char *res); + const char *res, const struct page_quota *q); #endif /* PAGE_H */ diff --git a/usergen b/usergen index 875ef20..ec63fd3 100755 --- a/usergen +++ b/usergen @@ -1,9 +1,13 @@ #! /bin/sh +set -e + echo Username: >&2 read -r USER echo Password: >&2 read -r PWD +echo "Quota, in MiB (leave empty for unlimited quota):" >&2 +read -r QUOTA PWD=$(printf '%s' $PWD | xxd -p | tr -d '\n') SALT=$(openssl rand 32 | xxd -p | tr -d '\n') @@ -24,6 +28,7 @@ cat <<-EOF "name": "$USER", "password": "$PWD", "salt": "$SALT", - "key": "$KEY" + "key": "$KEY", + "quota": "$QUOTA" } EOF