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 }