Implement public file sharing

An HTML form is now added next to each regular file, that generates a
POST request. Then, slcl replies with a HTML document with a link to the
public resource (which are implemented as symlinks).

Limitations:

- For now, only regular files can be shared i.e., sharing directories is
not possible. While feasible, it still requires a larger refactor to
list_dir and resource_layout, so that read-only access to the directory
is provided to anonymous users.
This commit is contained in:
Xavier Del Campo Romero 2023-03-09 01:29:48 +01:00
parent 13f96054f6
commit 2e1b131396
Signed by: xavi
GPG Key ID: 84FF3612A9BF43F2
3 changed files with 369 additions and 15 deletions

196
main.c
View File

@ -1,8 +1,11 @@
#include "auth.h"
#include "cftw.h"
#include "handler.h"
#include "hex.h"
#include "http.h"
#include "page.h"
#include <openssl/err.h>
#include <openssl/rand.h>
#include <dynstr.h>
#include <libgen.h>
#include <fcntl.h>
@ -352,6 +355,116 @@ end:
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 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_isrel(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 search(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
@ -367,6 +480,73 @@ static int search(const struct http_payload *const p,
return -1;
}
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;
char *sympath = NULL;
if (!(forms = get_forms(p, &n)))
{
fprintf(stderr, "%s: get_forms failed\n", __func__);
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_isrel(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:
if (forms)
for (size_t i = 0; i < n; i++)
form_free(&forms[i]);
free(forms);
free(sympath);
return ret;
}
static int add_length(const char *const fpath, const struct stat *const sb,
void *const user)
@ -445,20 +625,6 @@ static int check_length(const unsigned long long len,
return 0;
}
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 int getnode(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
@ -976,7 +1142,9 @@ int main(const int argc, char *const argv[])
|| handler_add(h, "/user/*", HTTP_OP_GET, getnode, a)
|| handler_add(h, "/login", HTTP_OP_POST, login, a)
|| handler_add(h, "/logout", HTTP_OP_POST, logout, a)
|| handler_add(h, "/public/*", HTTP_OP_GET, getpublic, a)
|| handler_add(h, "/search", HTTP_OP_POST, search, a)
|| handler_add(h, "/share", HTTP_OP_POST, share, a)
|| handler_add(h, "/upload", HTTP_OP_POST, upload, a)
|| handler_add(h, "/mkdir", HTTP_OP_POST, createdir, a)
|| handler_listen(h, port))

186
page.c
View File

@ -161,11 +161,89 @@ static int prepare_date(struct html_node *const n, const struct stat *const sb)
return 0;
}
static int prepare_share(struct html_node *const n,
const struct stat *const sb, const char *const dir, const char *const name)
{
int ret = -1;
const char *const fdir = dir + strlen("/user");
struct html_node *form, *file, *submit;
struct dynstr d;
dynstr_init(&d);
if (!S_ISREG(sb->st_mode))
return 0;
if (!(form = html_node_add_child(n, "form")))
{
fprintf(stderr, "%s: html_node_add_child form failed\n", __func__);
goto end;
}
else if (!(file = html_node_add_child(form, "input")))
{
fprintf(stderr, "%s: html_node_add_child file failed\n", __func__);
goto end;
}
else if (!(submit = html_node_add_child(form, "input")))
{
fprintf(stderr, "%s: html_node_add_child file failed\n", __func__);
goto end;
}
else if (html_node_add_attr(form, "method", "post"))
{
fprintf(stderr, "%s: html_node_add_attr method failed\n", __func__);
goto end;
}
else if (html_node_add_attr(form, "action", "/share"))
{
fprintf(stderr, "%s: html_node_add_attr action failed\n", __func__);
goto end;
}
else if (html_node_add_attr(file, "type", "hidden"))
{
fprintf(stderr, "%s: html_node_add_attr file type failed\n", __func__);
goto end;
}
else if (html_node_add_attr(file, "name", "name"))
{
fprintf(stderr, "%s: html_node_add_attr file name failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "%s%s", fdir, name))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (html_node_add_attr(file, "value", d.str))
{
fprintf(stderr, "%s: html_node_add_attr file value failed\n", __func__);
goto end;
}
else if (html_node_add_attr(submit, "type", "submit"))
{
fprintf(stderr, "%s: html_node_add_attr submit type failed\n",
__func__);
goto end;
}
else if (html_node_add_attr(submit, "value", "Share"))
{
fprintf(stderr, "%s: html_node_add_attr submit value failed\n",
__func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static int add_element(struct html_node *const n, const char *const dir,
const char *const res, const char *const name)
{
int ret = -1;
enum {NAME, SIZE, DATE, COLUMNS};
enum {NAME, SIZE, DATE, SHARE, COLUMNS};
struct html_node *tr, *td[COLUMNS];
struct dynstr path;
const char *const sep = res[strlen(res) - 1] != '/' ? "/" : "";
@ -214,6 +292,11 @@ static int add_element(struct html_node *const n, const char *const dir,
fprintf(stderr, "%s: prepare_date failed\n", __func__);
goto end;
}
else if (prepare_share(td[SHARE], &sb, dir, name))
{
fprintf(stderr, "%s: prepare_date failed\n", __func__);
goto end;
}
ret = 0;
@ -892,6 +975,32 @@ int page_resource(struct http_response *const r, const char *const dir,
return -1;
}
int page_public(struct http_response *const r, const char *const res)
{
struct stat sb;
if (stat(res, &sb))
{
fprintf(stderr, "%s: stat(2) %s: %s\n",
__func__, res, strerror(errno));
if (errno == ENOENT)
return page_not_found(r);
else
return -1;
}
const mode_t m = sb.st_mode;
if (!S_ISREG(m))
{
fprintf(stderr, "%s: only regular files are supported\n", __func__);
return -1;
}
return serve_file(r, &sb, res);
}
int page_failed_login(struct http_response *const r)
{
static const char index[] =
@ -1036,3 +1145,78 @@ int page_style(struct http_response *const r)
return 0;
}
int page_share(struct http_response *const r, const char *const path)
{
int ret = -1;
struct dynstr out;
struct html_node *const html = html_node_alloc("html"),
*head, *a;
dynstr_init(&out);
if (!html)
{
fprintf(stderr, "%s: html_node_alloc failed\n", __func__);
goto end;
}
else if (!(head = html_node_add_child(html, "head")))
{
fprintf(stderr, "%s: html_node_add_child head failed\n", __func__);
goto end;
}
else if (common_head(head, NULL))
{
fprintf(stderr, "%s: common_head failed\n", __func__);
goto end;
}
else if (!(a = html_node_add_child(html, "a")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (html_node_add_attr(a, "href", path))
{
fprintf(stderr, "%s: html_node_add_attr href failed\n", __func__);
goto end;
}
else if (html_node_set_value(a, "Copy this link"))
{
fprintf(stderr, "%s: html_node_set_value href failed\n", __func__);
goto end;
}
else if (dynstr_append(&out, DOCTYPE_TAG))
{
fprintf(stderr, "%s: dynstr_prepend failed\n", __func__);
goto end;
}
else if (html_serialize(html, &out))
{
fprintf(stderr, "%s: html_serialize failed\n", __func__);
goto end;
}
*r = (const struct http_response)
{
.status = HTTP_STATUS_OK,
.buf.rw = out.str,
.n = out.len,
.free = free
};
if (http_response_add_header(r, "Content-Type", "text/html"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
goto end;
}
ret = 0;
end:
html_node_free(html);
if (ret)
dynstr_free(&out);
return ret;
}

2
page.h
View File

@ -15,5 +15,7 @@ int page_forbidden(struct http_response *r);
int page_bad_request(struct http_response *r);
int page_resource(struct http_response *r, const char *dir, const char *root,
const char *res, const struct page_quota *q);
int page_public(struct http_response *r, const char *res);
int page_share(struct http_response *r, const char *path);
#endif /* PAGE_H */