blog

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

blog.c (15373B)



      1 /*
      2  * Copyright (c) 2018-2019 Joris Vink <joris@coders.se>
      3  *
      4  * Permission to use, copy, modify, and distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 #if defined(__linux__)
     18 #define _GNU_SOURCE
     19 #endif
     20 
     21 #include <sys/types.h>
     22 #include <sys/queue.h>
     23 #include <sys/stat.h>
     24 
     25 #include <kore/kore.h>
     26 #include <kore/http.h>
     27 
     28 #include <sodium.h>
     29 
     30 #include <ctype.h>
     31 #include <fts.h>
     32 #include <fcntl.h>
     33 #include <signal.h>
     34 #include <time.h>
     35 #include <unistd.h>
     36 
     37 #include "assets.h"
     38 
     39 #define BLOG_SESSION_LEN	32
     40 
     41 #define MSG_SESSION_ADD		100
     42 #define MSG_SESSION_DEL		200
     43 
     44 #define BLOG_DIR		"blogs"
     45 #define BLOG_VER		"kore-blog v0.2"
     46 #define BLOG_USER_CONF		"users.conf"
     47 
     48 #define POST_FLAG_DRAFT		0x0001
     49 
     50 struct cache {
     51 	u_int32_t		refs;
     52 	struct kore_buf		buf;
     53 };
     54 
     55 struct session {
     56 	uint32_t		uid;
     57 	char			data[(BLOG_SESSION_LEN * 2) + 1];
     58 };
     59 
     60 struct user {
     61 	u_int32_t		uid;
     62 	char			*name;
     63 	struct session		session;
     64 	char			*passphrase;
     65 	TAILQ_ENTRY(user)	list;
     66 };
     67 
     68 struct post {
     69 	int			flags;
     70 	size_t			coff;
     71 	size_t			clen;
     72 	time_t			mtime;
     73 	char			*uri;
     74 	char			*file;
     75 	char			*title;
     76 	struct cache		*cache;
     77 	TAILQ_ENTRY(post)	list;
     78 };
     79 
     80 void	user_reload(void);
     81 void	index_rebuild(void);
     82 void	signal_handler(int);
     83 void	tick(void *, u_int64_t);
     84 int	cache_sent(struct netbuf *);
     85 int	fts_compare(const FTSENT **, const FTSENT **);
     86 
     87 struct cache	*cache_create(size_t);
     88 struct post	*post_register(char *);
     89 void		post_cache(struct post *);
     90 void		post_remove(struct post *);
     91 int		post_send(struct http_request *, const char *, int);
     92 void		cache_ref_drop(struct cache **);
     93 
     94 int	auth_login(struct http_request *);
     95 int	auth_user_exists(struct http_request *, char *);
     96 void	auth_session_add(struct kore_msg *, const void *);
     97 void	auth_session_del(struct kore_msg *, const void *);
     98 int	auth_session(struct http_request *, const char *);
     99 
    100 int	redirect(struct http_request *);
    101 int	post_list(struct http_request *);
    102 int	post_render(struct http_request *);
    103 int	draft_list(struct http_request *);
    104 int	draft_render(struct http_request *);
    105 int	referer(struct http_request *, const void *);
    106 int	list_posts(struct http_request *, const char *, struct cache **, int);
    107 
    108 static TAILQ_HEAD(, post)	posts;
    109 static TAILQ_HEAD(, user)	users;
    110 static volatile sig_atomic_t	blog_sig = -1;
    111 static time_t			user_mtime = 0;
    112 
    113 static struct cache		*live_index = NULL;
    114 static struct cache		*draft_index = NULL;
    115 
    116 void
    117 signal_handler(int sig)
    118 {
    119 	blog_sig = sig;
    120 }
    121 
    122 void
    123 tick(void *unused, u_int64_t now)
    124 {
    125 	(void)now;
    126 	(void)unused;
    127 
    128 	if (blog_sig == SIGHUP) {
    129 		blog_sig = -1;
    130 		index_rebuild();
    131 		user_reload();
    132 	}
    133 }
    134 
    135 void
    136 kore_worker_configure(void)
    137 {
    138 	struct sigaction	sa;
    139 
    140 	memset(&sa, 0, sizeof(sa));
    141 	sa.sa_handler = signal_handler;
    142 
    143 	if (sigfillset(&sa.sa_mask) == -1)
    144 		fatal("sigfillset: %s", errno_s);
    145 	if (sigaction(SIGHUP, &sa, NULL) == -1)
    146 		fatal("sigaction: %s", errno_s);
    147 
    148 	(void)kore_timer_add(tick, 1000, NULL, 0);
    149 
    150 	TAILQ_INIT(&posts);
    151 	TAILQ_INIT(&users);
    152 
    153 	index_rebuild();
    154 	user_reload();
    155 
    156 	kore_msg_register(MSG_SESSION_ADD, auth_session_add);
    157 	kore_msg_register(MSG_SESSION_DEL, auth_session_del);
    158 }
    159 
    160 struct cache *
    161 cache_create(size_t len)
    162 {
    163 	struct cache		*cache;
    164 
    165 	cache = kore_calloc(1, sizeof(*cache));
    166 
    167 	cache->refs++;
    168 	kore_buf_init(&cache->buf, len);
    169 
    170 	return (cache);
    171 }
    172 
    173 void
    174 cache_ref_drop(struct cache **ptr)
    175 {
    176 	struct cache	*cache = *ptr;
    177 
    178 	cache->refs--;
    179 
    180 	if (cache->refs == 0) {
    181 		kore_buf_cleanup(&cache->buf);
    182 		kore_free(cache);
    183 		*ptr = NULL;
    184 	}
    185 }
    186 
    187 int
    188 cache_sent(struct netbuf *nb)
    189 {
    190 	struct cache	*cache = (struct cache *)nb->extra;
    191 
    192 	cache_ref_drop(&cache);
    193 
    194 	return (KORE_RESULT_OK);
    195 }
    196 
    197 int
    198 fts_compare(const FTSENT **a, const FTSENT **b)
    199 {
    200 	const FTSENT	*a1 = *a;
    201 	const FTSENT	*b1 = *b;
    202 
    203 	if (a1->fts_statp->st_mtime > b1->fts_statp->st_mtime)
    204 		return (-1);
    205 
    206 	if (a1->fts_statp->st_mtime < b1->fts_statp->st_mtime)
    207 		return (1);
    208 
    209 	return (0);
    210 }
    211 
    212 void
    213 user_reload(void)
    214 {
    215 	struct stat	st;
    216 	FILE		*fp;
    217 	u_int32_t	uids;
    218 	struct user	*user;
    219 	int		lineno;
    220 	char		*line, *pwd, buf[256];
    221 
    222 	if (stat(BLOG_USER_CONF, &st) == -1) {
    223 		if (errno != ENOENT) {
    224 			kore_log(LOG_INFO,
    225 			    "stat(%s): %s", BLOG_USER_CONF, errno_s);
    226 		}
    227 		return;
    228 	}
    229 
    230 	if (user_mtime == st.st_mtime)
    231 		return;
    232 
    233 	while (!TAILQ_EMPTY(&users)) {
    234 		user = TAILQ_FIRST(&users);
    235 		TAILQ_REMOVE(&users, user, list);
    236 		kore_free(user->passphrase);
    237 		kore_free(user->name);
    238 		kore_free(user);
    239 	}
    240 
    241 	TAILQ_INIT(&users);
    242 
    243 	if ((fp = fopen(BLOG_USER_CONF, "r")) == NULL) {
    244 		if (errno != ENOENT) {
    245 			kore_log(LOG_INFO,
    246 			    "fopen(%s): %s", BLOG_USER_CONF, errno_s);
    247 		}
    248 		return;
    249 	}
    250 
    251 	kore_log(LOG_INFO, "reloading users");
    252 
    253 	uids = 1;
    254 	lineno = 0;
    255 
    256 	while ((line = kore_read_line(fp, buf, sizeof(buf))) != NULL) {
    257 		lineno++;
    258 
    259 		if (*line == '\0')
    260 			continue;
    261 
    262 		if ((pwd = strchr(line, ':')) == NULL) {
    263 			kore_log(LOG_INFO, "malformed user @ %d", lineno);
    264 			continue;
    265 		}
    266 
    267 		*(pwd)++ = '\0';
    268 
    269 		if (*line == '\0' || *pwd == '\0') {
    270 			kore_log(LOG_INFO, "malformed user @ %d", lineno);
    271 			continue;
    272 		}
    273 
    274 		user = kore_calloc(1, sizeof(*user));
    275 		user->uid = uids++;
    276 		user->name = kore_strdup(line);
    277 		user->passphrase = kore_strdup(pwd);
    278 		TAILQ_INSERT_TAIL(&users, user, list);
    279 	}
    280 
    281 	fclose(fp);
    282 	user_mtime = st.st_mtime;
    283 }
    284 
    285 void
    286 index_rebuild(void)
    287 {
    288 	FTSENT		*fe;
    289 	FTS		*fts;
    290 	struct post	*post;
    291 	char		*path[] = { BLOG_DIR, NULL };
    292 
    293 	kore_log(LOG_INFO, "rebuilding post list");
    294 
    295 	if (live_index != NULL)
    296 		cache_ref_drop(&live_index);
    297 
    298 	if (draft_index != NULL)
    299 		cache_ref_drop(&draft_index);
    300 
    301 	if ((fts = fts_open(path,
    302 	    FTS_NOCHDIR | FTS_PHYSICAL, fts_compare)) == NULL) {
    303 		kore_log(LOG_ERR, "fts_open(): %s", errno_s);
    304 		return;
    305 	}
    306 
    307 	while (!TAILQ_EMPTY(&posts)) {
    308 		post = TAILQ_FIRST(&posts);
    309 		post_remove(post);
    310 	}
    311 
    312 	TAILQ_INIT(&posts);
    313 
    314 	while ((fe = fts_read(fts)) != NULL) {
    315 		if (!S_ISREG(fe->fts_statp->st_mode))
    316 			continue;
    317 		if ((post = post_register(fe->fts_accpath)) != NULL)
    318 			post_cache(post);
    319 	}
    320 
    321 	fts_close(fts);
    322 }
    323 
    324 void
    325 post_cache(struct post *post)
    326 {
    327 	int		fd;
    328 	struct stat	st;
    329 	ssize_t		bytes;
    330 	u_int8_t	buf[4096];
    331 
    332 	if ((fd = open(post->file, O_RDONLY)) == -1) {
    333 		kore_log(LOG_ERR, "failed to open '%s' (%s)",
    334 		    post->file, errno_s);
    335 		post_remove(post);
    336 		return;
    337 	}
    338 
    339 	if (fstat(fd, &st) == -1) {
    340 		kore_log(LOG_ERR, "fstat(%s): %s", post->file, errno_s);
    341 		post_remove(post);
    342 		return;
    343 	}
    344 
    345 	post->mtime = st.st_mtime;
    346 	post->cache = cache_create(st.st_size);
    347 
    348 	kore_buf_appendf(&post->cache->buf,
    349 	    (const char *)asset_post_start_html, post->title, post->title);
    350 
    351 	post->clen = 0;
    352 	post->coff = post->cache->buf.offset;
    353 
    354 	for (;;) {
    355 		bytes = read(fd, buf, sizeof(buf));
    356 		if (bytes == -1) {
    357 			if (errno == EINTR)
    358 				continue;
    359 			kore_log(LOG_ERR, "read(%s): %s", post->file, errno_s);
    360 			post_remove(post);
    361 			close(fd);
    362 			return;
    363 		}
    364 
    365 		if (bytes == 0)
    366 			break;
    367 
    368 		post->clen += bytes;
    369 		kore_buf_append(&post->cache->buf, buf, bytes);
    370 	}
    371 
    372 	close(fd);
    373 
    374 	kore_buf_appendf(&post->cache->buf,
    375 	    (const char *)asset_blog_version_html, BLOG_VER);
    376 
    377 	kore_buf_append(&post->cache->buf, asset_post_end_html,
    378 	    asset_len_post_end_html);
    379 }
    380 
    381 struct post *
    382 post_register(char *path)
    383 {
    384 	struct post	*post;
    385 	int		invalid;
    386 	char		*p, *fpath, *uri, title[128];
    387 
    388 	if (strlen(path) <= (strlen(BLOG_DIR) + 1))
    389 		fatal("invalid path from fts_read()?");
    390 
    391 	uri = path + strlen(BLOG_DIR) + 1;
    392 	if (uri[0] == '.' || uri[0] == '\0')
    393 		return (NULL);
    394 
    395 	fpath = kore_strdup(path);
    396 	if ((p = strrchr(path, '.')) != NULL)
    397 		*p = '\0';
    398 
    399 	if (kore_strlcpy(title, uri, sizeof(title)) >= sizeof(title)) {
    400 		kore_free(fpath);
    401 		kore_log(LOG_ERR, "blog name (title) '%s' too long", uri);
    402 		return (NULL);
    403 	}
    404 
    405 	invalid = 0;
    406 	for (p = &uri[0]; *p != '\0'; p++) {
    407 		if (*p == ' ' || *p == '-' || *p == '_') {
    408 			*p = '-';
    409 			continue;
    410 		}
    411 
    412 		if (!isalnum(*(unsigned char *)p)) {
    413 			invalid++;
    414 			continue;
    415 		}
    416 
    417 		if (*p >= 'A' && *p <= 'Z')
    418 			*p += 0x20;
    419 	}
    420 
    421 	if (invalid) {
    422 		kore_free(fpath);
    423 		kore_log(LOG_ERR, "'%s' contains invalid characters", fpath);
    424 		return (NULL);
    425 	}
    426 
    427 	post = kore_calloc(1, sizeof(*post));
    428 	post->flags = 0;
    429 	post->file = fpath;
    430 	post->uri = kore_strdup(uri);
    431 	post->title = kore_strdup(title);
    432 	TAILQ_INSERT_TAIL(&posts, post, list);
    433 
    434 	if (strstr(post->file, "draft"))
    435 		post->flags |= POST_FLAG_DRAFT;
    436 
    437 	return (post);
    438 }
    439 
    440 void
    441 post_remove(struct post *post)
    442 {
    443 	cache_ref_drop(&post->cache);
    444 
    445 	TAILQ_REMOVE(&posts, post, list);
    446 	kore_free(post->title);
    447 	kore_free(post->file);
    448 	kore_free(post->uri);
    449 	kore_free(post);
    450 }
    451 
    452 int
    453 referer(struct http_request *req, const void *unused)
    454 {
    455 	const char		*ref, *p;
    456 
    457 	(void)unused;
    458 
    459 	if (!http_request_header(req, "referer", &ref))
    460 		return (KORE_RESULT_OK);
    461 
    462 	p = ref;
    463 
    464 	while (*p != '\0') {
    465 		if (!isprint(*(const unsigned char *)p++)) {
    466 			ref = "[not printable]";
    467 			break;
    468 		}
    469 	}
    470 
    471 	kore_log(LOG_NOTICE, "blog (%s) visit from %s", req->path, ref);
    472 
    473 	return (KORE_RESULT_OK);
    474 }
    475 
    476 int
    477 auth_login(struct http_request *req)
    478 {
    479 	size_t			i;
    480 	int			len;
    481 	struct user		*up;
    482 	struct session		session;
    483 	char			*user, *pass;
    484 	u_int8_t		buf[BLOG_SESSION_LEN];
    485 
    486 	if (req->method == HTTP_METHOD_GET)
    487 		return (asset_serve_login_html(req));
    488 
    489 	http_populate_post(req);
    490 
    491 	if (!http_argument_get_string(req, "user", &user) ||
    492 	    !http_argument_get_string(req, "passphrase", &pass)) {
    493 		req->method = HTTP_METHOD_GET;
    494 		return (asset_serve_login_html(req));
    495 	}
    496 
    497 	up = NULL;
    498 	TAILQ_FOREACH(up, &users, list) {
    499 		if (!strcmp(user, up->name))
    500 			break;
    501 	}
    502 
    503 	if (up == NULL) {
    504 		req->method = HTTP_METHOD_GET;
    505 		kore_log(LOG_INFO, "auth_login: no user data?");
    506 		return (asset_serve_login_html(req));
    507 	}
    508 
    509 	if (crypto_pwhash_str_verify(up->passphrase, pass, strlen(pass)) != 0) {
    510 		req->method = HTTP_METHOD_GET;
    511 		return (asset_serve_login_html(req));
    512 	}
    513 
    514 	session.uid = up->uid;
    515 	memset(session.data, 0, sizeof(session.data));
    516 
    517 	randombytes_buf(buf, sizeof(buf));
    518 	for (i = 0; i < sizeof(buf); i++) {
    519 		len = snprintf(session.data + (i * 2),
    520 		    sizeof(session.data) - (i * 2), "%02x", buf[i]);
    521 		if (len == -1 || (size_t)len >= sizeof(session.data)) {
    522 			kore_log(LOG_ERR, "failed to hexify session");
    523 			req->method = HTTP_METHOD_GET;
    524 			return (asset_serve_login_html(req));
    525 		}
    526 	}
    527 
    528 	kore_msg_send(KORE_MSG_WORKER_ALL, MSG_SESSION_ADD,
    529 	    &session, sizeof(session));
    530 
    531 	http_response_header(req, "location", "/drafts/");
    532 	http_response_cookie(req, "blog_token", session.data,
    533 	    "/drafts/", 0, 0, NULL);
    534 
    535 	kore_log(LOG_INFO, "login for '%s'", up->name);
    536 	http_response(req, HTTP_STATUS_FOUND, NULL, 0);
    537 
    538 	return (KORE_RESULT_OK);
    539 }
    540 
    541 int
    542 auth_user_exists(struct http_request *req, char *user)
    543 {
    544 	struct user	*usr;
    545 
    546 	(void)req;
    547 
    548 	if (user == NULL)
    549 		return (KORE_RESULT_ERROR);
    550 
    551 	TAILQ_FOREACH(usr, &users, list) {
    552 		if (!strcmp(usr->name, user))
    553 			return (KORE_RESULT_OK);
    554 	}
    555 
    556 	return (KORE_RESULT_ERROR);
    557 }
    558 
    559 void
    560 auth_session_add(struct kore_msg *msg, const void *data)
    561 {
    562 	struct user		*user;
    563 	const struct session	*session;
    564 
    565 	if (msg->length != sizeof(*session)) {
    566 		kore_log(LOG_ERR, "auth_session_add: invalid len (%zu)",
    567 		    msg->length);
    568 		return;
    569 	}
    570 
    571 	session = data;
    572 
    573 	TAILQ_FOREACH(user, &users, list) {
    574 		if (user->uid == session->uid) {
    575 			memcpy(&user->session, session, sizeof(*session));
    576 			break;
    577 		}
    578 	}
    579 }
    580 
    581 void
    582 auth_session_del(struct kore_msg *msg, const void *data)
    583 {
    584 	u_int32_t	uid;
    585 	struct user	*user;
    586 
    587 	if (msg->length != sizeof(uid)) {
    588 		kore_log(LOG_ERR, "auth_session_del: invalid len (%zu)",
    589 		    msg->length);
    590 		return;
    591 	}
    592 
    593 	memcpy(&uid, data, sizeof(uid));
    594 
    595 	TAILQ_FOREACH(user, &users, list) {
    596 		if (user->uid == uid) {
    597 			memset(&user->session, 0, sizeof(user->session));
    598 			break;
    599 		}
    600 	}
    601 }
    602 
    603 int
    604 auth_session(struct http_request *req, const char *cookie)
    605 {
    606 	struct user	*user;
    607 
    608 	if (cookie == NULL)
    609 		return (KORE_RESULT_ERROR);
    610 
    611 	TAILQ_FOREACH(user, &users, list) {
    612 		if (!strcmp(user->session.data, cookie)) {
    613 			kore_log(LOG_INFO, "%s requested by %s",
    614 			    req->path, user->name);
    615 			return (KORE_RESULT_OK);
    616 		}
    617 	}
    618 
    619 	return (KORE_RESULT_ERROR);
    620 }
    621 
    622 int
    623 redirect(struct http_request *req)
    624 {
    625 	http_response_header(req, "location", "/");
    626 	http_response(req, HTTP_STATUS_FOUND, NULL, 0);
    627 	return (KORE_RESULT_OK);
    628 }
    629 
    630 int
    631 post_list(struct http_request *req)
    632 {
    633 	return (list_posts(req, "posts", &live_index, 0));
    634 }
    635 
    636 int
    637 draft_list(struct http_request *req)
    638 {
    639 	return (list_posts(req, "drafts", &draft_index, POST_FLAG_DRAFT));
    640 }
    641 
    642 int
    643 list_posts(struct http_request *req, const char *type, struct cache **ptr,
    644     int flags)
    645 {
    646 	struct post		*post;
    647 	struct cache		*cache;
    648 
    649 	if (req->method != HTTP_METHOD_GET) {
    650 		http_response_header(req, "allow", "get");
    651 		http_response(req, HTTP_STATUS_BAD_REQUEST, NULL, 0);
    652 		return (KORE_RESULT_OK);
    653 	}
    654 
    655 	cache = *ptr;
    656 
    657 	if (cache == NULL) {
    658 		cache = cache_create(4096);
    659 		kore_buf_append(&cache->buf,
    660 		    asset_index_top_html, asset_len_index_top_html);
    661 
    662 		TAILQ_FOREACH(post, &posts, list) {
    663 			if (post->flags != flags)
    664 				continue;
    665 			kore_buf_appendf(&cache->buf,
    666 			    (const char *)asset_index_entry_html,
    667 			    type, post->uri, post->title);
    668 		}
    669 
    670 		kore_buf_appendf(&cache->buf,
    671 		    (const char *)asset_blog_version_html, BLOG_VER);
    672 		kore_buf_append(&cache->buf,
    673 		    asset_index_end_html, asset_len_index_end_html);
    674 
    675 		*ptr = cache;
    676 	}
    677 
    678 	cache->refs++;
    679 
    680 	http_response_header(req, "content-type", "text/html; charset=utf-8");
    681 	http_response_stream(req, HTTP_STATUS_OK, cache->buf.data,
    682 	    cache->buf.offset, cache_sent, cache);
    683 
    684 	return (KORE_RESULT_OK);
    685 }
    686 
    687 int
    688 draft_render(struct http_request *req)
    689 {
    690 	return (post_send(req, "/drafts/", POST_FLAG_DRAFT));
    691 }
    692 
    693 int
    694 post_render(struct http_request *req)
    695 {
    696 	return (post_send(req, "/posts/", 0));
    697 }
    698 
    699 int
    700 post_send(struct http_request *req, const char *path, int flags)
    701 {
    702 	const char	*uri;
    703 	struct post	*post;
    704 	int		redirect;
    705 
    706 	if (req->method != HTTP_METHOD_GET) {
    707 		http_response_header(req, "allow", "get");
    708 		http_response(req, HTTP_STATUS_BAD_REQUEST, NULL, 0);
    709 		return (KORE_RESULT_OK);
    710 	}
    711 
    712 	if (strlen(req->path) <= strlen(path)) {
    713 		http_response(req, HTTP_STATUS_INTERNAL_ERROR, NULL, 0);
    714 		return (KORE_RESULT_OK);
    715 	}
    716 
    717 	post = NULL;
    718 	redirect = 0;
    719 	uri = req->path + strlen(path);
    720 
    721 	TAILQ_FOREACH(post, &posts, list) {
    722 		if (post->flags != flags)
    723 			continue;
    724 		if (!strcmp(post->uri, uri))
    725 			break;
    726 	}
    727 
    728 	if (post == NULL) {
    729 		redirect++;
    730 	} else if (post->cache == NULL) {
    731 		redirect++;
    732 		kore_log(LOG_ERR, "no cache for %s", post->uri);
    733 	}
    734 
    735 	if (redirect) {
    736 		http_response_header(req, "location", "/");
    737 		http_response(req, HTTP_STATUS_FOUND, NULL, 0);
    738 		return (KORE_RESULT_OK);
    739 	}
    740 
    741 	post->cache->refs++;
    742 
    743 	http_response_header(req, "content-type", "text/html; charset=utf-8");
    744 	http_response_stream(req, HTTP_STATUS_OK, post->cache->buf.data,
    745 	    post->cache->buf.offset, cache_sent, post->cache);
    746 
    747 	return (KORE_RESULT_OK);
    748 }