#define _POSIX_C_SOURCE 200809L #include "auth.h" #include "cftw.h" #include "hex.h" #include "page.h" #include "style.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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), *enckey = NULL, *encvalue = NULL, *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 (!(enckey = strndup(data, keylen))) { fprintf(stderr, "%s: strndup(3) enckey: %s\n", __func__, strerror(errno)); goto end; } else if (!(encvalue = strdup(sep + 1))) { fprintf(stderr, "%s: strdup(3) encvalue: %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; /* HTML input forms use '+' for whitespace, rather than %20. */ if ((ret = http_decode_url(enckey, true, &key))) { fprintf(stderr, "%s: http_decode_url enckey failed\n", __func__); goto end; } else if ((ret = http_decode_url(encvalue, true, &value))) { fprintf(stderr, "%s: http_decode_url encvalue failed\n", __func__); goto end; } f = &(*forms)[(*n)++]; *f = (const struct form) { .key = key, .value = value }; *s = end; ret = 0; end: if (ret) { free(key); free(value); } free(enckey); free(encvalue); 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, ".") || !strncmp(path, "./", strlen("./")) || !strncmp(path, "../", strlen("../")) || strstr(path, "/./") || strstr(path, "/../")) return true; static const char *const suffixes[] = {"/.", "/.."}; for (size_t i = 0; i < sizeof suffixes / sizeof *suffixes; i++) { const char *const suffix = suffixes[i]; 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), *const file = p->resource + strlen("/public/"); struct dynstr d; dynstr_init(&d); if (!adir) { fprintf(stderr, "%s: auth_dir failed\n", __func__); goto end; } else if (!*file || filename_invalid(file)) { fprintf(stderr, "%s: invalid filename %s\n", __func__, p->resource); ret = page_forbidden(r); 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_cookie(a, c)) { fprintf(stderr, "%s: auth_cookie failed\n", __func__); if (page_forbidden(r)) return -1; return 1; } else if (auth_quota(a, username, &has_quota, "a)) { 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; const int fd_old = open(old, O_RDONLY), fd_new = open(new, O_WRONLY | O_CREAT, 0600); struct stat sb; if (fd_old < 0) { fprintf(stderr, "%s: open(2) fd_old: %s\n", __func__, strerror(errno)); goto end; } else if (fd_new < 0) { fprintf(stderr, "%s: open(2): %s\n", __func__, strerror(errno)); goto end; } else if (fstat(fd_old, &sb)) { fprintf(stderr, "%s: fstat(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; const ssize_t r = read(fd_old, buf, rem); if (r < 0) { fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); goto end; } size_t wrem = r; const void *p = buf; while (wrem) { const ssize_t w = write(fd_new, p, wrem); if (w < 0) { fprintf(stderr, "%s: write(2): %s\n", __func__, strerror(errno)); goto end; } p = (const char *)p + w; wrem -= w; } i += rem; } ret = 0; end: if (fd_old >= 0 && close(fd_old)) { fprintf(stderr, "%s: close(2) fd_old: %s\n", __func__, strerror(errno)); ret = -1; } if (fd_new >= 0 && close(fd_new)) { fprintf(stderr, "%s: close(2) fd_new: %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 check_upload_dir(const char *const dir) { struct stat sb; if (stat(dir, &sb)) { switch (errno) { case ENOENT: /* Fall through. */ case ENOTDIR: fprintf(stderr, "%s: cannot upload to non-existing dir %s\n", __func__, dir); return 1; default: fprintf(stderr, "%s: stat(2) %s: %s\n", __func__, dir, strerror(errno)); return -1; } } else if (!S_ISDIR(sb.st_mode)) { fprintf(stderr, "%s: %s not a dir\n", __func__, dir); return 1; } return 0; } 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 dird, d; dynstr_init(&dird); dynstr_init(&d); if (!root) { fprintf(stderr, "%s: auth_dir failed\n", __func__); goto end; } else if (dynstr_append(&dird, "%s/user/%s/%s", root, user, dir)) { fprintf(stderr, "%s: dynstr_append dird failed\n", __func__); goto end; } else if ((ret = check_upload_dir(dird.str))) { if (ret < 0) fprintf(stderr, "%s: check_upload_dir failed\n", __func__); goto end; } else if (dynstr_append(&d, "%s%s", dird.str, f->filename)) { fprintf(stderr, "%s: dynstr_append d 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(&dird); 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[] = "No target directory set"; *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++) { const int ret = upload_file(&po->files[i], user, root, dir); if (ret < 0) { fprintf(stderr, "%s: upload_file failed\n", __func__); return -1; } else if (ret) return page_bad_request(r); } 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->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; char *encurl = NULL; 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; } const 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[] = "Directory already exists"; *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 (!(encurl = http_encode_url(userd.str))) { fprintf(stderr, "%s: http_encode_url failed\n", __func__); goto end; } else if (http_response_add_header(r, "Location", encurl)) { 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); free(encurl); 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, .post = { /* Arbitrary limit. */ .max_files = 10000, /* File upload only requires one pair. */ .max_pairs = 1 } }; unsigned short outport; if (!(h = handler_alloc(&cfg)) || add_urls(h, a) || handler_listen(h, port, &outport)) goto end; printf("Listening on port %hu\n", outport); if (handler_loop(h)) { fprintf(stderr, "%s: handler_loop failed\n", __func__); goto end; } ret = EXIT_SUCCESS; end: auth_free(a); handler_free(h); return ret; }