Implement user quota

This feature allows admins to set a specific quota for each user, in
MiB. This feature is particularly useful for shared instances, where
unlimited user storage might be unfeasible or even dangerous for the
server.

Also, a nice HTML5 <progress> element has been added to the site that
shows how much of the quota has been consumed.

If no quota is set, slcl falls back to the default behaviour i.e.,
assume unlimited storage.

Limitations:

- While HTTP does specify a Content-Length, which determines the length
of the whole request, it does not specify how many files are involved
or their individual sizes.
- Because of this, if multiple files are uploaded simultaneously, the
whole request would be dropped if user quota is exceeded, even if not
all files exceeded it.
- Also, Content-Length adds the length of some HTTP boilerplate
(e.g.: boundaries), but slcl must rely on this before accepting the
whole request. In other words, this means some requests might be
rejected by slcl because of the extra bytes caused by such boilerplate.
- When the quota is exceeded, slcl must close the connection so that
the rest of the transfer is cancelled. Unfortunately, this means no
HTML can be sent back to the customer to inform about the situation.
This commit is contained in:
Xavier Del Campo Romero 2023-03-06 05:09:56 +01:00
parent 804b8841f3
commit ff8da797a1
Signed by: xavi
GPG Key ID: 84FF3612A9BF43F2
14 changed files with 393 additions and 35 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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.

91
auth.c
View File

@ -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;

3
auth.h
View File

@ -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 */

View File

@ -68,7 +68,8 @@ The following schema is expected:
"name": "...",
"password": "...",
"salt": "...",
"key": "..."
"key": "...",
"quota": "..."
}]
}
.EE

View File

@ -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;
}

View File

@ -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);

21
http.c
View File

@ -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;
}
}
}

2
http.h
View File

@ -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;
};

114
main.c
View File

@ -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)

104
page.c
View File

@ -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);

7
page.h
View File

@ -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 */

View File

@ -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