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 }