aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--Makefile3
-rw-r--r--README.md14
-rw-r--r--auth.c91
-rw-r--r--auth.h3
-rw-r--r--doc/man1/slcl.13
-rw-r--r--handler.c48
-rw-r--r--handler.h10
-rw-r--r--http.c21
-rw-r--r--http.h2
-rw-r--r--main.c114
-rw-r--r--page.c104
-rw-r--r--page.h7
-rwxr-xr-xusergen7
14 files changed, 393 insertions, 35 deletions
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 <stdbool.h>
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, &quota))
+ {
+ 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