slcl/main.c
Xavier Del Campo Romero 9376361bcb
main.c: Use BUFSIZ instead of arbitrary value
According to C99 7.19.1p3:

BUFSIZ is a macro that expands to an integer constant expression that is
the size of the buffer used by the setbuf function.

In other words, this means BUFSIZ is the most optimal length for a
buffer that reads a file into memory in chunks using fread(3).
2023-10-19 15:32:22 +02:00

2041 lines
47 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "auth.h"
#include "cftw.h"
#include "hex.h"
#include "page.h"
#include "style.h"
#include <libweb/handler.h>
#include <libweb/http.h>
#include <libweb/wildcard_cmp.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <dynstr.h>
#include <dirent.h>
#include <libgen.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define STYLE_PATH "style.css"
struct form
{
char *key, *value;
};
static int redirect(struct http_response *const r)
{
*r = (const struct http_response)
{
.status = HTTP_STATUS_SEE_OTHER
};
if (http_response_add_header(r, "Location", "/user/"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
return -1;
}
return 0;
}
static int serve_index(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
struct auth *const a = user;
if (auth_cookie(a, &p->cookie))
return page_login(r);
return redirect(r);
}
static int serve_style(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
int ret = -1;
struct auth *const a = user;
const char *const dir = auth_dir(a);
struct dynstr d;
dynstr_init(&d);
if (!dir)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "%s/" STYLE_PATH, dir))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if ((ret = page_style(r, d.str)))
{
fprintf(stderr, "%s: page_style failed\n", __func__);
goto end;
}
end:
dynstr_free(&d);
return ret;
}
static char *alloc_form_data(const char *const s, const char **const end)
{
const char *const next = strchr(s, '&');
const size_t len = next ? next - s : strlen(s);
char *const data = malloc(len + 1);
if (!data)
{
fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
return NULL;
}
memcpy(data, s, len);
data[len] = '\0';
*end = s + len;
if (next)
*end += 1;
return data;
}
static void form_free(struct form *const f)
{
if (f)
{
free(f->key);
free(f->value);
}
}
static void forms_free(struct form *const f, const size_t n)
{
if (f)
for (size_t i = 0; i < n; i++)
form_free(&f[i]);
free(f);
}
static int append_form(struct form **const forms, const char **const s,
size_t *const n)
{
int ret = -1;
const char *end;
char *const data = alloc_form_data(*s, &end), *key = NULL, *value = NULL;
struct form *f = NULL, *fs = NULL;
if (!data)
{
fprintf(stderr, "%s: alloc_form_data failed\n", __func__);
goto end;
}
const char *const sep = strchr(data, '=');
if (!sep)
{
fprintf(stderr, "%s: strchr(3) returned NULL\n", __func__);
ret = 1;
goto end;
}
else if (!data || !*(sep + 1))
{
fprintf(stderr, "%s: expected key=value (%s)\n", __func__, data);
ret = 1;
goto end;
}
const size_t keylen = sep - data;
if (!(key = strndup(data, keylen)))
{
fprintf(stderr, "%s: strndup(3) key: %s\n", __func__, strerror(errno));
goto end;
}
else if (!(value = strdup(sep + 1)))
{
fprintf(stderr, "%s: strdup(3) value: %s\n", __func__, strerror(errno));
goto end;
}
else if (!(fs = realloc(*forms, (*n + 1) * sizeof **forms)))
{
fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno));
goto end;
}
*forms = fs;
f = &(*forms)[(*n)++];
/* HTML input forms use '+' for whitespace, rather than %20. */
*f = (const struct form)
{
.key = http_decode_url(key, true),
.value = http_decode_url(value, true)
};
if (!f->key || !f->value)
{
fprintf(stderr, "%s: http_decode_url key/value failed\n", __func__);
goto end;
}
*s = end;
ret = 0;
end:
free(key);
free(value);
free(data);
return ret;
}
static int get_forms(const struct http_payload *const pl,
struct form **const forms, size_t *const outn)
{
int ret = -1;
const struct http_post *const p = &pl->u.post;
struct form *f = NULL;
if (!p->data)
{
fprintf(stderr, "%s: expected non-NULL buffer\n", __func__);
ret = 1;
goto end;
}
const char *s = p->data;
*outn = 0;
while (*s)
if ((ret = append_form(&f, &s, outn)))
{
if (ret < 0)
fprintf(stderr, "%s: append_form failed\n", __func__);
goto end;
}
*forms = f;
ret = 0;
end:
if (ret)
forms_free(f, *outn);
return ret;
}
static int check_credentials(struct auth *const a,
const struct form *const forms, const size_t n, char **const cookie)
{
const char *username = NULL, *pwd = NULL;
*cookie = NULL;
for (size_t i = 0; i < n; i++)
{
const struct form *const f = &forms[i];
if (!strcmp(f->key, "username"))
username = f->value;
else if (!strcmp(f->key, "password"))
pwd = f->value;
}
if (!username || !pwd)
{
fprintf(stderr, "%s: missing credentials\n", __func__);
return 1;
}
const int ret = auth_login(a, username, pwd, cookie);
if (ret < 0)
fprintf(stderr, "%s: auth_login failed\n", __func__);
return ret;
}
static int login(const struct http_payload *const pl,
struct http_response *const r, void *const user)
{
int ret = -1;
size_t n = 0;
struct form *forms = NULL;
struct auth *const a = user;
char *cookie = NULL;
if ((ret = get_forms(pl, &forms, &n)))
{
if (ret < 0)
fprintf(stderr, "%s: get_forms failed\n", __func__);
goto end;
}
else if ((ret = check_credentials(a, forms, n, &cookie)))
{
if (ret < 0)
fprintf(stderr, "%s: check_credentials failed\n", __func__);
goto end;
}
else if ((ret = redirect(r)))
{
fprintf(stderr, "%s: redirect failed\n", __func__);
goto end;
}
else if (cookie
&& (ret = http_response_add_header(r, "Set-Cookie", cookie)))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
ret = 0;
end:
forms_free(forms, n);
free(cookie);
if (ret > 0 && (ret = page_failed_login(r)))
{
fprintf(stderr, "%s: page_failed_login failed\n", __func__);
return -1;
}
return ret;
}
static int logout(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
struct auth *const a = user;
const struct http_cookie *const c = &p->cookie;
const int res = auth_cookie(a, c);
if (res < 0)
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
return -1;
}
else if (res)
return page_forbidden(r);
int ret = -1;
struct dynstr d;
static const char date[] = "Thu, 1 Jan 1970 00:00:00 GMT";
dynstr_init(&d);
*r = (const struct http_response)
{
.status = HTTP_STATUS_SEE_OTHER
};
if (http_response_add_header(r, "Location", "/"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
else if (http_response_add_header(r, "Content-Type", "text/html"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
/* Force expired cookie so they are removed by the browser, too. */
else if (dynstr_append(&d, "%s=%s; Expires=%s", c->field, c->value, date))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (http_response_add_header(r, "Set-Cookie", d.str))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static bool path_isrel(const char *const path)
{
if (!strcmp(path, "..") || !strcmp(path, ".") || strstr(path, "/../"))
return true;
static const char suffix[] = "/..";
const size_t n = strlen(path), sn = strlen(suffix);
if (n >= sn && !strcmp(path + n - sn, suffix))
return true;
return false;
}
static bool path_invalid(const char *const path)
{
return path_isrel(path) || strchr(path, '*');
}
static bool filename_invalid(const char *const path)
{
return path_invalid(path) || strchr(path, '/');
}
static bool dirname_invalid(const char *const path)
{
return *path != '/' || path[strlen(path) - 1] != '/' || path_invalid(path);
}
static int getpublic(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
int ret = -1;
struct auth *const a = user;
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 (path_invalid(p->resource))
{
fprintf(stderr, "%s: illegal relative path %s\n",
__func__, p->resource);
ret = page_forbidden(r);
goto end;
}
else if (dynstr_append(&d, "%s%s", adir, p->resource))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (page_public(r, d.str))
{
fprintf(stderr, "%s: page_public failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static char *create_symlink(const char *const username, const char *const dir,
const char *const path)
{
char *ret = NULL;
unsigned char buf[16];
char dbuf[1 + 2 * sizeof buf];
struct dynstr user, abs, rel;
dynstr_init(&user);
dynstr_init(&abs);
dynstr_init(&rel);
if (RAND_bytes(buf, sizeof buf) != 1)
{
fprintf(stderr, "%s: RAND_bytes failed with %lu\n",
__func__, ERR_get_error());
goto end;
}
else if (hex_encode(buf, dbuf, sizeof buf, sizeof dbuf))
{
fprintf(stderr, "%s: hex_encode failed\n", __func__);
goto end;
}
else if (dynstr_append(&user, "%s/user/%s%s", dir, username, path))
{
fprintf(stderr, "%s: dynstr_append user failed\n", __func__);
goto end;
}
else if (dynstr_append(&rel, "/public/%s", dbuf))
{
fprintf(stderr, "%s: dynstr_append rel failed\n", __func__);
goto end;
}
else if (dynstr_append(&abs, "%s%s", dir, rel.str))
{
fprintf(stderr, "%s: dynstr_append abs failed\n", __func__);
goto end;
}
else if (symlink(user.str, abs.str))
{
fprintf(stderr, "%s: symlink(2): %s\n", __func__, strerror(errno));
goto end;
}
ret = rel.str;
end:
dynstr_free(&user);
dynstr_free(&abs);
if (!ret)
dynstr_free(&rel);
return ret;
}
static int check_search_input(const struct http_payload *const p,
int (**const f)(struct http_response *), const struct auth *const a,
char **const dir, struct dynstr *const res)
{
int ret = auth_cookie(a, &p->cookie);
struct form *forms = NULL;
size_t n = 0;
if (ret < 0)
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
goto end;
}
else if (ret)
{
*f = page_forbidden;
goto end;
}
else if ((ret = get_forms(p, &forms, &n)))
{
if (ret < 0)
fprintf(stderr, "%s: get_forms failed\n", __func__);
else
*f = page_bad_request;
goto end;
}
const char *tdir = NULL, *tres = NULL;
for (size_t i = 0; i < n; i++)
{
const struct form *const f = &forms[i];
if (!strcmp(f->key, "dir"))
tdir = f->value;
else if (!strcmp(f->key, "name"))
tres = f->value;
}
if (!tdir)
{
fprintf(stderr, "%s: expected non-null directory\n", __func__);
ret = 1;
*f = page_bad_request;
goto end;
}
else if (!tres)
{
fprintf(stderr, "%s: expected non-null resource\n", __func__);
ret = 1;
*f = page_bad_request;
goto end;
}
else if (dirname_invalid(tdir))
{
fprintf(stderr, "%s: invalid directory %s\n", __func__, tdir);
ret = 1;
*f = page_bad_request;
goto end;
}
else if (path_isrel(tres))
{
fprintf(stderr, "%s: invalid resource %s\n", __func__, tres);
ret = 1;
*f = page_bad_request;
goto end;
}
else if (!(*dir = strdup(tdir)))
{
fprintf(stderr, "%s: strdup(3) dir: %s\n", __func__, strerror(errno));
ret = -1;
goto end;
}
else if (dynstr_append(res, "*%s*", tres))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
ret = -1;
goto end;
}
end:
forms_free(forms, n);
return ret;
}
static void search_result_free(struct page_search_result *const r)
{
if (r)
free(r->name);
}
static void search_results_free(struct page_search *const s)
{
if (s)
{
for (size_t i = 0; i < s->n; i++)
search_result_free(&s->results[i]);
free(s->results);
}
}
struct search_args
{
const char *root, *res;
struct page_search *s;
};
static int search_fn(const char *const fpath, const struct stat *const sb,
bool *const done, void *const user)
{
static const size_t limit = 200;
const struct search_args *const sa = user;
const char *rel = fpath + strlen(sa->root);
struct page_search *const res = sa->s;
struct page_search_result *results = NULL, *r = NULL;
rel += strspn(rel, "/");
if (wildcard_cmp(rel, sa->res, false))
return 0;
else if (!(results = realloc(res->results,
(res->n + 1) * sizeof *res->results)))
{
fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno));
goto failure;
}
r = &results[res->n];
*r = (const struct page_search_result)
{
.name = strdup(rel)
};
if (!r->name)
{
fprintf(stderr, "%s: strdup(3): %s", __func__, strerror(errno));
goto failure;
}
res->results = results;
if (++res->n >= limit)
{
sa->s->limit_exceeded = true;
*done = true;
}
return 0;
failure:
search_result_free(r);
free(results);
return -1;
}
static int do_search(const char *const abs, const char *const root,
const char *const res, struct page_search *const s)
{
struct search_args sa =
{
.root = root,
.res = res,
.s = s
};
s->root = root;
if (cftw(abs, search_fn, &sa))
{
fprintf(stderr, "%s: cftw failed\n", __func__);
return -1;
}
return 0;
}
static int search(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
int ret = -1;
const struct auth *const a = user;
const char *const username = p->cookie.field, *const root = auth_dir(a);
struct page_search s = {0};
int (*f)(struct http_response *);
char *dir = NULL;
struct dynstr userd, d, res;
dynstr_init(&userd);
dynstr_init(&d);
dynstr_init(&res);
if (!root)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if ((ret = check_search_input(p, &f, a, &dir, &res)))
{
if (ret < 0)
fprintf(stderr, "%s: check_search_input failed\n", __func__);
else if ((ret = f(r)))
fprintf(stderr, "%s: check_search_input callback failed\n",
__func__);
goto end;
}
else if (dynstr_append(&userd, "%s/user/%s", root, username))
{
fprintf(stderr, "%s: dynstr_append userd failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "%s/%s", userd.str, dir + strspn(dir, "/")))
{
fprintf(stderr, "%s: dynstr_append d failed\n", __func__);
goto end;
}
else if ((ret = do_search(d.str, userd.str, res.str, &s)))
{
if (ret < 0)
fprintf(stderr, "%s: do_search failed\n", __func__);
goto end;
}
else if ((ret = page_search(r, &s)))
{
fprintf(stderr, "%s: page_search failed\n", __func__);
goto end;
}
end:
free(dir);
dynstr_free(&userd);
dynstr_free(&d);
dynstr_free(&res);
search_results_free(&s);
return ret;
}
static int share(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
struct auth *const a = user;
if (auth_cookie(a, &p->cookie))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
return page_forbidden(r);
}
const char *const adir = auth_dir(a);
if (!adir)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
return -1;
}
int ret = -1;
struct form *forms = NULL;
size_t n = 0;
char *sympath = NULL;
if ((ret = get_forms(p, &forms, &n)))
{
if (ret < 0)
fprintf(stderr, "%s: get_forms failed\n", __func__);
else
ret = page_bad_request(r);
goto end;
}
else if (n != 1)
{
fprintf(stderr, "%s: expected 1 form, got %zu\n", __func__, n);
ret = page_bad_request(r);
goto end;
}
const char *const path = forms->value, *const username = p->cookie.field;
if (path_invalid(path))
{
fprintf(stderr, "%s: invalid path %s\n", __func__, path);
ret = page_bad_request(r);
goto end;
}
else if (!(sympath = create_symlink(username, adir, path)))
{
fprintf(stderr, "%s: create_symlink failed\n", __func__);
goto end;
}
else if (page_share(r, sympath))
{
fprintf(stderr, "%s: page_share failed\n", __func__);
goto end;
}
ret = 0;
end:
forms_free(forms, n);
free(sympath);
return ret;
}
static int add_length(const char *const fpath, const struct stat *const sb,
bool *const done, void *const user)
{
if (!S_ISREG(sb->st_mode))
return 0;
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, struct http_response *const r,
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)
{
int res = check_quota(a, username, len, quota);
if (res < 0)
fprintf(stderr, "%s: check_quota failed\n", __func__);
else if (res > 0 && page_quota_exceeded(r, len, quota) < 0)
return -1;
return res;
}
return 0;
}
static int getnode(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
struct auth *const a = user;
if (auth_cookie(a, &p->cookie))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
return page_forbidden(r);
}
const char *const username = p->cookie.field,
*const resource = p->resource + strlen("/user/");
if (path_invalid(resource))
{
fprintf(stderr, "%s: illegal relative path %s\n", __func__, resource);
return page_forbidden(r);
}
int ret = -1;
struct dynstr dir, root, d;
const char *const adir = auth_dir(a),
*const sep = p->resource[strlen(p->resource) - 1] != '/' ? "/" : "";
dynstr_init(&dir);
dynstr_init(&d);
dynstr_init(&root);
if (!adir)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if (dynstr_append(&dir, "%s%s", p->resource, sep))
{
fprintf(stderr, "%s: dynstr_append dird failed\n", __func__);
goto end;
}
else if (dynstr_append(&root, "%s/user/%s/", adir, username)
|| dynstr_append(&d, "%s%s", root.str, resource))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
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_resource pr =
{
.r = r,
.args = p->args,
.n_args = p->n_args,
.dir = dir.str,
.root = root.str,
.res = d.str,
.q = available ?
&(const struct page_quota) {.cur = cur, .max = max }
: NULL
};
ret = page_resource(&pr);
end:
dynstr_free(&dir);
dynstr_free(&d);
dynstr_free(&root);
return ret;
}
static int getnode_head(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
struct auth *const a = user;
if (auth_cookie(a, &p->cookie))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
return page_forbidden(r);
}
const char *const username = p->cookie.field,
*const resource = p->resource + strlen("/user/");
if (path_invalid(resource))
{
fprintf(stderr, "%s: illegal relative path %s\n", __func__, resource);
return page_forbidden(r);
}
int ret = -1;
struct dynstr dir, root, d;
const char *const adir = auth_dir(a),
*const sep = p->resource[strlen(p->resource) - 1] != '/' ? "/" : "";
dynstr_init(&dir);
dynstr_init(&d);
dynstr_init(&root);
if (!adir)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if (dynstr_append(&dir, "%s%s", p->resource, sep))
{
fprintf(stderr, "%s: dynstr_append dird failed\n", __func__);
goto end;
}
else if (dynstr_append(&root, "%s/user/%s/", adir, username)
|| dynstr_append(&d, "%s%s", root.str, resource))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
ret = page_head_resource(r, d.str);
end:
dynstr_free(&dir);
dynstr_free(&d);
dynstr_free(&root);
return ret;
}
static int move_file(const char *const old, const char *const new)
{
int ret = -1;
FILE *const f = fopen(old, "rb");
const int fd = open(new, O_WRONLY | O_CREAT, 0600);
struct stat sb;
if (!f)
{
fprintf(stderr, "%s: fopen(3): %s\n", __func__, strerror(errno));
goto end;
}
else if (fd < 0)
{
fprintf(stderr, "%s: open(2): %s\n", __func__, strerror(errno));
goto end;
}
else if (stat(old, &sb))
{
fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno));
goto end;
}
for (off_t i = 0; i < sb.st_size;)
{
char buf[BUFSIZ];
const off_t left = sb.st_size - i;
const size_t rem = left > sizeof buf ? sizeof buf : left;
ssize_t w;
if (!fread(buf, rem, 1, f))
{
fprintf(stderr, "%s: fread(3) failed, feof=%d, ferror=%d\n",
__func__, feof(f), ferror(f));
goto end;
}
else if ((w = write(fd, buf, rem)) < 0)
{
fprintf(stderr, "%s: write(2): %s\n", __func__, strerror(errno));
goto end;
}
else if (w != rem)
{
fprintf(stderr, "%s: write(2): expected to write %zu bytes, "
"only %ju written\n", __func__, rem, (intmax_t)w);
goto end;
}
i += rem;
}
ret = 0;
end:
if (fd >= 0 && close(fd))
{
fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno));
ret = -1;
}
if (f && fclose(f))
{
fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno));
ret = -1;
}
return ret;
}
static int rename_or_move(const char *const old, const char *const new)
{
const int res = rename(old, new);
if (res && errno == EXDEV)
return move_file(old, new);
else if (res)
fprintf(stderr, "%s: rename(3): %s\n", __func__, strerror(errno));
return res;
}
static int upload_file(const struct http_post_file *const f,
const char *const user, const char *const root, const char *const dir)
{
int ret = -1;
struct dynstr d;
dynstr_init(&d);
if (!root)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "%s/user/%s/%s%s", root, user, dir, f->filename))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (rename_or_move(f->tmpname, d.str))
{
fprintf(stderr, "%s: rename_or_move failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static int redirect_to_dir(const char *const dir,
struct http_response *const r)
{
int ret = -1;
struct dynstr d;
char *const encdir = http_encode_url(dir);
dynstr_init(&d);
*r = (const struct http_response)
{
.status = HTTP_STATUS_SEE_OTHER
};
if (!encdir)
{
fprintf(stderr, "%s: http_encode_url failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "/user%s", encdir))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (http_response_add_header(r, "Location", d.str))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
ret = 0;
end:
free(encdir);
dynstr_free(&d);
return ret;
}
static const char *get_upload_dir(const struct http_post *const po)
{
for (size_t i = 0; i < po->npairs; i++)
{
const struct http_post_pair *p = &po->pairs[i];
if (!strcmp(p->name, "dir"))
return p->value;
}
return NULL;
}
static int upload_files(const struct http_payload *const p,
struct http_response *const r, const struct auth *const a)
{
const struct http_post *const po = &p->u.post;
const char *const root = auth_dir(a), *const user = p->cookie.field,
*const dir = get_upload_dir(po);
if (!po->files)
{
fprintf(stderr, "%s: expected file list\n", __func__);
return 1;
}
else if (!root)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
return -1;
}
else if (!dir)
{
static const char body[] = "<html>No target directory set</html>";
*r = (const struct http_response)
{
.status = HTTP_STATUS_BAD_REQUEST,
.buf.ro = body,
.n = strlen(body)
};
if (http_response_add_header(r, "Content-Type", "text/html"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
return -1;
}
return 0;
}
else if (dirname_invalid(dir))
{
fprintf(stderr, "%s: invalid directory %s\n", __func__, dir);
return page_bad_request(r);
}
for (size_t i = 0; i < po->nfiles; i++)
{
if (upload_file(&po->files[i], user, root, dir))
{
fprintf(stderr, "%s: upload_file failed\n", __func__);
return -1;
}
}
return redirect_to_dir(dir, r);
}
static int upload(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
const struct auth *const a = user;
if (auth_cookie(a, &p->cookie))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
return page_forbidden(r);
}
else if (p->u.post.expect_continue)
{
*r = (const struct http_response)
{
.status = HTTP_STATUS_CONTINUE
};
return 0;
}
return upload_files(p, r, a);
}
static int createdir(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
int ret = -1;
struct auth *const a = user;
struct dynstr d, userd;
struct form *forms = NULL;
size_t n = 0;
dynstr_init(&d);
dynstr_init(&userd);
if (auth_cookie(a, &p->cookie))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
ret = page_forbidden(r);
goto end;
}
else if ((ret = get_forms(p, &forms, &n)))
{
if (ret < 0)
fprintf(stderr, "%s: get_forms failed\n", __func__);
else
ret = page_bad_request(r);
goto end;
}
else if (n != 2)
{
fprintf(stderr, "%s: expected 2 forms, got %zu\n", __func__, n);
ret = page_bad_request(r);
goto end;
}
char *name = NULL, *dir = NULL;
for (size_t i = 0; i < n; i++)
{
const struct form *const f = &forms[i];
if (!strcmp(f->key, "name"))
name = f->value;
else if (!strcmp(f->key, "dir"))
dir = f->value;
else
{
fprintf(stderr, "%s: unexpected key %s\n", __func__, f->key);
ret = page_bad_request(r);
goto end;
}
}
if (!name || !dir)
{
fprintf(stderr, "%s: missing name or directory\n", __func__);
ret = page_bad_request(r);
goto end;
}
else if (filename_invalid(name))
{
fprintf(stderr, "%s: invalid directory name %s\n", __func__, dir);
ret = page_bad_request(r);
goto end;
}
else if (dirname_invalid(dir))
{
fprintf(stderr, "%s: invalid name %s\n", __func__, name);
ret = page_bad_request(r);
goto end;
}
const char *const root = auth_dir(a);
if (!root)
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "%s/user/%s/%s%s",
root, p->cookie.field, dir, name))
{
fprintf(stderr, "%s: dynstr_append d failed\n", __func__);
goto end;
}
else if (dynstr_append(&userd, "/user%s%s/", dir, name))
{
fprintf(stderr, "%s: dynstr_append userd failed\n", __func__);
goto end;
}
else if (mkdir(d.str, 0700))
{
if (errno != EEXIST)
{
fprintf(stderr, "%s: mkdir(2): %s\n", __func__, strerror(errno));
goto end;
}
else
{
static const char body[] = "<html>Directory already exists</html>";
*r = (const struct http_response)
{
.status = HTTP_STATUS_BAD_REQUEST,
.buf.ro = body,
.n = sizeof body - 1
};
if (http_response_add_header(r, "Content-Type", "text/html"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
return -1;
}
}
}
else
{
*r = (const struct http_response)
{
.status = HTTP_STATUS_SEE_OTHER
};
if (http_response_add_header(r, "Location", userd.str))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
}
ret = 0;
end:
forms_free(forms, n);
dynstr_free(&userd);
dynstr_free(&d);
return ret;
}
static int check_rm_input(const struct form *const forms, const size_t n,
struct page_rm *const rm, int (**const cb)(struct http_response *))
{
for (size_t i = 0; i < n; i++)
{
const struct form *const f = &forms[i];
if (!strcmp(f->key, "dir"))
{
if (!rm->dir)
rm->dir = f->value;
else
{
fprintf(stderr, "%s: directory defined more than once\n",
__func__);
*cb = page_bad_request;
return 1;
}
}
else if (!strcmp(f->key, "path"))
{
const char *const path = f->value;
if (path_isrel(path))
{
fprintf(stderr, "%s: invalid path %s\n", __func__, rm->dir);
*cb = page_bad_request;
return 1;
}
const char **tmp = realloc(rm->items, (rm->n + 1) * sizeof *tmp);
if (!tmp)
{
fprintf(stderr, "%s: realloc(3): %s\n",
__func__, strerror(errno));
return -1;
}
tmp[rm->n++] = path;
rm->items = tmp;
}
}
if (!rm->dir)
{
fprintf(stderr, "%s: expected non-null dir\n", __func__);
*cb = page_bad_request;
return 1;
}
else if (dirname_invalid(rm->dir))
{
fprintf(stderr, "%s: invalid directory %s\n", __func__, rm->dir);
*cb = page_bad_request;
return 1;
}
return 0;
}
static int confirm_rm(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
int ret = -1;
struct auth *const a = user;
struct form *forms = NULL;
const char **items = NULL;
size_t n = 0;
int (*f)(struct http_response *);
struct page_rm rm = {0};
if (auth_cookie(a, &p->cookie))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
ret = page_forbidden(r);
goto end;
}
else if ((ret = get_forms(p, &forms, &n)))
{
if (ret < 0)
fprintf(stderr, "%s: get_forms failed\n", __func__);
else
ret = page_bad_request(r);
goto end;
}
else if ((ret = check_rm_input(forms, n, &rm, &f)))
{
if (ret < 0)
fprintf(stderr, "%s: check_rm_input failed\n", __func__);
else if ((ret = f(r)))
fprintf(stderr, "%s: check_rm_input callback failed\n",
__func__);
goto end;
}
else if ((ret = page_rm(r, &rm)))
{
fprintf(stderr, "%s: page_rm failed\n", __func__);
goto end;
}
end:
forms_free(forms, n);
free(items);
free(rm.items);
return ret;
}
static const char *find_rm_dir(const struct form *const forms, const size_t n,
int (**const cb)(struct http_response *))
{
const char *dir = NULL;
for (size_t i = 0; i < n; i++)
{
const struct form *const f = &forms[i];
if (!strcmp(f->key, "dir"))
{
if (!dir)
dir = f->value;
else
{
fprintf(stderr, "%s: directory defined more than once\n",
__func__);
*cb = page_bad_request;
return NULL;
}
}
}
return dir;
}
static int rm_dir_contents(const char *const fpath,
const struct stat *const sb, bool *const done, void *const user)
{
if (S_ISDIR(sb->st_mode) && rmdir(fpath))
{
fprintf(stderr, "%s: rmdir(2) %s: %s\n",
__func__, fpath, strerror(errno));
return -1;
}
else if (S_ISREG(sb->st_mode) && remove(fpath))
{
fprintf(stderr, "%s: remove(3) %s: %s\n",
__func__, fpath, strerror(errno));
return -1;
}
return 0;
}
static int rmdir_r(const char *const path)
{
int ret = -1;
DIR *const d = opendir(path);
if (!d)
{
fprintf(stderr, "%s: opendir(3): %s\n", __func__, strerror(errno));
goto end;
}
else if (cftw(path, rm_dir_contents, NULL))
{
fprintf(stderr, "%s: rm_dir_contents failed\n", __func__);
goto end;
}
else if (rmdir(path))
{
fprintf(stderr, "%s: rmdir(2) %s: %s\n",
__func__, path, strerror(errno));
goto end;
}
ret = 0;
end:
if (d && closedir(d))
{
fprintf(stderr, "%s: closedir(3): %s\n", __func__, strerror(errno));
ret = -1;
}
return ret;
}
static int rm_item(const char *const root, const char *const item)
{
int ret = -1;
struct stat sb;
struct dynstr d;
dynstr_init(&d);
if (dynstr_append(&d, "%s/%s", root, item))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (stat(d.str, &sb))
{
fprintf(stderr, "%s: stat(2) %s: %s\n",
__func__, d.str, strerror(errno));
/* This might have already been removed from another request,
* and thus this situation should not be considered an error. */
if (errno == ENOENT || errno == ENOTDIR)
ret = 0;
goto end;
}
else if (S_ISDIR(sb.st_mode) && rmdir_r(d.str))
{
fprintf(stderr, "%s: rmdir_r failed\n", __func__);
goto end;
}
else if (S_ISREG(sb.st_mode) && remove(d.str))
{
fprintf(stderr, "%s: remove(3): %s\n", __func__, strerror(errno));
goto end;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static int do_rm(const struct form *const forms, const size_t n,
const char *const dir)
{
for (size_t i = 0; i < n; i++)
{
const struct form *const f = &forms[i];
if (!strcmp(f->key, "path"))
{
const char *const path = f->value;
if (path_isrel(path))
{
fprintf(stderr, "%s: invalid path %s\n", __func__, path);
return 1;
}
else if (rm_item(dir, f->value))
{
fprintf(stderr, "%s: rm_item failed\n", __func__);
return -1;
}
}
}
return 0;
}
static int rm(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
int ret = -1;
struct auth *const a = user;
struct form *forms = NULL;
size_t n = 0;
const struct http_cookie *const c = &p->cookie;
const char *username = c->field, *adir;
struct dynstr d, userdir;
dynstr_init(&d);
dynstr_init(&userdir);
if (auth_cookie(a, c))
{
fprintf(stderr, "%s: auth_cookie failed\n", __func__);
ret = page_forbidden(r);
goto end;
}
else if (!(adir = auth_dir(a)))
{
fprintf(stderr, "%s: auth_dir failed\n", __func__);
goto end;
}
else if ((ret = get_forms(p, &forms, &n)))
{
if (ret < 0)
fprintf(stderr, "%s: get_forms failed\n", __func__);
else
ret = page_bad_request(r);
goto end;
}
int (*f)(struct http_response *);
const char *const dir = find_rm_dir(forms, n, &f);
if (!dir)
{
fprintf(stderr, "%s: expected non-null directory\n", __func__);
ret = f(r);
goto end;
}
else if (dirname_invalid(dir))
{
fprintf(stderr, "%s: invalid directory %s\n", __func__, dir);
ret = page_bad_request(r);
goto end;
}
else if (dynstr_append(&userdir, "%s/user/%s%s", adir, username, dir))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if ((ret = do_rm(forms, n, userdir.str)))
{
if (ret < 0)
fprintf(stderr, "%s: do_rm failed\n", __func__);
else if ((ret = f(r)))
fprintf(stderr, "%s: rm callback failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "/user%s", dir))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
*r = (const struct http_response)
{
.status = HTTP_STATUS_SEE_OTHER
};
if (http_response_add_header(r, "Location", d.str))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
dynstr_free(&userdir);
forms_free(forms, n);
return ret;
}
static void usage(char *const argv[])
{
fprintf(stderr, "%s [-t tmpdir] [-p port] dir\n", *argv);
}
static int parse_args(const int argc, char *const argv[],
const char **const dir, unsigned short *const port,
const char **const tmpdir)
{
const char *const envtmp = getenv("TMPDIR");
int opt;
/* Default values. */
*port = 0;
*tmpdir = envtmp ? envtmp : "/tmp";
while ((opt = getopt(argc, argv, "t:p:")) != -1)
{
switch (opt)
{
case 't':
*tmpdir = optarg;
break;
case 'p':
{
char *endptr;
const unsigned long portul = strtoul(optarg, &endptr, 10);
if (*endptr || portul > UINT16_MAX)
{
fprintf(stderr, "%s: invalid port %s\n", __func__, optarg);
return -1;
}
*port = portul;
}
break;
default:
usage(argv);
return -1;
}
}
if (optind >= argc)
{
usage(argv);
return -1;
}
*dir = argv[optind];
return 0;
}
static int ensure_dir(const char *const dir)
{
struct stat sb;
if (stat(dir, &sb))
{
switch (errno)
{
case ENOENT:
if (mkdir(dir, S_IRWXU))
{
fprintf(stderr, "%s: mkdir(2) %s: %s\n",
__func__, dir, strerror(errno));
return -1;
}
printf("Created empty directory at %s\n", dir);
break;
default:
fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno));
return -1;
}
}
else if (!S_ISDIR(sb.st_mode))
{
fprintf(stderr, "%s: %s not a directory\n", __func__, dir);
return -1;
}
return 0;
}
static int init_dirs(const char *const dir)
{
int ret = -1;
struct dynstr user, public;
struct sb;
dynstr_init(&user);
dynstr_init(&public);
if (dynstr_append(&user, "%s/user", dir))
{
fprintf(stderr, "%s: dynstr_append user failed\n", __func__);
goto end;
}
else if (dynstr_append(&public, "%s/public", dir))
{
fprintf(stderr, "%s: dynstr_append public failed\n", __func__);
goto end;
}
else if (ensure_dir(dir))
{
fprintf(stderr, "%s: ensure_dir dir failed\n", __func__);
goto end;
}
else if (ensure_dir(user.str))
{
fprintf(stderr, "%s: ensure_dir user failed\n", __func__);
goto end;
}
else if (ensure_dir(public.str))
{
fprintf(stderr, "%s: ensure_dir public failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&user);
dynstr_free(&public);
return ret;
}
static int dump_default_style(const char *const path)
{
int ret = -1;
FILE *const f = fopen(path, "wb");
if (!f)
{
fprintf(stderr, "%s: fopen(3) %s: %s\n",
__func__, path, strerror(errno));
goto end;
}
else if (!fwrite(style_default, style_default_len, 1, f))
{
fprintf(stderr, "%s: fwrite(3): %s\n", __func__, strerror(errno));
goto end;
}
printf("Dumped default stylesheet into %s\n", path);
ret = 0;
end:
if (f && fclose(f))
{
fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno));
ret = -1;
}
return ret;
}
static int ensure_style(const char *const dir)
{
int ret = -1;
struct dynstr d;
struct stat sb;
dynstr_init(&d);
if (dynstr_append(&d, "%s/" STYLE_PATH, dir))
{
fprintf(stderr, "%s: dynstr_append user failed\n", __func__);
goto end;
}
else if (stat(d.str, &sb))
{
switch (errno)
{
case ENOENT:
if (dump_default_style(d.str))
{
fprintf(stderr, "%s: dump_default_style failed\n",
__func__);
goto end;
}
break;
default:
fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno));
goto end;
}
}
else if (!S_ISREG(sb.st_mode))
{
fprintf(stderr, "%s: %s not a regular file\n", __func__, d.str);
return -1;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static int add_urls(struct handler *const h, void *const user)
{
static const struct url
{
const char *url;
enum http_op op;
handler_fn f;
} urls[] =
{
{.url = "/", .op = HTTP_OP_GET, .f = serve_index},
{.url = "/index.html", .op = HTTP_OP_GET, .f = serve_index},
{.url = "/style.css", .op = HTTP_OP_GET, .f = serve_style},
{.url = "/user/*", .op = HTTP_OP_GET, .f = getnode},
{.url = "/user/*", .op = HTTP_OP_HEAD, .f = getnode_head},
{.url = "/login", .op = HTTP_OP_POST, .f = login},
{.url = "/logout", .op = HTTP_OP_POST, .f = logout},
{.url = "/public/*", .op = HTTP_OP_GET, .f = getpublic},
{.url = "/search", .op = HTTP_OP_POST, .f = search},
{.url = "/share", .op = HTTP_OP_POST, .f = share},
{.url = "/upload", .op = HTTP_OP_POST, .f = upload},
{.url = "/mkdir", .op = HTTP_OP_POST, .f = createdir},
{.url = "/confirm/rm", .op = HTTP_OP_POST, .f = confirm_rm},
{.url = "/rm", .op = HTTP_OP_POST, .f = rm}
};
for (size_t i = 0; i < sizeof urls / sizeof *urls; i++)
{
const struct url *const u = &urls[i];
if (handler_add(h, u->url, u->op, u->f, user))
{
fprintf(stderr, "%s: handler_add %s failed\n", __func__, u->url);
return -1;
}
}
return 0;
}
int main(int argc, char *argv[])
{
int ret = EXIT_FAILURE;
struct handler *h = NULL;
struct auth *a = NULL;
const char *dir, *tmpdir;
unsigned short port;
if (parse_args(argc, argv, &dir, &port, &tmpdir)
|| init_dirs(dir)
|| ensure_style(dir)
|| !(a = auth_alloc(dir)))
goto end;
const struct handler_cfg cfg =
{
.length = check_length,
.tmpdir = tmpdir,
.user = a
};
if (!(h = handler_alloc(&cfg))
|| add_urls(h, a)
|| handler_listen(h, port))
goto end;
ret = EXIT_SUCCESS;
end:
auth_free(a);
handler_free(h);
return ret;
}