Tiny web frontend for mlmmj
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

604 lines
14 KiB

/*-
* Copyright (c) 2021 Baptiste Daroussin <bapt@FreeBSD.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer
* in this position and unchanged.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <syslog.h>
#include <unistd.h>
#include <syslog.h>
#include <dirent.h>
#include <kcgi.h>
#include <kcgihtml.h>
#include "config.h"
#ifndef NELEM
#define NELEM(array) (sizeof(array) / sizeof((array)[0]))
#endif
enum page {
PAGE_INDEX,
PAGE_SUBSCRIPTION,
PAGE_ACTION,
};
struct conf {
int tplfd;
int mlsfd;
int archivesfd;
};
struct template_arg {
struct khtmlreq req;
struct kreq *r;
struct conf *conf;
};
static const char *const pages[] = {
"index",
"subscription",
"action",
};
static enum khttp sanitise(const struct kreq *r, bool post)
{
if (r->page == NELEM(pages))
return (KHTTP_404);
else if (r->mime != KMIME_TEXT_HTML)
return (KHTTP_404);
else if (post && r->method != KMETHOD_POST)
return (KHTTP_405);
else if (!post && r->method != KMETHOD_GET)
return (KHTTP_405);
return KHTTP_200;
}
static struct kvalid key[] = {
{ kvalid_stringne, "ml" },
{ kvalid_stringne, "Unsubscribe" },
{ kvalid_stringne, "Subscribe" },
{ kvalid_stringne, "email" },
};
enum keys {
KEY_ML,
KEY_UNSUBSCRIBE,
KEY_SUBSCRIBE,
KEY_EMAIL,
};
const char *const templs [] = {
"mw_name",
"mw_version",
"mw_url",
"mw_mailinglists",
"mw_archived_mailinglists",
"ml_name",
};
enum templ {
TEMPL_NAME,
TEMPL_VERSION,
TEMPL_URL,
TEMPL_MAILINGLISTS,
TEMPL_ARCHIVED_MAILINGLISTS,
TEMPL_MLNAME,
};
static void list_lists(struct khtmlreq *req, struct conf *conf);
static void list_archived(struct khtmlreq *req, struct conf *conf);
static void
send_error(struct kreq *r, enum khttp er)
{
khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[er]);
khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_PLAIN]);
khttp_body(r);
if (KMIME_TEXT_HTML == r->mime)
khttp_puts(r, "Could not service request.");
khttp_free(r);
}
static void
redirect_to(struct kreq *r, const char *path)
{
khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_303]);
khttp_head(r, kresps[KRESP_LOCATION], "%s", path);
khttp_body(r);
}
char *
rtrimspace(char *buf)
{
char *cp = buf + strlen(buf) -1;
while (cp > buf && isspace(*cp)) {
*cp = 0;
cp --;
}
return (buf);
}
static int
write_index(size_t idx, void *arg)
{
struct template_arg *ta = arg;
switch (idx) {
case TEMPL_NAME:
khtml_puts(&ta->req, "mlmmj-webview");
break;
case TEMPL_URL:
khtml_puts(&ta->req, "https://codeberg.org/bapt/mlmmj-webview");
break;
case TEMPL_VERSION:
khtml_puts(&ta->req, VERSION""GITHASH);
break;
case TEMPL_MAILINGLISTS:
list_lists(&ta->req, ta->conf);
break;
case TEMPL_ARCHIVED_MAILINGLISTS:
list_archived(&ta->req, ta->conf);
break;
case TEMPL_MLNAME:
khtml_puts(&ta->req, ta->r->path);
break;
default:
khtml_puts(&ta->req, "UNKNOWN");
break;
}
return (1);
}
static int
alphasort_thunk(void *thunk, const void *p1, const void *p2)
{
int (*dc)(const struct dirent **, const struct dirent **);
dc = *(int (**)(const struct dirent **, const struct dirent **))thunk;
return (dc((const struct dirent **)p1, (const struct dirent **)p2));
}
static int
myscandirat(int dirfp, const char *dirname, struct dirent ***namelist,
int (*select)(const struct dirent *), int (*dcomp)(const struct dirent **,
const struct dirent **))
{
struct dirent *d, *p, **names = NULL;
size_t arraysz, numitems;
DIR *dirp;
int fd;
if ((fd = openat(dirfp, dirname, O_DIRECTORY)) == -1)
return (-1);
if ((dirp = fdopendir(fd)) == NULL)
return(-1);
numitems = 0;
arraysz = 32; /* initial estimate of the array size */
names = (struct dirent **)malloc(arraysz * sizeof(struct dirent *));
if (names == NULL)
goto fail;
while ((d = readdir(dirp)) != NULL) {
if (select != NULL && !select(d))
continue; /* just selected names */
/*
* Make a minimum size copy of the data
*/
p = (struct dirent *)malloc(_GENERIC_DIRSIZ(d));
if (p == NULL)
goto fail;
p->d_fileno = d->d_fileno;
p->d_type = d->d_type;
p->d_reclen = d->d_reclen;
p->d_namlen = d->d_namlen;
bcopy(d->d_name, p->d_name, p->d_namlen + 1);
/*
* Check to make sure the array has space left and
* realloc the maximum size.
*/
if (numitems >= arraysz) {
struct dirent **names2;
names2 = reallocarray(names, arraysz,
2 * sizeof(struct dirent *));
if (names2 == NULL) {
free(p);
goto fail;
}
names = names2;
arraysz *= 2;
}
names[numitems++] = p;
}
closedir(dirp);
if (numitems && dcomp != NULL)
qsort_r(names, numitems, sizeof(struct dirent *),
&dcomp, alphasort_thunk);
*namelist = names;
return (numitems);
fail:
while (numitems > 0)
free(names[--numitems]);
free(names);
closedir(dirp);
return (-1);
}
static int
directories(const struct dirent *dp)
{
if (dp->d_name[0] == '.')
return (0);
if (dp->d_type != DT_DIR)
return (0);
return (1);
}
static void
list_lists(struct khtmlreq *req, struct conf *conf)
{
char *line = NULL;
size_t linecap = 0;
ssize_t linelen;
struct dirent *dp, **ent;
int nents, i;
char *value;
nents = myscandirat(conf->mlsfd, ".", &ent, directories, alphasort);
for (i = 0; i < nents; i++) {
bool noarchive = false;
int tmpfd, fd;
FILE *f;
char *link;
dp = ent[i];
tmpfd = openat(conf->mlsfd, dp->d_name, O_DIRECTORY);
/* silently skip directoy we are not allowrd to open */
if (tmpfd == -1)
continue;
if (faccessat(tmpfd, "private", F_OK, AT_EACCESS) == 0) {
close(tmpfd);
continue;
}
/* We skip if we don't have description */
fd = openat(tmpfd, "desc", O_RDONLY);
if (fd == -1) {
close(tmpfd);
continue;
}
f = fdopen(fd, "r");
if (f == NULL) {
close(tmpfd);
close(fd);
continue;
}
/* We skip if we don't have archive */
if (faccessat(tmpfd, "noarchive", F_OK, AT_EACCESS) == 0) {
noarchive = true;
}
linelen = getline(&line, &linecap, f);
fclose(f);
close(tmpfd);
if (linelen <= 0)
continue;
value = line;
while (isspace(*value) && *value != '\0')
value++;
rtrimspace(value);
khtml_elem(req, KELEM_TR);
khtml_attr(req, KELEM_TD,
KATTR_CLASS, "ml",
KATTR__MAX);
asprintf(&link, "/subscription/%s", dp->d_name);
khtml_attr(req, KELEM_A,
KATTR_HREF, link,
KATTR__MAX);
free(link);
khtml_puts(req, dp->d_name);
khtml_closeelem(req, 2);
khtml_elem(req, KELEM_TD);
khtml_puts(req, value);
khtml_closeelem(req, 1);
khtml_elem(req, KELEM_TD);
if (!noarchive) {
asprintf(&link, "/archives/%s/", dp->d_name);
khtml_attr(req, KELEM_A,
KATTR_HREF, link,
KATTR__MAX);
free(link);
khtml_attr(req, KELEM_INPUT,
KATTR_TYPE, "button",
KATTR_VALUE, "archives",
KATTR__MAX);
khtml_closeelem(req, 2);
}
khtml_closeelem(req, 1);
}
free(line);
}
static void
list_archived(struct khtmlreq *req, struct conf *conf)
{
char *line = NULL;
size_t linecap = 0;
ssize_t linelen;
struct dirent *dp, **ent;
char *value;
int nents, i;
if (conf->archivesfd == -1) {
khtml_puts(req, "archived lists not configured");
return;
}
nents = myscandirat(conf->archivesfd, ".", &ent, directories, alphasort);
for (i = 0; i < nents; i++) {
int tmpfd, fd;
FILE *f;
char *link;
dp = ent[i];
tmpfd = openat(conf->archivesfd, dp->d_name, O_DIRECTORY);
/* silently skip directoy we are not allowrd to open */
if (tmpfd == -1)
continue;
if (faccessat(tmpfd, "private", F_OK, AT_EACCESS) == 0) {
close(tmpfd);
continue;
}
/* We skip if we don't have description */
fd = openat(tmpfd, "desc", O_RDONLY);
if (fd == -1) {
close(tmpfd);
continue;
}
f = fdopen(fd, "r");
if (f == NULL) {
close(tmpfd);
close(fd);
continue;
}
linelen = getline(&line, &linecap, f);
fclose(f);
close(tmpfd);
if (linelen <= 0)
continue;
value = line;
while (isspace(*value) && *value != '\0')
value++;
rtrimspace(value);
khtml_elem(req, KELEM_TR);
khtml_attr(req, KELEM_TD,
KATTR_CLASS, "ml",
KATTR__MAX);
khtml_puts(req, dp->d_name);
khtml_closeelem(req, 1);
khtml_elem(req, KELEM_TD);
khtml_puts(req, value);
khtml_closeelem(req, 1);
khtml_elem(req, KELEM_TD);
asprintf(&link, "/archives/%s/", dp->d_name);
khtml_attr(req, KELEM_A,
KATTR_HREF, link,
KATTR__MAX);
free(link);
khtml_attr(req, KELEM_INPUT,
KATTR_TYPE, "button",
KATTR_VALUE, "archives",
KATTR__MAX);
khtml_closeelem(req, 3);
}
free(line);
}
bool
extract_check(struct kreq *r)
{
switch (r->page) {
case PAGE_SUBSCRIPTION:
if (r->path[0] == '\0') {
redirect_to(r, "/");
khttp_free(r);
return (false);
}
if (strchr(r->path, '/') != NULL) {
redirect_to(r, "/");
khttp_free(r);
return (false);
}
break;
}
return (true);
}
void
show_index(struct kreq *r, struct conf *conf)
{
struct template_arg ta;
struct ktemplate t = {
.key = templs,
.keysz = NELEM(templs),
.cb = write_index,
};
if (!extract_check(r))
return;
int fd = openat(conf->tplfd, pages[r->page], O_RDONLY);
if (fd == -1) {
syslog(LOG_ERR, "Impossible to open template '%s': %s", pages[r->page], strerror(errno));
send_error(r, KHTTP_500);
return;
}
ta.conf = conf;
ta.r = r;
khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[r->mime]);
khttp_body(r);
khtml_open(&ta.req, r, KHTML_PRETTY);
t.arg = &ta;
khttp_template_fd(r, &t, fd, pages[r->page]);
khttp_free(r);
}
void
store_tempfile(char *tpl, const char *ml, const char *email)
{
int fd = mkstemp(tpl);
if (fd == -1) {
syslog(LOG_ERR, "%s: %s", strerror(errno), tpl);
return;
}
dprintf(fd, "%s\n%s\n", ml, email);
chmod(tpl, 0644);
}
void
show_action(struct kreq *r)
{
struct khtmlreq req;
char directory[MAXPATHLEN];
directory[0] = '\0';
/* Never show an error, to avoid anyone to guess emails or setup */
khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[r->mime]);
khttp_body(r);
khtml_open(&req, r, KHTML_PRETTY);
khtml_elem(&req, KELEM_DOCTYPE);
khtml_elem(&req, KELEM_HTML);
khtml_elem(&req, KELEM_HEAD);
khtml_attr(&req, KELEM_META,
KATTR_HTTP_EQUIV, "refresh",
KATTR_CONTENT, "3, url=\"/\"",
KATTR__MAX);
khtml_attr(&req, KELEM_META,
KATTR_CHARSET, "utf-8", KATTR__MAX);
khtml_attr(&req, KELEM_META,
KATTR_NAME, "viewport",
KATTR_CONTENT, "width=device-width, initial-scale=1",
KATTR__MAX);
khtml_elem(&req, KELEM_TITLE);
khtml_puts(&req, "Mailing lists");
khtml_closeelem(&req, 2);
if (r->fieldmap[KEY_SUBSCRIBE] != NULL)
strlcat(directory, "/var/spool/mlmmj-webview/subscribe/req.XXXXXXX", MAXPATHLEN);
else if (r->fieldmap[KEY_UNSUBSCRIBE] != NULL)
strlcat(directory, "/var/spool/mlmmj-webview/unsubscribe/req.XXXXXXX", MAXPATHLEN);
store_tempfile(directory, r->fieldmap[KEY_ML]->parsed.s,
r->fieldmap[KEY_EMAIL]->parsed.s);
khtml_puts(&req, "Your request has been taken in account, you should "
"receive soon a confirmation email");
khttp_free(r);
}
int
main(int argc __unused, char **argv __unused)
{
struct kreq r;
struct conf conf;
enum khttp er;
const char *temp;
if ((temp = getenv("MLMMJ_WEBVIEW_TEMPLATES")) == NULL) {
syslog(LOG_ERR, "MLMMJ_WEBVIEW_TEMPLATES not defined");
return (EXIT_FAILURE);
}
if ((conf.tplfd = open(temp, O_DIRECTORY)) == -1) {
syslog(LOG_ERR, "impossible to open the template directory %s", strerror(errno));
return (EXIT_FAILURE);
}
if ((temp = getenv("MLMMJ_WEBVIEW_MAILING_LISTS")) == NULL) {
syslog(LOG_ERR, "MLMMJ_WEBVIEW_MAILING_LISTS not defined");
return (EXIT_FAILURE);
}
if ((conf.mlsfd = open(temp, O_DIRECTORY)) == -1) {
syslog(LOG_ERR, "impossible to open the mailing list directory %s", strerror(errno));
return (EXIT_FAILURE);
}
conf.archivesfd = -1;
if ((temp = getenv("MLMMJ_WEBVIEW_ARCHIVED_MAILING_LISTS")) != NULL) {
if ((conf.archivesfd = open(temp, O_DIRECTORY)) == -1) {
syslog(LOG_ERR, "impossible to open the archived mailing list directory %s", strerror(errno));
return (EXIT_FAILURE);
}
}
if (khttp_parse(&r, key, NELEM(key), pages, NELEM(pages), PAGE_INDEX))
return (EXIT_FAILURE);
switch (r.page) {
case PAGE_INDEX:
case PAGE_SUBSCRIPTION:
if ((er = sanitise(&r, false)) != KHTTP_200) {
send_error(&r, er);
return (EXIT_SUCCESS);
}
show_index(&r, &conf);
break;
case PAGE_ACTION:
if ((er = sanitise(&r, true)) != KHTTP_200) {
send_error(&r, er);
return (EXIT_SUCCESS);
}
show_action(&r);
break;
default:
send_error(&r, KHTTP_404);
return (EXIT_SUCCESS);
}
if (conf.tplfd != -1)
close(conf.tplfd);
if (conf.mlsfd != -1)
close(conf.mlsfd);
return (EXIT_SUCCESS);
}