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.

1411 lines
38 KiB

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <err.h>
#include <errno.h>
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <regex.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "dpi.h"
#include "io.h"
#include "sundown/markdown.h"
#include "sundown/html.h"
#include "sundown/buffer.h"
#include "sundown/houdini.h"
static const size_t sigil_size = sizeof("§")-1;
static const int id_length = 16;
static const char ssb_ref_base[] = "http://localhost:8027/";
static const char ssb_blob_base[] = "ssbget:";
static const char ssb_channel_base[] = "http://localhost:8027/channel/";
static const int max_cols = 54;
static char server_hostname[256];
static char server_url[256];
static char zet_dir[_POSIX_PATH_MAX-64];
static bool seeded = false;
enum http_method {
HTTP_METHOD_GET,
HTTP_METHOD_HEAD,
HTTP_METHOD_POST,
HTTP_METHOD_OTHER
};
struct http_request {
enum http_method method;
char *path;
char *query;
char *hash;
char *http_version;
char *host;
char *referer;
ssize_t content_length;
char *content_type;
char *multipart_boundary;
int fd;
char *data, *full_data;
size_t len;
bool add_new_id;
};
struct zet_search {
DIR *dir;
const char *id;
regex_t regex;
bool has_regex;
};
struct zet_mtime {
char *id;
int mtime;
};
struct zet_renderopt {
struct html_renderopt html;
const char *query;
};
enum page {
PAGE_NEW,
PAGE_ALL,
PAGE_RECENT,
PAGE_ZET,
PAGE_SEARCH,
};
static int count_lines_in(const char *str) {
int count = 1;
char c;
int col = 0;
while ((c = *str++)) {
if (c == '\n' || col++ > max_cols) {
count++;
col = 0;
}
}
return count;
}
static int count_lines(int fd) {
int count = 1;
if (fd >= 0) do {
unsigned char buf[4096];
size_t len = sizeof(buf)-1;
int rc = read_some(fd, buf, &len);
if (rc < 0) {
if (errno == EPIPE) break;
return -1;
}
buf[len] = '\0';
count += count_lines_in((char *)buf);
} while (1);
return count;
}
static int write_html_char(int fd, char c) {
switch (c) {
case '&': return write_buf(fd, "&amp;");
case '<': return write_buf(fd, "&lt;");
case '>': return write_buf(fd, "&gt;");
case '"': return write_buf(fd, "&quot;");
}
return write_char(fd, c);
}
static void write_err(int fd, const char *subject) {
int rc = dprintf(fd,
"\n<strong>Error</strong>: %s: %s\n",
subject,
strerror(errno));
if (rc < 0) warnx("dprintf");
}
static int write_html(int fd, const char *buf, size_t len) {
while (len > 0) {
// note: buf must null-terminated
char *next = strpbrk(buf, "&<>\"");
if (next == NULL) return write_all(fd, (unsigned char *)buf, len);
char c = *next;
size_t sublen = next - buf;
int rc = write_all(fd, (unsigned char *)buf, sublen);
if (rc < 0) return -1;
buf += sublen + 1;
len -= sublen + 1;
rc = write_html_char(fd, c);
if (rc < 0) return -1;
}
return 0;
}
static int write_zet_link(int fd, const char *id, const char *title) {
if (title == NULL) {
return dprintf(fd, " <a href=\"%s\"><strong><code>§%s</code></strong></a>", id, id);
}
int rc = dprintf(fd, " <a href=\"%s\">[<strong>", id);
rc |= write_html(fd, title, strlen(title));
rc |= dprintf(fd, "</strong>](<code>§%s</code>)</a>", id);
return rc;
}
static int write_zet_link_btn(int fd, const char *id, const char *title) {
int rc = 0;
rc |= dprintf(fd, "<form action=\"%s\" method=get>"
"<input type=submit value=\"&rarr;\">", id);
rc |= write_zet_link(fd, id, title);
rc |= write_buf(fd, "</form>\n");
return rc;
}
static int write_topbar(int fd, enum page page, const char *id, const char *title) {
int rc = write_buf(fd,
"<table style=\"width:100%; border-collapse:collapse\"><tr>"
"<td style=\"padding:0\">"
"<form action=\"search\">");
rc |= write_buf(fd, " ");
if (page == PAGE_ALL) rc |= write_buf(fd, "<strong>");
rc |= write_buf(fd, "<a href=\"all\">All</a>");
if (page == PAGE_ALL) rc |= write_buf(fd, "</strong>");
rc |= write_buf(fd, " ");
if (page == PAGE_RECENT) rc |= write_buf(fd, "<strong>");
rc |= write_buf(fd, "<a href=\"recent\">Recent</a>");
if (page == PAGE_RECENT) rc |= write_buf(fd, "</strong>");
rc |= write_buf(fd, " ");
if (page == PAGE_NEW) rc |= write_buf(fd, "<strong>");
rc |= write_buf(fd, "<a href=\"new\">New</a>");
if (page == PAGE_NEW) rc |= write_buf(fd, "</strong>");
rc |= write_buf(fd, " <input name=q placeholder=Search");
if (page == PAGE_SEARCH) {
rc |= write_buf(fd, " value=\"");
rc |= write_html(fd, id, strlen(id));
rc |= write_buf(fd, "\"");
}
rc |= write_buf(fd, ">");
if (page == PAGE_ZET) {
rc |= write_zet_link(fd, id, title);
rc |= write_buf(fd, "</form></td>");
rc |= write_buf(fd, "<td style=\"text-align:right; padding:0;\">"
"<a href=\"?print\">print</a></td>");
} else {
rc |= write_buf(fd, "</form></td>");
}
rc |= write_buf(fd, "</tr></table>\n");
return rc;
}
static int passthrough_note_html(int fd, int note_fd) {
int rc;
if (note_fd >= 0) do {
unsigned char buf[4096];
size_t len = sizeof(buf)-1;
rc = read_some(note_fd, buf, &len);
if (rc < 0) {
if (errno != EPIPE) dprintf(fd,
"\n<strong>Error</strong>: %s", strerror(errno));
break;
}
buf[len] = '\0';
rc = write_html(fd, (char *)buf, len);
if (rc < 0) warn("write_html");
} while (rc == 0);
return 0;
}
static int dpi_respond_err(int fd, const char *fmt, ...) {
int rc;
va_list ap;
int err = errno;
va_start(ap, fmt);
rc = dpi_send_header(fd, "text/plain");
rc |= vdprintf(fd, fmt, ap);
rc |= dprintf(fd, ": %s", strerror(err));
va_end(ap);
rc |= close(fd);
return rc;
}
static int html_error(int fd, const char *fmt, ...) {
int rc;
va_list ap;
int err = errno;
va_start(ap, fmt);
rc = write_buf(fd, "\n<strong>Error</strong>:\n");
rc |= vdprintf(fd, fmt, ap);
if (err != 0) rc |= dprintf(fd, ": %s", strerror(err));
if (rc < 0) warn("dprintf");
va_end(ap);
if (close(fd) < 0) warn("close");
return 0;
}
static char *zet_get_title_fd(int fd, char *buf, size_t len) {
if (fd < 0) return NULL;
int rc = read_some(fd, (unsigned char *)buf, &len);
if (rc < 0) return NULL;
buf[len] = '\0';
char *end = strchr(buf, '\n');
if (end != NULL) *end = '\0';
while (*buf == '#') buf++;
while (*buf == ' ') buf++;
return buf;
}
static char *zet_get_title(const char *id, char *buf, size_t len) {
char path_buf[_POSIX_PATH_MAX];
ssize_t sz = snprintf(path_buf, sizeof(path_buf), "%s/%s", zet_dir, id);
if (sz < 0 || (size_t)sz >= sizeof(path_buf)) return NULL;
int fd = open(path_buf, O_RDONLY);
if (fd < 0) return NULL;
char *title_buf = zet_get_title_fd(fd, buf, len);
if (title_buf == NULL || *title_buf == '\0') {
strncpy(buf, "§", sigil_size);
strncpy(buf + sigil_size, id, len);
title_buf = buf;
}
close(fd);
return title_buf;
}
static int zet_stat(const char *id, struct stat *st) {
char path_buf[_POSIX_PATH_MAX];
ssize_t sz = snprintf(path_buf, sizeof(path_buf), "%s/%s", zet_dir, id);
if (sz < 0 || (size_t)sz >= sizeof(path_buf)) return -1;
return stat(path_buf, st);
}
static char *zet_get_buf_title(const struct buf *link, char *buf, size_t len) {
char id_buf[128];
strncpy(id_buf, (char *)link->data, link->size);
id_buf[sizeof(id_buf)-1] = '\0';
return zet_get_title(id_buf + sigil_size, buf, len);
}
static int zet_links_in(const char *str, const char *id_to) {
while (1) {
str = strstr(str, "§");
if (str == NULL) return 0;
str += sigil_size;
if (!strncmp(str, id_to, id_length)) return 1;
}
return 0;
}
static int zet_links_to(const char *id_from, const char *id_to) {
// TODO: index links, so that every source file doesn't have to be read all
// the time
int rc;
char path_buf[_POSIX_PATH_MAX];
ssize_t sz = snprintf(path_buf, sizeof(path_buf), "%s/%s", zet_dir, id_from);
if (sz < 0 || (size_t)sz >= sizeof(path_buf)) return -1;
int fd = open(path_buf, O_RDONLY);
if (fd < 0) return -1;
size_t fringe = 0;
size_t link_len = strlen(id_to) + id_length;
unsigned char buf[4096];
do {
size_t len = sizeof(buf)-fringe-1;
rc = read_some(fd, buf + fringe, &len);
if (rc < 0) {
if (errno != EPIPE) { close(fd); return -1; }
close(fd);
return 0;
}
buf[len - fringe] = '\0';
rc = zet_links_in((char *)buf, id_to);
if (rc < 0) { close(fd); return -1; }
// in case a link is across the boundary between this buf and the next,
// keep some "fringe" of the end of this buf in the beginning of the
// next one
if (len > link_len) {
fringe = link_len;
memcpy(buf, buf + len - fringe, fringe);
}
} while (rc == 0);
close(fd);
return rc;
}
static int zet_matches_regex(const char *id, regex_t *regex) {
char path_buf[_POSIX_PATH_MAX];
ssize_t sz = snprintf(path_buf, sizeof(path_buf), "%s/%s", zet_dir, id);
if (sz < 0 || (size_t)sz >= sizeof(path_buf)) return -1;
int note_fd = open(path_buf, O_RDONLY);
if (note_fd < 0) return -1;
char *text = read_full(note_fd);
bool found = regexec(regex, text, 0, NULL, 0) == 0;
free(text);
return found ? 1 : 0;
}
static int zet_search_init_query(struct zet_search *zs, const char *query, char *errbuf, size_t errbuf_size) {
int rc = regcomp(&zs->regex, query, REG_NOSUB | REG_EXTENDED | REG_ICASE);
if (rc != 0) {
size_t sz = regerror(rc, &zs->regex, errbuf, errbuf_size);
if (sz > errbuf_size-1 && errbuf_size >= 4) {
errbuf[errbuf_size-2] = '.';
errbuf[errbuf_size-3] = '.';
errbuf[errbuf_size-4] = '.';
}
return -1;
}
return 0;
}
static int zet_search_start(struct zet_search *zs, const char *id, const char *query, char *errbuf, size_t errbuf_size) {
zs->dir = opendir(zet_dir);
if (zs->dir == NULL) return -1;
zs->id = id;
if (query != NULL) {
zs->has_regex = true;
return zet_search_init_query(zs, query, errbuf, errbuf_size);
} else {
zs->has_regex = false;
return 0;
}
}
static int zet_search_next(struct zet_search *zs, const char **idp) {
int rc;
struct dirent *ent;
const char *id;
const char *dest_id = zs->id;
while (1) {
errno = 0;
ent = readdir(zs->dir);
if (ent == NULL) {
if (errno != 0) return -1;
id = NULL;
break;
}
id = ent->d_name;
if (id[0] == '.') continue;
if (strpbrk(id, "<>&\"~") != NULL) continue;
if (dest_id != NULL) {
rc = zet_links_to(id, dest_id);
if (rc < 0) return -1;
if (rc != 1) continue;
}
if (zs->has_regex) {
rc = zet_matches_regex(id, &zs->regex);
if (rc < 0) return -1;
if (rc != 1) continue;
}
break;
};
*idp = id;
return 0;
}
static int zet_search_close(struct zet_search *sz) {
if (sz->has_regex) regfree(&sz->regex);
return closedir(sz->dir);
}
static int dpi_serve_zet_all(int fd) {
int rc;
struct zet_search zs;
rc = dpi_send_header(fd, "text/html");
if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; }
rc = dprintf(fd,
"<!doctype html><html><head>"
"<title>Notes - All</title>"
"<meta charset=utf-8>"
"</head>"
"<body style=\"margin:0\">\n");
rc |= write_topbar(fd, PAGE_ALL, NULL, NULL);
if (rc < 0) { warn("write"); close(fd); return 0; }
char errbuf[LINE_MAX] = "";
rc = zet_search_start(&zs, NULL, NULL, errbuf, sizeof(errbuf));
if (rc < 0) return html_error(fd, "Unable to list: %s", errbuf);
rc = write_buf(fd, "<ul>");
if (rc < 0) { warn("write"); close(fd); return 0; }
const char *id;
while (1) {
rc = zet_search_next(&zs, &id);
if (rc < 0) return html_error(fd, "Unable to search");
if (id == NULL) break;
char title_buf[128];
char *title = zet_get_title(id, title_buf, sizeof(title_buf));
if (title == NULL) {
warn("zet_get_title");
title = title_buf;
strncpy(title, id, sizeof(title)-1);
title[sizeof(title)-1] = '\0';
return 0;
}
rc = dprintf(fd, "<li><a href=\"%s\">", id);
rc |= write_html(fd, title, strlen(title));
rc |= write_buf(fd, "</a></li>");
if (rc < 0) { warnx("write"); close(fd); return 0; }
}
rc = dprintf(fd, "</ul></body></html>");
if (rc < 0) { warn("dprintf"); close(fd); return 0; }
rc = zet_search_close(&zs);
if (rc < 0) warn("closedir");
close(fd);
return 0;
}
static int cmp_zet_mtimes(const void *ptr1, const void *ptr2) {
const struct zet_mtime *zet_mtime1 = ptr1;
const struct zet_mtime *zet_mtime2 = ptr2;
return zet_mtime2->mtime - zet_mtime1->mtime;
}
static int dpi_serve_zet_recent(int fd) {
int rc, i;
struct zet_search zs;
int capacity = 0;
int count = 0;
struct zet_mtime *zets = NULL;
rc = dpi_send_header(fd, "text/html");
if (rc < 0) { warn("dpi_send_header"); goto cleanup; }
rc = dprintf(fd,
"<!doctype html><html><head>"
"<title>Notes - Recent</title>"
"<meta charset=utf-8>"
"</head>"
"<body style=\"margin:0\">\n");
rc |= write_topbar(fd, PAGE_RECENT, NULL, NULL);
if (rc < 0) { warn("write"); goto cleanup; }
char errbuf[LINE_MAX] = "";
rc = zet_search_start(&zs, NULL, NULL, errbuf, sizeof(errbuf));
if (rc < 0) return html_error(fd, "Unable to list: %s", errbuf);
rc = write_buf(fd, "<ul>");
if (rc < 0) { warn("write"); goto cleanup; }
const char *id;
for (i = 0;; i++) {
struct stat st;
rc = zet_search_next(&zs, &id);
if (rc == 0 && id == NULL) break;
if (rc == 0) rc = zet_stat(id, &st);
if (rc == 0 && i >= capacity) {
if (capacity == 0) capacity = 256;
else capacity *= 2;
void *new_zets = reallocarray(zets, capacity, sizeof *zets);
if (new_zets == NULL) rc = -1;
else zets = new_zets;
}
if (rc < 0) {
write_err(fd, "list");
goto cleanup;
}
zets[i].mtime = st.st_mtime;
zets[i].id = strdup(id);
count++;
}
qsort(zets, count, sizeof(*zets), cmp_zet_mtimes);
static int limit = 50;
if (limit > count) limit = count;
for (i = 0; i < limit; i++) {
id = zets[i].id;
char title_buf[128];
char *title = zet_get_title(id, title_buf, sizeof(title_buf));
if (title == NULL) {
write_err(fd, "get_title");
goto cleanup;
}
rc = dprintf(fd, "<li><a href=\"%s\">", id);
rc |= write_html(fd, title, strlen(title));
rc |= write_buf(fd, "</a></li>");
if (rc < 0) { warnx("write"); goto cleanup; }
}
rc = dprintf(fd, "</ul></body></html>");
if (rc < 0) warn("dprintf");
cleanup:
for (i = 0; i < count; i++) {
free(zets[i].id);
}
free(zets);
rc = zet_search_close(&zs);
if (rc < 0) warn("closedir");
close(fd);
return 0;
}
static void unescape(char *buf, size_t len, const char *str) {
struct buf *ob = bufnew(1024);
houdini_unescape_url(ob, (unsigned char *)str, strlen(str));
if (ob->size < len) len = ob->size;
strncpy(buf, (char *)ob->data, len);
buf[len] = '\0';
bufrelease(ob);
}
static int dpi_serve_zet_search(int fd, char *qs) {
int rc;
struct zet_search zs;
char *query = NULL, query_unescaped[1024];
char *name = qs, *next_name;
while (name != NULL) {
char *value = strchr(name, '=');
if (value != NULL) *value++ = '\0';
next_name = strchr(value != NULL ? value : name, '&');
if (next_name != NULL) *next_name++ = '\0';
if (!strcmp(name, "q")) query = value;
name = next_name;
}
if (query != NULL) {
unescape(query_unescaped, sizeof(query_unescaped), query);
query = query_unescaped;
}
rc = dpi_send_header(fd, "text/html");
if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; }
rc = dprintf(fd,
"<!doctype html><html><head>"
"<title>Notes</title>"
"<meta charset=utf-8>"
"</head>"
"<body style=\"margin:0\">\n"
);
rc |= write_topbar(fd, PAGE_SEARCH, query, NULL);
if (rc < 0) { warn("write"); close(fd); return 0; }
char errbuf[LINE_MAX] = "";
rc = zet_search_start(&zs, NULL, query, errbuf, sizeof(errbuf));
if (rc < 0) return html_error(fd, "Unable to list: %s", errbuf);
rc = write_buf(fd, "<ul>");
if (rc < 0) { warn("write"); close(fd); return 0; }
const char *id;
while (1) {
rc = zet_search_next(&zs, &id);
if (rc < 0) return html_error(fd, "Unable to search");
if (id == NULL) break;
char title_buf[128];
char *title = zet_get_title(id, title_buf, sizeof(title_buf));
if (title == NULL) {
warn("zet_get_title");
title = title_buf;
strncpy(title, id, sizeof(title)-1);
title[sizeof(title)-1] = '\0';
return 0;
}
rc = write_buf(fd, "<li>");
rc |= write_zet_link_btn(fd, id, title);
rc |= write_buf(fd, "</li>");
if (rc < 0) { warnx("dprintf/write_html/write_buf"); close(fd); return 0; }
}
rc = dprintf(fd, "</ul></body></html>");
if (rc < 0) { warn("dprintf"); close(fd); return 0; }
rc = zet_search_close(&zs);
if (rc < 0) warn("closedir");
close(fd);
return 0;
}
static int dpi_serve_not_found(int fd) {
int rc = dpi_send_header(fd, "text/html");
if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; }
write_buf(fd, "Not Found");
close(fd);
return 0;
}
static int dpi_serve_zet_new(int fd) {
int rc = dpi_send_header(fd, "text/html");
if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; }
dprintf(fd,
"<!doctype html><html><head>"
"<title>New Note</title>"
"<meta charset=utf-8>"
"</head>"
"<body style=\"margin:0\">\n");
write_topbar(fd, PAGE_NEW, NULL, NULL);
dprintf(fd,
"<table style=\"width:100%%; height:100%%; border-collapse:collapse\"><tr>"
"<td><form method=post action=\"%snew\" enctype=\"multipart/form-data\">"
"<textarea name=text cols=64 rows=8 style=\"width:50%%\"></textarea><br>"
"<input type=submit value=\"Save\">"
"</form></td></tr></form></html>"
, server_url);
close(fd);
return 0;
}
static void parse_req_uri(char *path, char **hashp, char **queryp) {
char *hash = strchr(path, '#');
if (hash != NULL) *hash++ = '\0';
*hashp = hash;
char *query = strchr(path, '?');
if (query != NULL) *query++ = '\0';
*queryp = query;
}
static int
md_rndr_link(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *content, void *opaque)
{
struct zet_renderopt *options = opaque;
if (link != NULL && (options->html.flags & HTML_SAFELINK) != 0 && !sd_autolink_issafe(link->data, link->size))
return 0;
BUFPUTSL(ob, "<a href=\"");
if (link && link->size > sigil_size) {
if (link->data[0] == '#') {
BUFPUTSL(ob, ssb_channel_base);
houdini_escape_href(ob, link->data + 1, link->size - 1);
} else if (link->data[0] == '#' || link->data[0] == '@'
|| link->data[0] == '&' || link->data[0] == '%') {
BUFPUTSL(ob, ssb_ref_base);
houdini_escape_href(ob, link->data, link->size);
} else if (!strncmp((char *)link->data, "§", sigil_size)) {
houdini_escape_href(ob, link->data + sigil_size,
link->size - sigil_size);
const char *query = options->query;
if (query != NULL) {
bufputc(ob, '?');
bufputs(ob, query);
}
} else
houdini_escape_href(ob, link->data, link->size);
}
if (title && title->size) {
BUFPUTSL(ob, "\" title=\"");
houdini_escape_html0(ob, title->data, title->size, 0);
}
if (options->html.link_attributes) {
bufputc(ob, '\"');
options->html.link_attributes(ob, link, opaque);
bufputc(ob, '>');
} else {
BUFPUTSL(ob, "\">");
}
if (content && content->size) bufput(ob, content->data, content->size);
BUFPUTSL(ob, "</a>");
return 1;
}
static int
md_rndr_image(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *alt, void *opaque)
{
struct zet_renderopt *options = opaque;
if (!link || !link->size) return 0;
BUFPUTSL(ob, "<img src=\"");
BUFPUTSL(ob, ssb_blob_base);
houdini_escape_href(ob, link->data, link->size);
BUFPUTSL(ob, "\" alt=\"");
if (alt && alt->size)
houdini_escape_html0(ob, alt->data, alt->size, 0);
if (title && title->size) {
BUFPUTSL(ob, "\" title=\"");
houdini_escape_html0(ob, title->data, title->size, 0);
}
(void)options;
bufputs(ob, "\"/>");
return 1;
}
static void put_link_truncated(struct buf *ob, const struct buf *link, int max_length) {
char *buf = (char *)link->data;
size_t len = link->size;
if (!strncmp(buf, "http", 4)) {
if (!strncmp(buf + 4, "://", 3)) {
buf += 7;
len -= 7;
}
if (!strncmp(buf + 4, "s://", 4)) {
buf += 8;
len -= 8;
}
}
if (len < (size_t)max_length) {
houdini_escape_html0(ob, (unsigned char *)buf, len, 0);
return;
}
houdini_escape_html0(ob, (unsigned char *)buf, max_length, 0);
BUFPUTSL(ob, "");
}
static int
md_rndr_autolink(struct buf *ob, const struct buf *link, enum mkd_autolink type, void *opaque)
{
struct zet_renderopt *options = opaque;
if (!link || !link->size)
return 0;
if ((options->html.flags & HTML_SAFELINK) != 0 &&
!sd_autolink_issafe(link->data, link->size) &&
type != MKDA_EMAIL)
return 0;
BUFPUTSL(ob, "<a href=\"");
if (type == MKDA_EMAIL)
BUFPUTSL(ob, "mailto:");
if (type == MKDA_ZET)
houdini_escape_href(ob, link->data + sigil_size, link->size - sigil_size);
else if (type == MKDA_SSB) {
if (link->data[0] == '#') {
BUFPUTSL(ob, ssb_channel_base);
houdini_escape_href(ob, link->data + 1, link->size - 1);
} else {
BUFPUTSL(ob, ssb_ref_base);
houdini_escape_href(ob, link->data, link->size);
}
} else
houdini_escape_href(ob, link->data, link->size);
if (type == MKDA_ZET) {
char title_buf[128];
char *title = zet_get_buf_title(link, title_buf, sizeof(title_buf));
if (title != NULL) {
BUFPUTSL(ob, "\" title=\"");
houdini_escape_html0(ob, (unsigned char *)title, strlen(title), 0);
}
}
if (options->html.link_attributes) {
bufputc(ob, '\"');
options->html.link_attributes(ob, link, opaque);
bufputc(ob, '>');
} else {
BUFPUTSL(ob, "\">");
}
/*
* Pretty printing: if we get an email address as
* an actual URI, e.g. `mailto:foo@bar.com`, we don't
* want to print the `mailto:` prefix
*/
if (bufprefix(link, "mailto:") == 0) {
houdini_escape_html0(ob, link->data + 7, link->size - 7, 0);
} else if (link->data[0] == '&' || link->data[0] == '@' || link->data[0] == '%') {
put_link_truncated(ob, link, 8);
} else if (!strncmp((char *)link->data, "§", sigil_size)) {
put_link_truncated(ob, link, 9);
} else {
put_link_truncated(ob, link, max_cols);
}
BUFPUTSL(ob, "</a>");
return 1;
}
static int dpi_serve_zet(int fd, char *path) {
int rc = 0;
char *hash, *query;
parse_req_uri(path, &hash, &query);
if (!strcmp(path, "/")) return dpi_serve_zet_all(fd);
if (!strcmp(path, "/all")) return dpi_serve_zet_all(fd);
if (!strcmp(path, "/recent")) return dpi_serve_zet_recent(fd);
if (!strcmp(path, "/search")) return dpi_serve_zet_search(fd, query);
if (!strcmp(path, "/new")) return dpi_serve_zet_new(fd);
char path_buf[_POSIX_PATH_MAX];
char *id = path+1;
ssize_t sz = snprintf(path_buf, sizeof(path_buf), "%s/%s", zet_dir, id);
if (sz < 0 || (size_t)sz >= sizeof(path_buf)) return dpi_respond_err(fd, "read_file");
int note_fd = open(path_buf, O_RDONLY);
if (note_fd < 0 && errno != ENOENT) return dpi_respond_err(fd, "open");
int rows = count_lines(note_fd);
if (rows < 0) return dpi_respond_err(fd, "count_lines");
static const int max_rows = 57, min_rows = 8;
if (rows > max_rows) rows = max_rows;
else if (rows < min_rows) rows = min_rows;
if (note_fd >= 0) rc = lseek(note_fd, 0, SEEK_SET);
if (rc < 0) return dpi_respond_err(fd, "lseek");
char buf[128];
char *title = zet_get_title_fd(note_fd, buf, sizeof(buf));
if (title == NULL) title = id;
if (note_fd >= 0) rc = lseek(note_fd, 0, SEEK_SET);
if (rc < 0) return dpi_respond_err(fd, "lseek");
rc = dpi_send_header(fd, "text/html");
rc |= dprintf(fd,
"<!doctype html><head>"
"<title>");
if (title != NULL) rc |= write_html(fd, title, strlen(title));
rc |= dprintf(fd,
"</title>"
"<meta charset=utf-8>"
"<style>"
"pre { white-space: pre-wrap; }"
"td { padding: 0; }"
"</style>"
"</head>");
// TODO: iterate
bool print = query != NULL && strncmp(query, "print", 5) == 0
&& (query[5] == '\0' || query[5] == '&' ||
(query[5] == '=' && !(query[6] == '\0' || query[6] == '&')));
if (!print) {
rc |= write_buf(fd, "<body style=\"margin:0\">\n");
rc |= write_topbar(fd, PAGE_ZET, id, title);
rc |= dprintf(fd,
"<table style=\"width:100%%; height:100%%; border-collapse:collapse\"><tr>"
"<td style=\"width:50%%\"><form method=post action=\"%s%s\" enctype=\"multipart/form-data\" style=\"height:100%%\">"
"<textarea name=text cols=64 rows=%d style=\"width:100%%\">\n"
, server_url, id, rows);
rc = passthrough_note_html(fd, note_fd);
if (rc < 0) { warnx("passthrough_note_html"); close(fd); return 0; }
rc = dprintf(fd, "</textarea><br>"
"<input type=submit value=\"Save\">"
" <input type=submit name=add_new_id value=\"Add New ID\">"
"</form></td>"
"<td>"
);
if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; }
if (note_fd >= 0) rc = lseek(note_fd, 0, SEEK_SET);
if (rc < 0) return html_error(fd, "Unable to read file");
} else {
rc |= write_buf(fd, "<body>");
}
char *text = note_fd >= 0 ? read_full(note_fd) : NULL;
if (text == NULL) return html_error(fd, "Unable to read file");
struct sd_callbacks callbacks;
struct zet_renderopt options;
struct sd_markdown *markdown;
struct buf *ob = bufnew(128);
sdhtml_renderer(&callbacks, &options.html, HTML_ESCAPE);
options.query = query;
callbacks.link = md_rndr_link;
callbacks.image = md_rndr_image;
callbacks.autolink = md_rndr_autolink;
static const unsigned int extensions =
MKDEXT_NO_INTRA_EMPHASIS |
MKDEXT_TABLES |
MKDEXT_FENCED_CODE |
MKDEXT_AUTOLINK |
MKDEXT_STRIKETHROUGH |
MKDEXT_SPACE_HEADERS |
MKDEXT_LAX_SPACING;
markdown = sd_markdown_new(extensions, 16, &callbacks, &options);
sd_markdown_render(ob, (unsigned char *)text, strlen(text), markdown);
free(text);
sd_markdown_free(markdown);
rc = write_all(fd, ob->data, ob->size);
if (rc < 0) warn("write_all");
bufrelease(ob);
if (!print) {
rc = dprintf(fd, "</td></tr>");
if (rc < 0) warn("dprintf");
rc = dprintf(fd, "<tr><td></td><td>");
if (rc < 0) warn("dprintf");
} else {
rc = write_buf(fd, "<br>");
if (rc < 0) warn("write_all");
}
struct zet_search zs;
bool first = true;
char errbuf[LINE_MAX] = "";
rc = zet_search_start(&zs, id, NULL, errbuf, sizeof(errbuf));
if (rc < 0) return html_error(fd, "Unable to list: %s", errbuf);
do {
const char *result_id;
rc = zet_search_next(&zs, &result_id);
if (rc < 0) warn("zet_search_next");
if (result_id == NULL) break;
if (first) {
first = false;
rc = dprintf(fd, "Backlinks:<ul>");
if (rc < 0) warn("dprintf");
}
char title_buf[128];
char *result_title = zet_get_title(result_id, title_buf, sizeof(title_buf));
if (result_title == NULL) {
warn("zet_get_title");
result_title = title_buf;
strncpy(result_title, result_id, sizeof(result_title)-1);
result_title[sizeof(result_title)-1] = '\0';
return 0;
}
rc = dprintf(fd, "<li><a href=\"%s%s%s\">", result_id,
query ? "?" : "", query ? query : "");
rc |= write_html(fd, result_title, strlen(result_title));
rc |= write_buf(fd, "</a></li>");
} while (1);
if (!first) (void)dprintf(fd, "</ul>");
if (!print) {
rc = dprintf(fd, "</td></tr>");
if (rc < 0) warn("dprintf");
}
(void)zet_search_close(&zs);
if (!print) {
rc = write_buf(fd, "</table>");
if (rc < 0) warn("write_buf");
}
rc = write_buf(fd, "</body></html>");
if (rc < 0) warn("write_buf");
close(fd);
return 0;
}
static int handle_dpi_client(int fd) {
int rc;
char url[2048];
rc = dpi_check_auth(fd);
if (rc < 0) {
warn("dpi auth");
close(fd);
return 0;
}
rc = dpi_read_request(fd, url, sizeof(url));
if (rc < 0) {
if (errno == ESHUTDOWN) return -1;
warn("dpi request");
dpi_respond_err(fd, "Unable to read request");
return 0;
}
if (!strncmp(url, "dpi:/zet/", 9)) {
return dpi_serve_zet(fd, url + 8);
}
return dpi_serve_not_found(fd);
}
static char *next_line(char *str) {
char *nextline = strchr(str, '\n');
if (nextline != NULL) {
if (nextline[-1] == '\r') nextline[-1] = '\0';
else *nextline = '\0';
nextline++;
}
return nextline;
}
static char *parse_header(char *line) {
char *value = strchr(line, ':');
if (value != NULL) {
*value++ = '\0';
while (*value == ' ') value++;
}
return value;
}
static int http_serve_index(struct http_request *req) {
write_buf(req->fd, "HTTP/1.0 200 OK\r\nConnection: close\r\n\r\n");
close(req->fd);
return 0;
}
static void generate_id(char id[64]) {
if (!seeded) {
srandom(time(NULL) ^ getpid());
seeded = true;
}
(void)snprintf(id, 64, "%08lx%08lx", random(), random());
}
static int save_part(struct http_request *req, int note_fd) {
int rc = write_all(note_fd, (unsigned char *)req->data, req->len);
if (req->add_new_id) {
char id[72] = "\n§";
generate_id(id + 1 + sigil_size);
rc |= write_all(note_fd, (unsigned char *)id, strlen(id));
}
rc |= close(note_fd);
free(req->full_data);
return rc;
}
static int parse_multipart(struct http_request *req) {
size_t boundary_len = req->multipart_boundary != NULL
? strlen(req->multipart_boundary) : 0;
if (boundary_len == 0) return -1;
ssize_t len = req->content_length;
if (len < 0) return -1;
if (req->len < (size_t)len) {
req->full_data = malloc(len+1);
memcpy(req->full_data, req->data, req->len);
char *rest = req->full_data + req->len;
int rc = read_all(req->fd, (unsigned char *)rest, len - req->len);
if (rc < 0) return -1;
req->data = req->full_data;
req->len = len;
}
req->data[req->len] = '\0';
// iterate through parts
char *part, *next;
const char *boundary = req->multipart_boundary;
for (part = strstr(req->data, boundary); part != NULL; part = next) {
part += boundary_len;
if (part[0] == '-' && part[1] == '-') break; // end of parts
if (*part == '\r') part++;
if (*part == '\n') part++;
next = strstr(part, req->multipart_boundary);
char *end = next;
if (end != NULL) {
if (end[-1] == '\n') end--;
if (end[-1] == '\r') end--;
*end = '\0';
}
// iterate through headers in part
char *line, *nextline;
char *cdisp = NULL;
for (line = part; line != NULL; line = nextline) {
nextline = next_line(line);
if (!*line) break; // end of headers
char *name = line;
char *value = parse_header(line);
if (!strncmp(name, "Content-Disposition", 3)) cdisp = value;
}
char *name = NULL;
if (cdisp != NULL && !strncmp(cdisp, "form-data; name=\"", 17)) {
name = cdisp + 17;
char *end = strchr(name, '"');
if (end != NULL) *end = '\0';
}
// handle part body
char *body = nextline;
if (body == NULL) continue;
if (!strcmp(name, "text")) {
req->data = body;
req->len = end == NULL ? strlen(body) : (size_t)end - (size_t)body;
} else if (!strcmp(name, "add_new_id")) {
req->add_new_id = true;
}
}
return 0;
}
static int http_serve_new(struct http_request *req) {
int rc;
char note_path[_POSIX_PATH_MAX];
char id[64];
rc = parse_multipart(req);
if (rc < 0) {
write_buf(req->fd, "HTTP/1.0 400 Bad Request\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Unable to parse form submission");
close(req->fd);
return 0;
}
(void)mkdir(zet_dir, 0700);
generate_id(id);
(void)snprintf(note_path, sizeof(note_path), "%s/%s", zet_dir, id);
int note_fd = open(note_path, O_CREAT|O_WRONLY|O_TRUNC, 0640);
if (note_fd < 0) {
dprintf(req->fd, "HTTP/1.0 500 Internal Server Error\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Unable to open file: %s\n", strerror(errno));
close(req->fd);
return 0;
}
rc = save_part(req, note_fd);
if (rc < 0) {
dprintf(req->fd, "HTTP/1.0 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Error saving: %s", strerror(errno));
close(req->fd);
return 0;
}
dprintf(req->fd, "HTTP/1.0 302 Found\r\n"
"Content-Type: text/html\r\n"
"Location: dpi:/zet/%s\r\n"
"Connection: close\r\n\r\n"
"<!doctype html><head>"
"<title>Created: §%s</title>"
"<meta charset=utf-8>"
"</head><body>"
"Created: <a href=\"dpi:/zet/%s\">§%s</a>"
"</body></html>"
, id, id, id, id);
close(req->fd);
return 0;
}
static int http_serve_not_found(struct http_request *req) {
dprintf(req->fd, "HTTP/1.0 200 OK\r\n"
"Connection: close\r\n\r\n"
"Not found\n");
close(req->fd);
return 0;
}
static int http_serve_zet(struct http_request *req) {
int rc;
char note_path[_POSIX_PATH_MAX];
char note_path_backup[_POSIX_PATH_MAX];
char *id = req->path + 1;
rc = parse_multipart(req);
if (rc < 0) {
write_buf(req->fd, "HTTP/1.0 400 Bad Request\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Unable to parse form submission");
close(req->fd);
return 0;
}
(void)snprintf(note_path, sizeof(note_path), "%s/%s", zet_dir, id);
(void)snprintf(note_path_backup, sizeof(note_path_backup), "%s/%s~", zet_dir, id);
rc = rename(note_path, note_path_backup);
if (rc < 0 && errno != ENOENT) warn("rename");
int note_fd = open(note_path, O_CREAT|O_WRONLY|O_TRUNC, 0640);
if (note_fd < 0) {
if (errno == ENOENT) return http_serve_not_found(req);
dprintf(req->fd, "HTTP/1.0 500 Internal Server Error\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Unable to open file: %s\n", strerror(errno));
close(req->fd);
return 0;
}
rc = save_part(req, note_fd);
if (rc < 0) {
dprintf(req->fd, "HTTP/1.0 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Error saving: %s", strerror(errno));
close(req->fd);
return 0;
}
dprintf(req->fd, "HTTP/1.0 302 Found\r\n"
"Content-Type: text/html\r\n"
"Location: dpi:/zet/%s\r\n"
"Connection: close\r\n\r\n"
"<!doctype html><head>"
"<title>Saved: §%s</title>"
"<meta charset=utf-8>"
"</head><body>"
"Saved: <a href=\"dpi:/zet/%s\">§%s</a>"
"</body></html>"
, id, id, id, id);
close(req->fd);
return 0;
}
enum http_method parse_method(const char *method) {
if (!strcasecmp(method, "GET")) return HTTP_METHOD_GET;
if (!strcasecmp(method, "HEAD")) return HTTP_METHOD_HEAD;
if (!strcasecmp(method, "POST")) return HTTP_METHOD_POST;
return HTTP_METHOD_OTHER;
}
static int handle_http_client(int fd, int firstchar) {
int rc;
struct http_request req;
char buf[2048];
buf[0] = firstchar;
size_t len = sizeof(buf)-1;
rc = read_some(fd, (unsigned char *)buf+1, &len);
buf[len+1] = '\0';
len++;
if (rc < 0) { close(fd); return 0; }
char *nextline = next_line(buf);
req.path = strchr(buf, ' ');
if (req.path == NULL) { close(fd); return 0; }
*req.path++ = '\0';
req.method = parse_method(buf);
req.http_version = strchr(req.path, ' ');
if (req.http_version != NULL) *req.http_version++ = '\0';
req.referer = NULL;
req.host = NULL;
req.content_length = -1;
req.content_type = NULL;
req.multipart_boundary = NULL;
req.add_new_id = false;
char *line = nextline;
while (line != NULL) {
nextline = next_line(line);
if (!line[0]) break;
char *name = line;
char *value = parse_header(line);
if (!strncmp(name, "Host", 3)) req.host = value;
else if (!strncmp(name, "Referer", 7)) req.referer = value;
else if (!strncmp(name, "Content-Length", 14)) req.content_length = atoi(value);
else if (!strncmp(name, "Content-Type", 14)) req.content_type = value;
// else fprintf(stderr, "%s = %s\n", name, value);
line = nextline;
}
if (req.content_type != NULL
&& !strncmp(req.content_type, "multipart/", 10)) {
char *subtype = req.content_type + 10;
char *boundary = strstr(subtype, "; boundary=");
if (boundary != NULL) {
*boundary = '\0';
boundary += 11;
if (boundary[0] == '"') {
boundary++;
char *end = strchr(boundary, '"');
if (end != NULL) *end = '\0';
}
*--boundary = '-';
*--boundary = '-';
req.multipart_boundary = boundary;
}
}
if (req.referer == NULL || strncmp(req.referer, server_url, strlen(server_url))) {
dprintf(fd, "HTTP/1.0 403 Forbidden\r\n"
"Connection: close\r\n\r\n"
"Invalid Referer\n");
close(fd);
return 0;
}
if (req.host != NULL && strcmp(req.host, server_hostname)) {
dprintf(fd, "HTTP/1.0 403 Forbidden\r\n"
"Connection: close\r\n\r\n"
"Invalid Host\n");
close(fd);
return 0;
}
if (req.method != HTTP_METHOD_POST) {
write_buf(fd, "HTTP/1.0 405 Method Not Allowed\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Expected POST request");
close(fd);
return 0;
}
ptrdiff_t offset = nextline - buf;
req.data = nextline;
req.full_data = NULL;
req.len = len - offset;
req.fd = fd;
parse_req_uri(req.path, &req.hash, &req.query);
if (!strcmp(req.path, "/")) return http_serve_index(&req);
if (!strcmp(req.path, "/new")) return http_serve_new(&req);
return http_serve_zet(&req);
}
static int handle_client(int server_fd) {
int fd;
do {
fd = accept(server_fd, NULL, NULL);
if (fd < 0 && errno != ENETDOWN && errno != EPROTO
&& errno != ENOPROTOOPT && errno != EHOSTDOWN
&& errno != ENONET && errno != EHOSTUNREACH
&& errno != EOPNOTSUPP && errno != ENETUNREACH
) return -1;
} while (fd < 0);
int firstchar = read_char(fd);
if (firstchar < 0) {
// ignore errno
close(fd);
return 0;
}
if (firstchar == '<') return handle_dpi_client(fd);
return handle_http_client(fd, firstchar);
}
void get_server_address() {
struct sockaddr_storage addr;
struct sockaddr *addrp = (struct sockaddr *)&addr;
socklen_t addr_len = sizeof(addr);
char host_buf[128];
const char *host;
unsigned short port;
int rc = getsockname(STDIN_FILENO, addrp, &addr_len);
if (rc < 0) err(1, "getsockname");
if (addr.ss_family == AF_INET) {
struct sockaddr_in *addr_in = (struct sockaddr_in *)&addr;
port = ntohs(addr_in->sin_port);
host = inet_ntop(AF_INET, &addr_in->sin_addr, host_buf, sizeof(host_buf));
} else if (addr.ss_family == AF_INET6) {
struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)&addr;
port = ntohs(addr_in6->sin6_port);
host = inet_ntop(AF_INET6, &addr_in6->sin6_addr, host_buf, sizeof(host_buf));
} else {
errx(1, "Unknown server address family: %d", addr.ss_family);
}
int wrote = snprintf(server_hostname, sizeof(server_hostname), "%s:%hu", host, port);
if (wrote <= 0 || wrote >= (int)sizeof(server_hostname)) {
errx(1, "Unable to write hostname");
}
wrote = snprintf(server_url, sizeof(server_url), "http://%s/", server_hostname);
if (wrote <= 0 || wrote >= (int)sizeof(server_url)) {
errx(1, "Unable to write local URL");
}
}
int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
warnx("started");
const char *home = getenv("HOME");
int len = snprintf(zet_dir, sizeof(zet_dir), "%s/.zet", home);
if (len < 0 || len >= (int)sizeof(zet_dir)) {
err(1, "unable to build directory name");
}
// Since dpi requests don't support a request body, we POST via HTTP
// to our dpi server socket which is already listing on a local TCP port.
get_server_address();
// handle connections one at a time
while (handle_client(STDIN_FILENO) == 0);
warnx("exiting");
}