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
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, "&");
|
|
case '<': return write_buf(fd, "<");
|
|
case '>': return write_buf(fd, ">");
|
|
case '"': return write_buf(fd, """);
|
|
}
|
|
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=\"→\">", 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");
|
|
}
|