blog

The tiny blog platform powering https://blog.kore.io
Commits | Files | Refs | README | git clone https://git.kore.io/kore-blog.git

commit 142984136a8b5dd0c8d096e082ff69feab462d2d
Author: Joris Vink <joris@coders.se>
Date:   Wed, 18 Apr 2018 14:01:17 +0200

Initial kore based blog platform.

Diffstat:
.gitignore | 9+++++++++
assets/blog_post.html | 1+
assets/index.html | 12++++++++++++
assets/index_end.html | 2++
assets/index_entry.html | 2++
assets/index_top.html | 17+++++++++++++++++
assets/kore.png | 0
assets/no_index.txt | 1+
assets/post_end.html | 3+++
assets/post_start.html | 17+++++++++++++++++
assets/style.css | 49+++++++++++++++++++++++++++++++++++++++++++++++++
conf/blog.conf | 16++++++++++++++++
conf/build.conf | 25+++++++++++++++++++++++++
src/blog.c | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 521 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,9 @@ +*.o +.flavor +.objs +blog.so +blog +assets.h +cert +blogs +dh2048.pem diff --git a/assets/blog_post.html b/assets/blog_post.html @@ -0,0 +1 @@ +<h2 class="title">[title]</title> diff --git a/assets/index.html b/assets/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE> + +<html> +<head> +<title>%s</title> +</head> + +<body> +content +</body> + +</html> diff --git a/assets/index_end.html b/assets/index_end.html @@ -0,0 +1,2 @@ +</body> +</html> diff --git a/assets/index_entry.html b/assets/index_entry.html @@ -0,0 +1,2 @@ +<a href="/%s/%s">%s</a> +<br> diff --git a/assets/index_top.html b/assets/index_top.html @@ -0,0 +1,17 @@ +<!DOCTYPE> + +<html> +<head> +<title>Blog posts</title> +<link rel="stylesheet" type="text/css" href="/style.css"> +</head> + +<body> + +<div style="width: 100%; text-align: center"> + <a href="/"><img src="/logo.png" style="width: 400px; height: auto"></a> +</div> + +<div style="margin-top: 45px"></div> + +<h2 class="title">Blog posts</h2> diff --git a/assets/kore.png b/assets/kore.png Binary files differ. diff --git a/assets/no_index.txt b/assets/no_index.txt @@ -0,0 +1 @@ +No blog index generated. diff --git a/assets/post_end.html b/assets/post_end.html @@ -0,0 +1,3 @@ + +</body> +</html> diff --git a/assets/post_start.html b/assets/post_start.html @@ -0,0 +1,17 @@ +<!DOCTYPE> + +<html> +<head> +<title>%s</title> +<link rel="stylesheet" type="text/css" href="/style.css"> +</head> + +<body> + +<div style="width: 100%%; text-align: center"> + <a href="/"><img src="/logo.png" style="width: 400px; height: auto"></a> +</div> + +<div style="margin-top: 45px"></div> + +<h2 class="title">%s</h2> diff --git a/assets/style.css b/assets/style.css @@ -0,0 +1,49 @@ +* { + margin: 0px; + padding: 0px; + font-family: "Courier New", Courier, monospace +} + +body { + margin-top: 75px; + margin-left: auto; + margin-right: auto; + width: 600px; + font-size: 18px; + background-color: #101010; + color: #fff; +} + +h2.title { + margin-bottom: 15px; + font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; +} + +a, a:visited { + color: #40e0d0; +} + +p { + margin-top: 25px; +} + +pre { + font-size: 14px; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; +} + +code { + background-color: #303030; + border: 1px solid #303030; + border-radius: 5px; + color: #fff; + font-size: 12px; + width: 100%; + min-height: 200px; + display: block; + padding: 5px 10px 5px 10px; +} diff --git a/conf/blog.conf b/conf/blog.conf @@ -0,0 +1,16 @@ +# blog configuration + +bind 127.0.0.1 9988 + +domain * { + static / post_list + dynamic ^/posts/[a-z1-9\-]+$ post_render + + static /drafts/ draft_list + dynamic ^/drafts/[a-z1-9\-]+$ draft_render + + static /logo.png asset_serve_kore_png + static /style.css asset_serve_style_css + + dynamic ^.*$ redirect +} diff --git a/conf/build.conf b/conf/build.conf @@ -0,0 +1,25 @@ +# blog build config +# You can switch flavors using: kodev flavor [newflavor] + +# Set to yes if you wish to produce a single binary instead +# of a dynamic library. If you set this to yes you must also +# set kore_source together with kore_flavor. +single_binary=yes +kore_source=/Users/joris/src/kore +kore_flavor=NOTLS=1 + +# The flags below are shared between flavors +cflags=-std=c99 -pedantic -O3 +cflags=-Wall -Wmissing-declarations -Wshadow +cflags=-Wstrict-prototypes -Wmissing-prototypes +cflags=-Wpointer-arith -Wcast-qual -Wsign-compare + +mime_add=png:image/png +mime_add=css:text/css + +dev { + cflags=-g +} + +prod { +} diff --git a/src/blog.c b/src/blog.c @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2018 Joris Vink <joris@coders.se> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/stat.h> + +#include <kore/kore.h> +#include <kore/http.h> + +#include <fts.h> +#include <fcntl.h> +#include <ctype.h> +#include <signal.h> +#include <unistd.h> + +#include "assets.h" + +#define BLOG_DIR "blogs" +#define POST_FLAG_DRAFT 0x0001 + +struct post { + int flags; + char *uri; + char *file; + char *title; + struct kore_buf *cache; + TAILQ_ENTRY(post) list; +}; + +void index_rebuild(void); +void signal_handler(int); +void tick(void *, u_int64_t); +int fts_compare(const FTSENT **, const FTSENT **); + +struct post *post_register(char *); +void post_cache(struct post *); +void post_remove(struct post *); +int post_send(struct http_request *, const char *, int); + +int redirect(struct http_request *); +int post_list(struct http_request *); +int post_render(struct http_request *); +int draft_list(struct http_request *); +int draft_render(struct http_request *); +int list_posts(struct http_request *, const char *, int); + +static TAILQ_HEAD(, post) posts; +static volatile sig_atomic_t blog_sig = -1; + +void +signal_handler(int sig) +{ + blog_sig = sig; +} + +void +tick(void *unused, u_int64_t now) +{ + if (blog_sig == SIGHUP) { + blog_sig = -1; + index_rebuild(); + } +} + +void +kore_worker_configure(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = signal_handler; + + if (sigfillset(&sa.sa_mask) == -1) + fatal("sigfillset: %s", errno_s); + if (sigaction(SIGHUP, &sa, NULL) == -1) + fatal("sigaction: %s", errno_s); + + (void)kore_timer_add(tick, 1000, NULL, 0); + + TAILQ_INIT(&posts); + index_rebuild(); +} + +int +fts_compare(const FTSENT **a, const FTSENT **b) +{ + const FTSENT *a1 = *a; + const FTSENT *b1 = *b; + + if (a1->fts_statp->st_mtime > b1->fts_statp->st_mtime) + return (-1); + + if (a1->fts_statp->st_mtime < b1->fts_statp->st_mtime) + return (1); + + return (0); +} + +void +index_rebuild(void) +{ + FTSENT *fe; + FTS *fts; + struct post *post; + char *path[] = { BLOG_DIR, NULL }; + + kore_log(LOG_INFO, "rebuilding post list"); + + if ((fts = fts_open(path, + FTS_NOCHDIR | FTS_PHYSICAL, fts_compare)) == NULL) { + kore_log(LOG_ERR, "fts_open(): %s", errno_s); + return; + } + + while (!TAILQ_EMPTY(&posts)) { + post = TAILQ_FIRST(&posts); + post_remove(post); + } + + TAILQ_INIT(&posts); + + while ((fe = fts_read(fts)) != NULL) { + if (!S_ISREG(fe->fts_statp->st_mode)) + continue; + if ((post = post_register(fe->fts_accpath)) != NULL) + post_cache(post); + } + + fts_close(fts); +} + +void +post_cache(struct post *post) +{ + int fd; + ssize_t bytes; + u_int8_t buf[1024]; + + if ((fd = open(post->file, O_RDONLY)) == -1) { + kore_log(LOG_ERR, "failed to open '%s' (%s)", + post->file, errno_s); + post_remove(post); + return; + } + + post->cache = kore_buf_alloc(1024); + kore_buf_appendf(post->cache, + (const char *)asset_post_start_html, post->title, post->title); + + for (;;) { + bytes = read(fd, buf, sizeof(buf)); + if (bytes == -1) { + if (errno == EINTR) + continue; + kore_log(LOG_ERR, "read(%s): %s", post->file, errno_s); + post_remove(post); + close(fd); + return; + } + + if (bytes == 0) + break; + + kore_buf_append(post->cache, buf, bytes); + } + + close(fd); + + kore_buf_append(post->cache, asset_post_end_html, + asset_len_post_end_html); +} + +struct post * +post_register(char *path) +{ + struct post *post; + int invalid; + char *p, *fpath, *uri, title[128]; + + if (strlen(path) <= (strlen(BLOG_DIR) + 1)) + fatal("invalid path from fts_read()?"); + + uri = path + strlen(BLOG_DIR) + 1; + if (uri[0] == '.' || uri[0] == '\0') + return (NULL); + + fpath = kore_strdup(path); + if ((p = strrchr(path, '.')) != NULL) + *p = '\0'; + + if (kore_strlcpy(title, uri, sizeof(title)) >= sizeof(title)) { + kore_free(fpath); + kore_log(LOG_ERR, "blog name (title) '%s' too long", uri); + return (NULL); + } + + invalid = 0; + for (p = &uri[0]; *p != '\0'; p++) { + if (*p == ' ' || *p == '-' || *p == '_') { + *p = '-'; + continue; + } + + if (!isalnum(*(unsigned char *)p)) { + invalid++; + continue; + } + + if (*p >= 'A' && *p <= 'Z') + *p += 0x20; + } + + if (invalid) { + kore_free(fpath); + kore_log(LOG_ERR, "'%s' contains invalid characters", fpath); + return (NULL); + } + + post = kore_calloc(1, sizeof(*post)); + post->flags = 0; + post->file = fpath; + post->uri = kore_strdup(uri); + post->title = kore_strdup(title); + TAILQ_INSERT_TAIL(&posts, post, list); + + if (strstr(post->file, "draft")) + post->flags |= POST_FLAG_DRAFT; + + return (post); +} + +void +post_remove(struct post *post) +{ + TAILQ_REMOVE(&posts, post, list); + kore_buf_free(post->cache); + kore_free(post->title); + kore_free(post->file); + kore_free(post->uri); + kore_free(post); +} + +int +redirect(struct http_request *req) +{ + http_response_header(req, "location", "/"); + http_response(req, HTTP_STATUS_FOUND, NULL, 0); + return (KORE_RESULT_OK); +} + +int +post_list(struct http_request *req) +{ + return (list_posts(req, "posts", 0)); +} + +int +draft_list(struct http_request *req) +{ + return (list_posts(req, "drafts", POST_FLAG_DRAFT)); +} + +int +list_posts(struct http_request *req, const char *type, int flags) +{ + struct kore_buf buf; + struct post *post; + + if (req->method != HTTP_METHOD_GET) { + http_response_header(req, "allow", "get"); + http_response(req, HTTP_STATUS_BAD_REQUEST, NULL, 0); + return (KORE_RESULT_OK); + } + + kore_buf_init(&buf, 128); + kore_buf_append(&buf, asset_index_top_html, asset_len_index_top_html); + + TAILQ_FOREACH(post, &posts, list) { + if (post->flags != flags) + continue; + kore_buf_appendf(&buf, (const char *)asset_index_entry_html, + type, post->uri, post->title); + } + + kore_buf_append(&buf, asset_index_end_html, asset_len_index_end_html); + + http_response_header(req, "content-type", "text/html; charset=utf-8"); + http_response(req, 200, buf.data, buf.offset); + + kore_buf_cleanup(&buf); + + return (KORE_RESULT_OK); +} + +int +draft_render(struct http_request *req) +{ + return (post_send(req, "/drafts/", POST_FLAG_DRAFT)); +} + +int +post_render(struct http_request *req) +{ + return (post_send(req, "/posts/", 0)); +} + +int +post_send(struct http_request *req, const char *path, int flags) +{ + const char *uri; + struct post *post; + int redirect; + + if (req->method != HTTP_METHOD_GET) { + http_response_header(req, "allow", "get"); + http_response(req, HTTP_STATUS_BAD_REQUEST, NULL, 0); + return (KORE_RESULT_OK); + } + + if (strlen(req->path) <= strlen(path)) { + http_response(req, HTTP_STATUS_INTERNAL_ERROR, NULL, 0); + return (KORE_RESULT_OK); + } + + post = NULL; + redirect = 0; + uri = req->path + strlen(path); + + TAILQ_FOREACH(post, &posts, list) { + if (post->flags != flags) + continue; + if (!strcmp(post->uri, uri)) + break; + } + + if (post == NULL) { + redirect++; + } else if (post->cache == NULL) { + redirect++; + kore_log(LOG_ERR, "no cache for %s", post->uri); + } + + if (redirect) { + http_response_header(req, "location", "/"); + http_response(req, HTTP_STATUS_FOUND, NULL, 0); + return (KORE_RESULT_OK); + } + + http_response_header(req, "content-type", "text/html; charset=utf-8"); + http_response(req, 200, post->cache->data, post->cache->offset); + + return (KORE_RESULT_OK); +}