2161 lines
50 KiB
C
2161 lines
50 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), *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[] = "<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++)
|
|
{
|
|
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[] = "<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 (!(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;
|
|
}
|