server: let some boards be exempt from /recent
[rb-79.git] / rb79-server.c
blobdf4855b54829ab496cd42e7effdcd995bb9d8e6a
1 /*
2 * Copyright (c) 2017-2020, De Rais <derais@cock.li>
4 * Permission to use, copy, modify, and/or distribute this software for
5 * any purpose with or without fee is hereby granted, provided that the
6 * above copyright notice and this permission notice appear in all
7 * copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
13 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
14 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
15 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 * PERFORMANCE OF THIS SOFTWARE.
18 #include <limits.h>
19 #include <locale.h>
20 #include <stdint.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <strings.h>
25 #include <time.h>
26 #include <unistd.h>
28 #include <fcgiapp.h>
30 #include "macros.h"
31 #include "rb79.h"
33 #include "config.h"
35 const char *program_name = "rb79-server";
37 /* Print out a page saying the request was malformed (400) */
38 static int
39 report_bad_request(FCGX_Request *r, const char *reason)
41 if (!reason) {
42 reason = "That's not a real request. That's all we know.";
45 FCGX_FPrintF(r->out, BAD_REQUEST_FMT, reason);
47 return 0;
50 /* Print out a page saying they failed the CAPTCHA (403) */
51 static int
52 report_bad_challenge(FCGX_Request *r)
54 FCGX_FPrintF(r->out, BAD_CHALLENGE_FMT);
56 return 0;
59 /* Print out a BANNED page (403) */
60 static int
61 report_ban(FCGX_Request *r, char *ban_until, char *ban_reason)
63 FCGX_FPrintF(r->out, BAN_FMT, UBSAFES(ban_until), UBSAFES(ban_reason));
65 return 0;
68 /* Print out a BAD METHOD page (405) */
69 static int
70 report_bad_method(FCGX_Request *r)
72 FCGX_FPrintF(r->out, BAD_METHOD_FMT);
74 return 0;
77 /* Print out a FILE TOO LARGE page (413) */
78 static int
79 report_too_large(FCGX_Request *r, const char *large_thing)
81 FCGX_FPrintF(r->out, TOO_LARGE_FMT, UBSAFES(large_thing));
83 return 0;
86 /* Print out a page saying they're posting too fast (429) */
87 static int
88 report_cooldown(FCGX_Request *r, char *cooldown_length)
90 FCGX_FPrintF(r->out, COOLDOWN_FMT, UBSAFES(cooldown_length));
92 return 0;
95 /* Print out an INTERNAL ERROR page (500) */
96 static int
97 report_internal_error(FCGX_Request *r)
99 FCGX_FPrintF(r->out, INTERNAL_ERROR_FMT);
101 return 0;
104 /* Print out a POST SUCCESSFUL page (200) */
105 static int
106 report_post_successful(FCGX_Request *r, const char *buf)
108 FCGX_FPrintF(r->out, POST_SUCCESSFUL_FMT, buf);
110 return 0;
113 /* Make sure every board has a page (really only for brand-new boards) */
114 static int
115 board_pages_init(struct configuration *conf)
117 int ret = -1;
118 uintmax_t *thread_ids = 0;
119 size_t thread_ids_num = 0;
120 size_t board_pages_num = 0;
122 for (size_t j = 0; j < conf->boards_num; ++j) {
123 free(thread_ids);
124 thread_ids = 0;
126 if (lock_acquire(j) < 0) {
127 goto done;
130 if (db_cull_and_report_threads(j, &thread_ids, &thread_ids_num,
131 &board_pages_num) < 0) {
132 goto done;
135 if (wt_write_board(j, thread_ids, thread_ids_num,
136 board_pages_num) < 0) {
137 goto done;
140 lock_release(j);
143 ret = 0;
144 done:
145 free(thread_ids);
147 return ret;
150 /* Free what needs to be freed */
151 static void
152 clean_post_cmd(struct post_cmd *p)
154 if (!p) {
155 return;
158 free(p->raw.action);
159 free(p->raw.board);
160 free(p->raw.thread);
161 free(p->raw.post);
162 free(p->raw.name);
163 free(p->raw.email);
164 free(p->raw.tripcode);
165 free(p->raw.subject);
166 free(p->raw.comment);
167 free(p->raw.file_name);
168 free(p->raw.file_contents);
169 free(p->raw.challenge_id);
170 free(p->raw.challenge_response);
171 free(p->prepared.name);
172 free(p->prepared.email);
173 free(p->prepared.tripcode);
174 free(p->prepared.subject);
175 free(p->prepared.comment);
176 free(p->prepared.ext);
177 free(p->prepared.file_name);
178 free(p->prepared.system_full_path);
179 free(p->prepared.system_thumb_path);
180 free(p->prepared.file_info);
181 free(p->scannable_comment);
182 free(p->position_map);
183 *p = (struct post_cmd) { 0 };
186 /* The bulk of work for processing a post */
187 static void
188 handle_op_or_reply(struct configuration *conf, FCGX_Request *r, struct
189 post_cmd *pc, const char *ip, size_t parent_thread)
191 char *buf = 0;
192 char *abs_file_path = 0;
193 size_t len = 0;
194 int our_fault = 0;
195 uintmax_t real_thread = 0;
196 int cooldown = 0;
197 int thread_dne = 0;
198 int thread_full = 0;
199 int thread_closed = 0;
200 const struct filetype *f;
201 size_t board_pages_num = 0;
202 uintmax_t *thread_ids = 0;
203 size_t thread_ids_num = 0;
204 uint_fast8_t need_to_unlock = 0;
206 if (!parent_thread &&
207 (!pc->raw.file_contents ||
208 !pc->raw.file_contents_len)) {
209 LOG("New thread, yet no file (400)");
210 report_bad_request(r, "New threads must have a file");
211 goto done;
214 /* pc comes in with a bunch of these lens set not-as-desired */
215 if (pc->raw.file_name_len > conf->max_text_len) {
216 LOG("File name length (%zu) larger than max (%zu) (413)",
217 pc->raw.file_name_len, conf->max_text_len);
218 report_too_large(r, "Filename");
219 goto done;
222 if (pc->raw.subject_len > conf->max_text_len) {
223 LOG("Subject length (%zu) larger than max (%zu) (413)",
224 pc->raw.subject_len, conf->max_text_len);
225 report_too_large(r, "Subject text");
226 goto done;
229 if (pc->raw.email_len > conf->max_text_len) {
230 LOG("Email length (%zu) larger than max (%zu) (413)",
231 pc->raw.email_len, conf->max_text_len);
232 report_too_large(r, "Email address");
233 goto done;
236 if (pc->raw.comment_len > conf->max_text_len) {
237 LOG("Comment length (%zu) larger than max (%zu) (413)",
238 pc->raw.comment_len, conf->max_text_len);
239 report_too_large(r, "Comment text");
240 goto done;
243 if (pc->raw.file_contents_len > conf->max_file_size) {
244 LOG("File size (%zu) larger than max (%zu) (413)",
245 pc->raw.file_contents_len, conf->max_file_size);
246 report_too_large(r, "File size");
247 goto done;
250 if (sf_check_mime_type(pc->raw.file_contents, pc->raw.file_contents_len,
251 &f) < 0) {
252 LOG("Bad MIME check (400)");
253 report_bad_request(r, "Unsupported file type");
254 goto done;
257 /* Calculate tripcodes before HTML-escaping everything */
258 if (tripcodes_calculate(pc) < 0) {
259 LOG("Error in tripcodes_calculate (500)");
260 report_internal_error(r);
261 goto done;
264 /* HTML-escape, wordfilter, linkify */
265 if (st_sanitize_text(pc, &our_fault) < 0) {
266 if (our_fault) {
267 LOG("Error in st_sanitize_text (500)");
268 report_internal_error(r);
269 goto done;
272 LOG("Bad text (400)");
273 LOG("Comment was \"%s\"", UBSAFES(pc->raw.comment));
274 report_bad_request(r, "Disallowed text");
275 goto done;
278 cooldown = pc->prepared.comment_len ?
279 conf->boards[pc->board_idx].text_cooldown :
280 conf->boards[pc->board_idx].blank_cooldown;
283 * From now on, everything must be under lock, since we
284 * could be touching the filesystem. Strictly, we don't
285 * need to worry about locking for db-only operations, so
286 * this could be delayed a bit.
288 if (lock_acquire(pc->board_idx) < 0) {
289 LOG("Error in lock_acquire (500)");
290 report_internal_error(r);
291 goto done;
294 need_to_unlock = 1;
296 if (db_insert_post(ip, parent_thread, cooldown, pc, &thread_dne,
297 &thread_closed, &thread_full, &pc->prepared.id) <
298 0) {
299 LOG("Error in insert_post (500)");
300 report_internal_error(r);
301 goto done;
304 LOG("Post %ju on board /%s/", pc->prepared.id,
305 conf->boards[pc->board_idx].name);
307 if (thread_dne) {
308 LOG("Thread %zu does not exist (400)", (size_t) 0);
309 report_bad_request(r, "Thread does not exist");
310 goto done;
313 if (thread_full) {
314 LOG("Thread %zu is full (400)", (size_t) 0);
315 report_bad_request(r, "Thread is full");
316 goto done;
319 if (thread_closed) {
320 LOG("Thread %zu is closed (400)", (size_t) 0);
321 report_bad_request(r, "Thread is closed");
322 goto done;
325 /* Make thumbnails and insert them */
326 if (f) {
327 if (sf_install_files(pc->board_idx, pc->raw.file_contents,
328 pc->raw.file_contents_len,
329 &pc->prepared.now, f, &abs_file_path,
330 &pc->prepared.system_full_path,
331 &pc->prepared.system_full_path_len,
332 &pc->prepared.system_thumb_path,
333 &pc->prepared.system_thumb_path_len,
334 &our_fault) < 0) {
335 if (our_fault) {
336 LOG("Error in sf_install_files (500)");
337 report_internal_error(r);
338 goto done;
341 LOG("Couldn't install files (400)");
342 report_bad_request(r, "Bad file upload");
343 goto done;
346 /* ... and now that they're inserted, describe them ... */
347 if (sf_describe_file(f->mime_type, abs_file_path,
348 &pc->prepared.file_info,
349 &pc->prepared.file_info_len) < 0) {
350 LOG("Error in sf_describe_file (500)");
351 report_internal_error(r);
352 goto done;
355 /* ... and alert the db about that description. */
356 if (db_update_file_info(pc->board_idx, pc->prepared.id,
357 pc->prepared.file_info,
358 pc->prepared.file_info_len,
359 pc->prepared.system_full_path,
360 pc->prepared.system_full_path_len,
361 pc->prepared.system_thumb_path,
362 pc->prepared.system_thumb_path_len) <
363 0) {
364 LOG("Error in db_update_post_description (500)");
365 report_internal_error(r);
366 goto done;
371 * We're about ready to write out the threads, boards, etc.
372 * Therefore, we must now check for thread culling, and
373 * also calculate how many board pages we need.
375 if (db_cull_and_report_threads(pc->board_idx, &thread_ids,
376 &thread_ids_num, &board_pages_num) < 0) {
377 LOG("Error in db_cull_and_report_threads (500)");
378 report_internal_error(r);
379 goto done;
382 real_thread = parent_thread ? parent_thread : pc->prepared.id;
384 if (wt_write_thread(pc->board_idx, real_thread) < 0) {
385 LOG("Error in wt_write_thread (500)");
386 report_internal_error(r);
387 goto done;
390 if (wt_write_board(pc->board_idx, thread_ids, thread_ids_num,
391 board_pages_num) < 0) {
392 LOG("Error in wt_write_board (500)");
393 report_internal_error(r);
394 goto done;
397 len = snprintf(0, 0, "/%s/res/%s", pc->raw.board, pc->raw.thread);
399 if (len + 1 < len) {
400 ERROR_MESSAGE("overflow");
401 report_internal_error(r);
402 goto done;
405 if (!(buf = malloc(len + 1))) {
406 PERROR_MESSAGE("malloc");
407 report_internal_error(r);
408 goto done;
411 if (pc->raw.email &&
412 !strcmp(pc->raw.email, "noko")) {
413 sprintf(buf, "/%s/res/%s", pc->raw.board, pc->raw.thread);
414 } else {
415 sprintf(buf, "/%s", pc->raw.board);
418 report_post_successful(r, buf);
419 done:
421 if (need_to_unlock) {
422 lock_release(pc->board_idx);
425 free(buf);
426 free(abs_file_path);
427 free(thread_ids);
430 /* Rebuild every thread and every board */
431 static void
432 handle_rebuild (struct configuration *conf, FCGX_Request *r)
434 uint_fast8_t had_errors = util_rebuild(conf);
436 FCGX_FPrintF(r->out, "Status: 200\r\nContent-type: text/plain\r\n\r\n"
437 "Rebuild complete%s\n", had_errors ?
438 " with errors" : "");
440 return;
443 /* Figure out what they want us to do */
444 static void
445 handle(struct configuration *conf, FCGX_Request *r)
447 char *p = 0;
448 char *content_type = 0;
449 char *content_len_str = 0;
450 size_t content_len = 0;
451 char *buf_main = 0;
452 size_t buf_main_len = 0;
453 const char *content_type_prefix = "Content-Type: ";
454 struct post_cmd post_cmd = { 0 };
455 const char *ip_raw = FCGX_GetParam("REMOTE_ADDR", r->envp);
456 char *ip = 0;
457 char *ban_reason = 0;
458 char *ban_until = 0;
459 char *cooldown_length = 0;
460 uint_fast8_t found_idx = 0;
462 /* In case someone is trying for a time GET, prioritize that */
463 time(&post_cmd.prepared.now);
464 LOG("-----------------------------------------");
465 LOG("Handling post at %zu from %s", (size_t) post_cmd.prepared.now,
466 UBSAFES(ip_raw));
468 if (!ip_raw) {
469 LOG("Couldn't get REMOTE_ADDR (500)");
470 report_internal_error(r);
471 goto done;
474 if (util_normalize_ip(ip_raw, &ip) < 0) {
475 LOG("Couldn't normalize ip (500)");
476 report_internal_error(r);
477 goto done;
480 /* You can only POST to /action */
481 if (!(p = FCGX_GetParam("REQUEST_METHOD", r->envp))) {
482 LOG("Couldn't get request method (500)");
483 report_internal_error(r);
484 goto done;
487 if (strcmp(p, "POST")) {
488 LOG("request method was not POST (405)");
489 report_bad_method(r);
490 goto done;
493 /* We have to somehow feed this into multipart */
494 if (!(content_type = FCGX_GetParam("CONTENT_TYPE", r->envp))) {
495 LOG("Can't get CONTENT_TYPE (500)");
496 report_internal_error(r);
497 goto done;
500 if (!(content_len_str = FCGX_GetParam("CONTENT_LENGTH", r->envp))) {
501 LOG("Can't get CONTENT_LENGTH (500)");
502 report_internal_error(r);
503 goto done;
506 content_len = (size_t) strtoll(content_len_str, 0, 0);
508 if (content_len > max_form_data_size) {
509 LOG("Buffer would have exceeded %zuB (413)",
510 max_form_data_size);
511 report_too_large(r, "Total POST");
512 goto done;
515 buf_main_len = strlen(content_type_prefix) + strlen(content_type) +
516 strlen("\r\n\r\n") + content_len;
518 if (buf_main_len + 1 < buf_main_len) {
519 ERROR_MESSAGE("overflow");
520 goto done;
523 if (!(buf_main = malloc(buf_main_len + 1))) {
524 PERROR_MESSAGE("malloc");
525 goto done;
528 size_t offset = sprintf(buf_main, "%s%s\r\n\r\n", content_type_prefix,
529 content_type);
531 /* Try and swallow this thing into a buffer */
532 FCGX_GetStr(buf_main + offset, content_len, r->in);
534 /* Okay, we've got it in the buffer */
535 if (multipart_decompose(buf_main, buf_main_len, &post_cmd) < 0) {
536 LOG("Decoding message failed, returning (400)");
537 report_bad_request(r, "Invalid multipart/form-data");
538 goto done;
541 /* Now we can check what they actually wanted us to DO */
542 if (!post_cmd.raw.action) {
543 LOG("No action specified (400)");
544 report_bad_request(r, "You have to give action=something");
545 goto done;
546 } else if (!(strcmp(post_cmd.raw.action, "reply"))) {
547 post_cmd.action_id = REPLY;
548 } else if (!(strcmp(post_cmd.raw.action, "newthread"))) {
549 post_cmd.action_id = NEWTHREAD;
550 } else if (!(strcmp(post_cmd.raw.action, "rebuild"))) {
551 post_cmd.action_id = REBUILD;
554 if (post_cmd.raw.thread) {
555 post_cmd.thread_id = strtoll(post_cmd.raw.thread, 0, 0);
558 if (post_cmd.action_id == NONE) {
559 LOG("Invalid action \"%s\" (400)", post_cmd.raw.action);
560 report_bad_request(r, "That's not a valid action");
561 goto done;
565 * XXX: the idea is to only accept REBUILD commmands from
566 * the local machine. Is this necessary and sufficient in
567 * the world of ipv6?
569 if (post_cmd.action_id == REBUILD) {
570 /* Note that the IP is normalized so we can sort it */
571 if (strcmp(ip, "127.000.000.001") &&
572 strcmp(ip, "000.000.000.000") &&
573 strcmp(ip, "0000:0000:0000:0000:0000:0000:0000:0001")) {
574 LOG("REBUILD requested from invalid ip %s", ip);
575 report_bad_request(r, "You can(not) rebuild");
576 goto done;
579 goto take_action;
582 /* And we can find where they wanted to do it */
583 found_idx = 0;
585 if (!post_cmd.raw.board) {
586 LOG("No board specified (400)");
587 report_bad_request(r, "You have to give board=something");
588 goto done;
591 if (post_cmd.action_id == REPLY &&
592 !post_cmd.thread_id) {
593 LOG("Reply, yet no thread (400)");
594 report_bad_request(r, "You have to give thread=something");
595 goto done;
598 for (size_t j = 0; j < conf->boards_num; ++j) {
599 if (!strcmp(post_cmd.raw.board, conf->boards[j].name)) {
600 post_cmd.board_idx = j;
601 found_idx = 1;
602 break;
606 if (!found_idx) {
607 LOG("Invalid board \"%s\" (400)", post_cmd.raw.board);
608 report_bad_request(r, "That's not a valid board");
609 goto done;
612 int is_banned = 0;
614 if (db_check_bans(ip, post_cmd.board_idx, post_cmd.prepared.now,
615 &is_banned, &ban_until, &ban_reason) < 0) {
616 LOG("Couldn't determine ban status (500)");
617 report_internal_error(r);
618 goto done;
621 if (is_banned) {
622 /* This should give HTTP 403 */
623 LOG("Ban detected (until=\"%s\", reason=\"%s\") (403)",
624 ban_until, ban_reason);
625 report_ban(r, ban_until, ban_reason);
626 goto done;
629 if (post_cmd.action_id == REPLY ||
630 post_cmd.action_id == NEWTHREAD) {
631 int is_cooled = 0;
633 if (db_check_cooldowns(ip, post_cmd.board_idx,
634 post_cmd.prepared.now, &is_cooled,
635 &cooldown_length) < 0) {
636 LOG("Couldn't determine cooldown status (500)");
637 report_internal_error(r);
638 goto done;
641 if (is_cooled) {
642 /* This should give HTTP 429 */
643 LOG("Cooldown triggered (length=\"%s\") (429)",
644 cooldown_length);
645 report_cooldown(r, cooldown_length);
646 goto done;
649 int correct_challenge = 0;
651 if (!post_cmd.raw.challenge_id) {
652 LOG("No challenge id given (403)");
653 report_bad_challenge(r);
654 goto done;
657 char *e = 0;
658 size_t challenge_idx = (size_t) strtoll(
659 post_cmd.raw.challenge_id, &e, 0);
661 if (e &&
662 *e) {
663 challenge_idx = conf->challenges_num;
666 if (challenge_idx >= conf->challenges_num) {
667 LOG("Bad challenge id \"%s\" given (403)",
668 post_cmd.raw.challenge_id);
669 report_bad_challenge(r);
670 goto done;
673 if (!post_cmd.raw.challenge_response) {
674 LOG("No challenge response given (403)");
675 report_bad_challenge(r);
676 goto done;
679 for (size_t j = 0; j < NUM_CHALLENGE_ANSWERS; ++j) {
680 if (!conf->challenges[challenge_idx].answers[j]) {
681 continue;
684 if (!strcasecmp(post_cmd.raw.challenge_response,
685 conf->challenges[challenge_idx].answers[
686 j])) {
687 correct_challenge = 1;
691 if (!correct_challenge) {
692 LOG("Incorrect response \"%s\" to challenge %s (403)",
693 post_cmd.raw.challenge_response,
694 post_cmd.raw.challenge_id);
695 LOG("Comment was \"%s\"", UBSAFES(
696 post_cmd.raw.comment));
697 report_bad_challenge(r);
698 goto done;
702 take_action:
704 /* Now we split into specific actions */
705 switch (post_cmd.action_id) {
706 case REPLY:
707 LOG("reply to /%s/%ju", UBSAFES(post_cmd.raw.board),
708 post_cmd.thread_id);
709 handle_op_or_reply(conf, r, &post_cmd, ip, post_cmd.thread_id);
710 break;
711 case NEWTHREAD:
712 LOG("newthread on /%s/", UBSAFES(post_cmd.raw.board));
713 handle_op_or_reply(conf, r, &post_cmd, ip, 0);
714 break;
715 case REBUILD:
716 LOG("rebuild");
717 handle_rebuild(conf, r);
718 break;
719 case NONE:
720 ERROR_MESSAGE("Impossible");
721 report_internal_error(r);
722 break;
725 done:
726 clean_post_cmd(&post_cmd);
727 free(buf_main);
728 free(ban_reason);
729 free(ban_until);
730 free(cooldown_length);
733 /* Do the thing */
735 main(void)
737 int ret = 1;
738 FCGX_Request r = { 0 };
739 struct configuration conf = { 0 };
741 setlocale(LC_ALL, "");
743 /* tedu@ is probably laughing at me right now. Hi! */
744 srand(time(0));
745 conf = (struct configuration) {
746 /* */
747 .static_www_folder = static_www_folder, /* */
748 .work_path = work_path, /* */
749 .temp_dir_template = temp_dir_template, /* */
750 .trip_salt = trip_salt, /* */
751 .trip_salt_len = strlen(trip_salt), /* */
752 .boards = boards, /* */
753 .boards_num = NUM_OF(boards), /* */
754 .max_form_data_size = max_form_data_size, /* */
755 .max_file_size = max_file_size, /* */
756 .max_text_len = max_text_len, /* */
757 .filetypes = filetypes, /* */
758 .filetypes_num = NUM_OF(filetypes), /* */
759 .file_description_prog = file_description_prog, /* */
760 .headers = headers, /* */
761 .headers_num = NUM_OF(headers), /* */
762 .challenges = challenges, /* */
763 .challenges_num = NUM_OF(challenges), /* */
764 .wordfilter_inputs = wordfilter_inputs, /* */
765 .wordfilter_inputs_num = NUM_OF(wordfilter_inputs), /* */
766 .forbidden_inputs = forbidden_inputs, /* */
767 .forbidden_inputs_num = NUM_OF(forbidden_inputs), /* */
770 if (preconditions_check(&conf) < 0) {
771 goto done;
774 if (board_pages_init(&conf) < 0) {
775 goto done;
778 FCGX_Init();
779 FCGX_InitRequest(&r, 0, 0);
781 while (FCGX_Accept_r(&r) == 0) {
782 handle(&conf, &r);
783 FCGX_Finish_r(&r);
786 ret = 0;
787 done:
788 clean_dbs();
789 clean_locks();
790 clean_multipart();
791 clean_sanitize_comment();
792 clean_sanitize_file();
793 clean_tripcodes();
794 clean_write_thread();
796 return ret;