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 (15309B)



      1 /*
      2  * Copyright (c) 2018 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.1"
     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 	if (blog_sig == SIGHUP) {
    126 		blog_sig = -1;
    127 		index_rebuild();
    128 		user_reload();
    129 	}
    130 }
    131 
    132 void
    133 kore_worker_configure(void)
    134 {
    135 	struct sigaction	sa;
    136 
    137 	memset(&sa, 0, sizeof(sa));
    138 	sa.sa_handler = signal_handler;
    139 
    140 	if (sigfillset(&sa.sa_mask) == -1)
    141 		fatal("sigfillset: %s", errno_s);
    142 	if (sigaction(SIGHUP, &sa, NULL) == -1)
    143 		fatal("sigaction: %s", errno_s);
    144 
    145 	(void)kore_timer_add(tick, 1000, NULL, 0);
    146 
    147 	TAILQ_INIT(&posts);
    148 	TAILQ_INIT(&users);
    149 
    150 	index_rebuild();
    151 	user_reload();
    152 
    153 	kore_msg_register(MSG_SESSION_ADD, auth_session_add);
    154 	kore_msg_register(MSG_SESSION_DEL, auth_session_del);
    155 }
    156 
    157 struct cache *
    158 cache_create(size_t len)
    159 {
    160 	struct cache		*cache;
    161 
    162 	cache = kore_calloc(1, sizeof(*cache));
    163 
    164 	cache->refs++;
    165 	kore_buf_init(&cache->buf, len);
    166 
    167 	return (cache);
    168 }
    169 
    170 void
    171 cache_ref_drop(struct cache **ptr)
    172 {
    173 	struct cache	*cache = *ptr;
    174 
    175 	cache->refs--;
    176 
    177 	if (cache->refs == 0) {
    178 		kore_buf_cleanup(&cache->buf);
    179 		kore_free(cache);
    180 		*ptr = NULL;
    181 	}
    182 }
    183 
    184 int
    185 cache_sent(struct netbuf *nb)
    186 {
    187 	struct cache	*cache = (struct cache *)nb->extra;
    188 
    189 	cache_ref_drop(&cache);
    190 
    191 	return (KORE_RESULT_OK);
    192 }
    193 
    194 int
    195 fts_compare(const FTSENT **a, const FTSENT **b)
    196 {
    197 	const FTSENT	*a1 = *a;
    198 	const FTSENT	*b1 = *b;
    199 
    200 	if (a1->fts_statp->st_mtime > b1->fts_statp->st_mtime)
    201 		return (-1);
    202 
    203 	if (a1->fts_statp->st_mtime < b1->fts_statp->st_mtime)
    204 		return (1);
    205 
    206 	return (0);
    207 }
    208 
    209 void
    210 user_reload(void)
    211 {
    212 	struct stat	st;
    213 	FILE		*fp;
    214 	u_int32_t	uids;
    215 	struct user	*user;
    216 	int		lineno;
    217 	char		*line, *pwd, buf[256];
    218 
    219 	if (stat(BLOG_USER_CONF, &st) == -1) {
    220 		if (errno != ENOENT) {
    221 			kore_log(LOG_INFO,
    222 			    "stat(%s): %s", BLOG_USER_CONF, errno_s);
    223 		}
    224 		return;
    225 	}
    226 
    227 	if (user_mtime == st.st_mtime)
    228 		return;
    229 
    230 	while (!TAILQ_EMPTY(&users)) {
    231 		user = TAILQ_FIRST(&users);
    232 		TAILQ_REMOVE(&users, user, list);
    233 		kore_free(user->passphrase);
    234 		kore_free(user->name);
    235 		kore_free(user);
    236 	}
    237 
    238 	TAILQ_INIT(&users);
    239 
    240 	if ((fp = fopen(BLOG_USER_CONF, "r")) == NULL) {
    241 		if (errno != ENOENT) {
    242 			kore_log(LOG_INFO,
    243 			    "fopen(%s): %s", BLOG_USER_CONF, errno_s);
    244 		}
    245 		return;
    246 	}
    247 
    248 	kore_log(LOG_INFO, "reloading users");
    249 
    250 	uids = 1;
    251 	lineno = 0;
    252 
    253 	while ((line = kore_read_line(fp, buf, sizeof(buf))) != NULL) {
    254 		lineno++;
    255 
    256 		if (*line == '\0')
    257 			continue;
    258 
    259 		if ((pwd = strchr(line, ':')) == NULL) {
    260 			kore_log(LOG_INFO, "malformed user @ %d", lineno);
    261 			continue;
    262 		}
    263 
    264 		*(pwd)++ = '\0';
    265 
    266 		if (*line == '\0' || *pwd == '\0') {
    267 			kore_log(LOG_INFO, "malformed user @ %d", lineno);
    268 			continue;
    269 		}
    270 
    271 		user = kore_calloc(1, sizeof(*user));
    272 		user->uid = uids++;
    273 		user->name = kore_strdup(line);
    274 		user->passphrase = kore_strdup(pwd);
    275 		TAILQ_INSERT_TAIL(&users, user, list);
    276 	}
    277 
    278 	fclose(fp);
    279 	user_mtime = st.st_mtime;
    280 }
    281 
    282 void
    283 index_rebuild(void)
    284 {
    285 	FTSENT		*fe;
    286 	FTS		*fts;
    287 	struct post	*post;
    288 	char		*path[] = { BLOG_DIR, NULL };
    289 
    290 	kore_log(LOG_INFO, "rebuilding post list");
    291 
    292 	if (live_index != NULL)
    293 		cache_ref_drop(&live_index);
    294 
    295 	if (draft_index != NULL)
    296 		cache_ref_drop(&draft_index);
    297 
    298 	if ((fts = fts_open(path,
    299 	    FTS_NOCHDIR | FTS_PHYSICAL, fts_compare)) == NULL) {
    300 		kore_log(LOG_ERR, "fts_open(): %s", errno_s);
    301 		return;
    302 	}
    303 
    304 	while (!TAILQ_EMPTY(&posts)) {
    305 		post = TAILQ_FIRST(&posts);
    306 		post_remove(post);
    307 	}
    308 
    309 	TAILQ_INIT(&posts);
    310 
    311 	while ((fe = fts_read(fts)) != NULL) {
    312 		if (!S_ISREG(fe->fts_statp->st_mode))
    313 			continue;
    314 		if ((post = post_register(fe->fts_accpath)) != NULL)
    315 			post_cache(post);
    316 	}
    317 
    318 	fts_close(fts);
    319 }
    320 
    321 void
    322 post_cache(struct post *post)
    323 {
    324 	int		fd;
    325 	struct stat	st;
    326 	ssize_t		bytes;
    327 	u_int8_t	buf[4096];
    328 
    329 	if ((fd = open(post->file, O_RDONLY)) == -1) {
    330 		kore_log(LOG_ERR, "failed to open '%s' (%s)",
    331 		    post->file, errno_s);
    332 		post_remove(post);
    333 		return;
    334 	}
    335 
    336 	if (fstat(fd, &st) == -1) {
    337 		kore_log(LOG_ERR, "fstat(%s): %s", post->file, errno_s);
    338 		post_remove(post);
    339 		return;
    340 	}
    341 
    342 	post->mtime = st.st_mtime;
    343 	post->cache = cache_create(st.st_size);
    344 
    345 	kore_buf_appendf(&post->cache->buf,
    346 	    (const char *)asset_post_start_html, post->title, post->title);
    347 
    348 	post->clen = 0;
    349 	post->coff = post->cache->buf.offset;
    350 
    351 	for (;;) {
    352 		bytes = read(fd, buf, sizeof(buf));
    353 		if (bytes == -1) {
    354 			if (errno == EINTR)
    355 				continue;
    356 			kore_log(LOG_ERR, "read(%s): %s", post->file, errno_s);
    357 			post_remove(post);
    358 			close(fd);
    359 			return;
    360 		}
    361 
    362 		if (bytes == 0)
    363 			break;
    364 
    365 		post->clen += bytes;
    366 		kore_buf_append(&post->cache->buf, buf, bytes);
    367 	}
    368 
    369 	close(fd);
    370 
    371 	kore_buf_appendf(&post->cache->buf,
    372 	    (const char *)asset_blog_version_html, BLOG_VER);
    373 
    374 	kore_buf_append(&post->cache->buf, asset_post_end_html,
    375 	    asset_len_post_end_html);
    376 }
    377 
    378 struct post *
    379 post_register(char *path)
    380 {
    381 	struct post	*post;
    382 	int		invalid;
    383 	char		*p, *fpath, *uri, title[128];
    384 
    385 	if (strlen(path) <= (strlen(BLOG_DIR) + 1))
    386 		fatal("invalid path from fts_read()?");
    387 
    388 	uri = path + strlen(BLOG_DIR) + 1;
    389 	if (uri[0] == '.' || uri[0] == '\0')
    390 		return (NULL);
    391 
    392 	fpath = kore_strdup(path);
    393 	if ((p = strrchr(path, '.')) != NULL)
    394 		*p = '\0';
    395 
    396 	if (kore_strlcpy(title, uri, sizeof(title)) >= sizeof(title)) {
    397 		kore_free(fpath);
    398 		kore_log(LOG_ERR, "blog name (title) '%s' too long", uri);
    399 		return (NULL);
    400 	}
    401 
    402 	invalid = 0;
    403 	for (p = &uri[0]; *p != '\0'; p++) {
    404 		if (*p == ' ' || *p == '-' || *p == '_') {
    405 			*p = '-';
    406 			continue;
    407 		}
    408 
    409 		if (!isalnum(*(unsigned char *)p)) {
    410 			invalid++;
    411 			continue;
    412 		}
    413 
    414 		if (*p >= 'A' && *p <= 'Z')
    415 			*p += 0x20;
    416 	}
    417 
    418 	if (invalid) {
    419 		kore_free(fpath);
    420 		kore_log(LOG_ERR, "'%s' contains invalid characters", fpath);
    421 		return (NULL);
    422 	}
    423 
    424 	post = kore_calloc(1, sizeof(*post));
    425 	post->flags = 0;
    426 	post->file = fpath;
    427 	post->uri = kore_strdup(uri);
    428 	post->title = kore_strdup(title);
    429 	TAILQ_INSERT_TAIL(&posts, post, list);
    430 
    431 	if (strstr(post->file, "draft"))
    432 		post->flags |= POST_FLAG_DRAFT;
    433 
    434 	return (post);
    435 }
    436 
    437 void
    438 post_remove(struct post *post)
    439 {
    440 	cache_ref_drop(&post->cache);
    441 
    442 	TAILQ_REMOVE(&posts, post, list);
    443 	kore_free(post->title);
    444 	kore_free(post->file);
    445 	kore_free(post->uri);
    446 	kore_free(post);
    447 }
    448 
    449 int
    450 referer(struct http_request *req, const void *unused)
    451 {
    452 	const char		*ref, *p;
    453 
    454 	if (!http_request_header(req, "referer", &ref))
    455 		return (KORE_RESULT_OK);
    456 
    457 	p = ref;
    458 
    459 	while (*p != '\0') {
    460 		if (!isprint(*(const unsigned char *)p++)) {
    461 			ref = "[not printable]";
    462 			break;
    463 		}
    464 	}
    465 
    466 	kore_log(LOG_NOTICE, "blog (%s) visit from %s", req->path, ref);
    467 
    468 	return (KORE_RESULT_OK);
    469 }
    470 
    471 int
    472 auth_login(struct http_request *req)
    473 {
    474 	size_t			i;
    475 	int			len;
    476 	struct user		*up;
    477 	struct session		session;
    478 	char			*user, *pass;
    479 	u_int8_t		buf[BLOG_SESSION_LEN];
    480 
    481 	if (req->method == HTTP_METHOD_GET)
    482 		return (asset_serve_login_html(req));
    483 
    484 	http_populate_post(req);
    485 
    486 	if (!http_argument_get_string(req, "user", &user) ||
    487 	    !http_argument_get_string(req, "passphrase", &pass)) {
    488 		req->method = HTTP_METHOD_GET;
    489 		return (asset_serve_login_html(req));
    490 	}
    491 
    492 	up = NULL;
    493 	TAILQ_FOREACH(up, &users, list) {
    494 		if (!strcmp(user, up->name))
    495 			break;
    496 	}
    497 
    498 	if (up == NULL) {
    499 		req->method = HTTP_METHOD_GET;
    500 		kore_log(LOG_INFO, "auth_login: no user data?");
    501 		return (asset_serve_login_html(req));
    502 	}
    503 
    504 	if (crypto_pwhash_str_verify(up->passphrase, pass, strlen(pass)) != 0) {
    505 		req->method = HTTP_METHOD_GET;
    506 		return (asset_serve_login_html(req));
    507 	}
    508 
    509 	session.uid = up->uid;
    510 	memset(session.data, 0, sizeof(session.data));
    511 
    512 	randombytes_buf(buf, sizeof(buf));
    513 	for (i = 0; i < sizeof(buf); i++) {
    514 		len = snprintf(session.data + (i * 2),
    515 		    sizeof(session.data) - (i * 2), "%02x", buf[i]);
    516 		if (len == -1 || (size_t)len >= sizeof(session.data)) {
    517 			kore_log(LOG_ERR, "failed to hexify session");
    518 			req->method = HTTP_METHOD_GET;
    519 			return (asset_serve_login_html(req));
    520 		}
    521 	}
    522 
    523 	kore_msg_send(KORE_MSG_WORKER_ALL, MSG_SESSION_ADD,
    524 	    &session, sizeof(session));
    525 
    526 	http_response_header(req, "location", "/drafts/");
    527 	http_response_cookie(req, "blog_token", session.data,
    528 	    "/drafts/", 0, 0, NULL);
    529 
    530 	kore_log(LOG_INFO, "login for '%s'", up->name);
    531 	http_response(req, HTTP_STATUS_FOUND, NULL, 0);
    532 
    533 	return (KORE_RESULT_OK);
    534 }
    535 
    536 int
    537 auth_user_exists(struct http_request *req, char *user)
    538 {
    539 	struct user	*usr;
    540 
    541 	if (user == NULL)
    542 		return (KORE_RESULT_ERROR);
    543 
    544 	TAILQ_FOREACH(usr, &users, list) {
    545 		if (!strcmp(usr->name, user))
    546 			return (KORE_RESULT_OK);
    547 	}
    548 
    549 	return (KORE_RESULT_ERROR);
    550 }
    551 
    552 void
    553 auth_session_add(struct kore_msg *msg, const void *data)
    554 {
    555 	struct user		*user;
    556 	const struct session	*session;
    557 
    558 	if (msg->length != sizeof(*session)) {
    559 		kore_log(LOG_ERR, "auth_session_add: invalid len (%u)",
    560 		    msg->length);
    561 		return;
    562 	}
    563 
    564 	session = data;
    565 
    566 	TAILQ_FOREACH(user, &users, list) {
    567 		if (user->uid == session->uid) {
    568 			memcpy(&user->session, session, sizeof(*session));
    569 			break;
    570 		}
    571 	}
    572 }
    573 
    574 void
    575 auth_session_del(struct kore_msg *msg, const void *data)
    576 {
    577 	u_int32_t	uid;
    578 	struct user	*user;
    579 
    580 	if (msg->length != sizeof(uid)) {
    581 		kore_log(LOG_ERR, "auth_session_del: invalid len (%u)",
    582 		    msg->length);
    583 		return;
    584 	}
    585 
    586 	memcpy(&uid, data, sizeof(uid));
    587 
    588 	TAILQ_FOREACH(user, &users, list) {
    589 		if (user->uid == uid) {
    590 			memset(&user->session, 0, sizeof(user->session));
    591 			break;
    592 		}
    593 	}
    594 }
    595 
    596 int
    597 auth_session(struct http_request *req, const char *cookie)
    598 {
    599 	struct user	*user;
    600 
    601 	if (cookie == NULL)
    602 		return (KORE_RESULT_ERROR);
    603 
    604 	TAILQ_FOREACH(user, &users, list) {
    605 		if (!strcmp(user->session.data, cookie)) {
    606 			kore_log(LOG_INFO, "%s requested by %s",
    607 			    req->path, user->name);
    608 			return (KORE_RESULT_OK);
    609 		}
    610 	}
    611 
    612 	return (KORE_RESULT_ERROR);
    613 }
    614 
    615 int
    616 redirect(struct http_request *req)
    617 {
    618 	http_response_header(req, "location", "/");
    619 	http_response(req, HTTP_STATUS_FOUND, NULL, 0);
    620 	return (KORE_RESULT_OK);
    621 }
    622 
    623 int
    624 post_list(struct http_request *req)
    625 {
    626 	return (list_posts(req, "posts", &live_index, 0));
    627 }
    628 
    629 int
    630 draft_list(struct http_request *req)
    631 {
    632 	return (list_posts(req, "drafts", &draft_index, POST_FLAG_DRAFT));
    633 }
    634 
    635 int
    636 list_posts(struct http_request *req, const char *type, struct cache **ptr,
    637     int flags)
    638 {
    639 	struct post		*post;
    640 	struct cache		*cache;
    641 
    642 	if (req->method != HTTP_METHOD_GET) {
    643 		http_response_header(req, "allow", "get");
    644 		http_response(req, HTTP_STATUS_BAD_REQUEST, NULL, 0);
    645 		return (KORE_RESULT_OK);
    646 	}
    647 
    648 	cache = *ptr;
    649 
    650 	if (cache == NULL) {
    651 		cache = cache_create(4096);
    652 		kore_buf_append(&cache->buf,
    653 		    asset_index_top_html, asset_len_index_top_html);
    654 
    655 		TAILQ_FOREACH(post, &posts, list) {
    656 			if (post->flags != flags)
    657 				continue;
    658 			kore_buf_appendf(&cache->buf,
    659 			    (const char *)asset_index_entry_html,
    660 			    type, post->uri, post->title);
    661 		}
    662 
    663 		kore_buf_appendf(&cache->buf,
    664 		    (const char *)asset_blog_version_html, BLOG_VER);
    665 		kore_buf_append(&cache->buf,
    666 		    asset_index_end_html, asset_len_index_end_html);
    667 
    668 		*ptr = cache;
    669 	}
    670 
    671 	cache->refs++;
    672 
    673 	http_response_header(req, "content-type", "text/html; charset=utf-8");
    674 	http_response_stream(req, HTTP_STATUS_OK, cache->buf.data,
    675 	    cache->buf.offset, cache_sent, cache);
    676 
    677 	return (KORE_RESULT_OK);
    678 }
    679 
    680 int
    681 draft_render(struct http_request *req)
    682 {
    683 	return (post_send(req, "/drafts/", POST_FLAG_DRAFT));
    684 }
    685 
    686 int
    687 post_render(struct http_request *req)
    688 {
    689 	return (post_send(req, "/posts/", 0));
    690 }
    691 
    692 int
    693 post_send(struct http_request *req, const char *path, int flags)
    694 {
    695 	const char	*uri;
    696 	struct post	*post;
    697 	int		redirect;
    698 
    699 	if (req->method != HTTP_METHOD_GET) {
    700 		http_response_header(req, "allow", "get");
    701 		http_response(req, HTTP_STATUS_BAD_REQUEST, NULL, 0);
    702 		return (KORE_RESULT_OK);
    703 	}
    704 
    705 	if (strlen(req->path) <= strlen(path)) {
    706 		http_response(req, HTTP_STATUS_INTERNAL_ERROR, NULL, 0);
    707 		return (KORE_RESULT_OK);
    708 	}
    709 
    710 	post = NULL;
    711 	redirect = 0;
    712 	uri = req->path + strlen(path);
    713 
    714 	TAILQ_FOREACH(post, &posts, list) {
    715 		if (post->flags != flags)
    716 			continue;
    717 		if (!strcmp(post->uri, uri))
    718 			break;
    719 	}
    720 
    721 	if (post == NULL) {
    722 		redirect++;
    723 	} else if (post->cache == NULL) {
    724 		redirect++;
    725 		kore_log(LOG_ERR, "no cache for %s", post->uri);
    726 	}
    727 
    728 	if (redirect) {
    729 		http_response_header(req, "location", "/");
    730 		http_response(req, HTTP_STATUS_FOUND, NULL, 0);
    731 		return (KORE_RESULT_OK);
    732 	}
    733 
    734 	post->cache->refs++;
    735 
    736 	http_response_header(req, "content-type", "text/html; charset=utf-8");
    737 	http_response_stream(req, HTTP_STATUS_OK, post->cache->buf.data,
    738 	    post->cache->buf.offset, cache_sent, post->cache);
    739 
    740 	return (KORE_RESULT_OK);
    741 }