#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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_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; char *query; }; 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, "\nError: %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, " §%s", id, id); } int rc = dprintf(fd, " [", id); rc |= write_html(fd, title, strlen(title)); rc |= dprintf(fd, "](§%s)", id); return rc; } static int write_topbar(int fd, enum page page, const char *id, const char *title) { int rc = write_buf(fd, "
"); rc |= write_buf(fd, " "); if (page == PAGE_ALL) rc |= write_buf(fd, ""); rc |= write_buf(fd, "All"); if (page == PAGE_ALL) rc |= write_buf(fd, ""); rc |= write_buf(fd, " "); if (page == PAGE_RECENT) rc |= write_buf(fd, ""); rc |= write_buf(fd, "Recent"); if (page == PAGE_RECENT) rc |= write_buf(fd, ""); rc |= write_buf(fd, " "); if (page == PAGE_NEW) rc |= write_buf(fd, ""); rc |= write_buf(fd, "New"); if (page == PAGE_NEW) rc |= write_buf(fd, ""); rc |= write_buf(fd, " "); if (page == PAGE_ZET) { rc |= write_zet_link(fd, id, title); } rc |= write_buf(fd, "
"); 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, "\nError: %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 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_query(const char *id, const char *query) { 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 = strcasestr(text, query) != NULL; free(text); return found ? 1 : 0; } static int zet_search_start(struct zet_search *zs, const char *id, char *query) { zs->dir = opendir(zet_dir); if (zs->dir == NULL) return -1; zs->id = id; zs->query = query; 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; const char *query = zs->query; 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 (query != NULL) { rc = zet_matches_query(id, query); if (rc < 0) return -1; if (rc != 1) continue; } break; }; *idp = id; return 0; } static int zet_search_close(struct zet_search *sz) { return closedir(sz->dir); } static int dpi_serve_zet_all(int fd) { int rc; struct zet_search zs; rc = zet_search_start(&zs, NULL, NULL); if (rc < 0) return dpi_respond_err(fd, "Unable to list"); rc = dpi_send_header(fd, "text/html"); if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; } rc = dprintf(fd, "" "Notes - All" "" "" ""); rc |= write_topbar(fd, PAGE_ALL, NULL, NULL); rc |= write_buf(fd, "
    "); if (rc < 0) { warn("write"); close(fd); return 0; } const char *id; while (1) { rc = zet_search_next(&zs, &id); if (rc < 0) { rc = dprintf(fd, "\nError: %s\n", strerror(errno)); if (rc < 0) warnx("dprintf"); close(fd); return 0; } 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, "
  • ", id); rc |= write_html(fd, title, strlen(title)); rc |= write_buf(fd, "
  • "); if (rc < 0) { warnx("write"); close(fd); return 0; } } rc = dprintf(fd, "
"); 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 = zet_search_start(&zs, NULL, NULL); if (rc < 0) return dpi_respond_err(fd, "Unable to list"); rc = dpi_send_header(fd, "text/html"); if (rc < 0) { warn("dpi_send_header"); goto cleanup; } rc = dprintf(fd, "" "Notes - Recent" "" "" ""); rc |= write_topbar(fd, PAGE_RECENT, NULL, NULL); rc |= write_buf(fd, "
    "); 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, "
  • ", id); rc |= write_html(fd, title, strlen(title)); rc |= write_buf(fd, "
  • "); if (rc < 0) { warnx("write"); goto cleanup; } } rc = dprintf(fd, "
"); 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 = zet_search_start(&zs, NULL, query); if (rc < 0) return dpi_respond_err(fd, "Unable to list"); rc = dpi_send_header(fd, "text/html"); if (rc < 0) { warn("dpi_send_header"); close(fd); return 0; } rc = dprintf(fd, "" "Notes" "" "" "" ); rc |= write_topbar(fd, PAGE_SEARCH, query, NULL); rc |= write_buf(fd, "
    "); if (rc < 0) { warn("write"); close(fd); return 0; } const char *id; while (1) { rc = zet_search_next(&zs, &id); if (rc < 0) { rc = dprintf(fd, "\nError: %s\n", strerror(errno)); if (rc < 0) warnx("dprintf"); close(fd); return 0; } 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, "
  • "); rc |= write_zet_link(fd, id, title); rc |= write_buf(fd, "
  • "); if (rc < 0) { warnx("dprintf/write_html/write_buf"); close(fd); return 0; } } rc = dprintf(fd, "
"); 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, "" "New Note" "" "" ""); write_topbar(fd, PAGE_NEW, NULL, NULL); dprintf(fd, "" "" , 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, "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, ""); 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, "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, "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, ""); 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, "" ""); if (title != NULL) rc |= write_html(fd, title, strlen(title)); rc |= dprintf(fd, "" "" "" ""); rc |= write_buf(fd, ""); rc |= write_topbar(fd, PAGE_ZET, id, title); rc |= dprintf(fd, "
" "
" "" "
" "" ""); if (rc < 0) warn("dprintf"); rc = dprintf(fd, ""); if (rc < 0) warn("dprintf"); (void)zet_search_close(&zs); rc = write_buf(fd, "
" "
" "" " " "
" ); 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) { rc = dprintf(fd, "\nError: %s", strerror(errno)); if (rc < 0) warnx("dprintf"); close(fd); } char *text = note_fd >= 0 ? read_full(note_fd) : NULL; if (text == NULL) { rc = dprintf(fd, "\nError: %s", strerror(errno)); if (rc < 0) warnx("dprintf"); close(fd); } 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); rc = dprintf(fd, "
"); if (rc < 0) warn("dprintf"); struct zet_search zs; bool first = true; rc = zet_search_start(&zs, id, NULL); if (rc < 0) warn("zet_search_start"); 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:"); rc = dprintf(fd, "
"); if (rc < 0) warn("write_buf"); rc = write_buf(fd, ""); 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" "" "Created: §%s" "" "" "Created: §%s" "" , 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" "" "Saved: §%s" "" "" "Saved: §%s" "" , 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"); }