misc: a few portability tweaks
[rb-79.git] / rb79-server.c
blobfd45b831a7406801a2d677288a920d64a1068862
1 /*
2 * Copyright (c) 2017-2018, 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 report_bad_request(FCGX_Request *r, const char *reason)
40 if (!reason) {
41 reason = "That's not a real request. That's all we know.";
44 FCGX_FPrintF(r->out, BAD_REQUEST_FMT, reason);
46 return 0;
49 /* Print out a page saying they failed the CAPTCHA (403) */
50 static int report_bad_challenge(FCGX_Request *r)
52 FCGX_FPrintF(r->out, BAD_CHALLENGE_FMT);
54 return 0;
57 /* Print out a BANNED page (403) */
58 static int report_ban(FCGX_Request *r, char *ban_until, char *ban_reason)
60 FCGX_FPrintF(r->out, BAN_FMT, UBSAFES(ban_until), UBSAFES(ban_reason));
62 return 0;
65 /* Print out a BAD METHOD page (405) */
66 static int report_bad_method(FCGX_Request *r)
68 FCGX_FPrintF(r->out, BAD_METHOD_FMT);
70 return 0;
73 /* Print out a FILE TOO LARGE page (413) */
74 static int report_too_large(FCGX_Request *r, const char *large_thing)
76 FCGX_FPrintF(r->out, TOO_LARGE_FMT, UBSAFES(large_thing));
78 return 0;
81 /* Print out a page saying they're posting too fast (429) */
82 static int report_cooldown(FCGX_Request *r, char *cooldown_length)
84 FCGX_FPrintF(r->out, COOLDOWN_FMT, UBSAFES(cooldown_length));
86 return 0;
89 /* Print out an INTERNAL ERROR page (500) */
90 static int report_internal_error(FCGX_Request *r)
92 FCGX_FPrintF(r->out, INTERNAL_ERROR_FMT);
94 return 0;
97 /* Print out a POST SUCCESSFUL page (200) */
98 static int report_post_successful(FCGX_Request *r, const char *buf)
100 FCGX_FPrintF(r->out, POST_SUCCESSFUL_FMT, buf);
102 return 0;
105 /* Make sure every board has a page (really only for brand-new boards) */
106 static int board_pages_init(struct configuration *conf)
108 int ret = -1;
109 uintmax_t *thread_ids = 0;
110 size_t thread_ids_num = 0;
111 size_t board_pages_num = 0;
113 for (size_t j = 0; j < conf->boards_num; ++j) {
114 free(thread_ids);
115 thread_ids = 0;
117 if (lock_acquire(j) < 0) {
118 goto done;
121 if (db_cull_and_report_threads(j, &thread_ids, &thread_ids_num,
122 &board_pages_num) < 0) {
123 goto done;
126 if (wt_write_board(j, thread_ids, thread_ids_num,
127 board_pages_num) < 0) {
128 goto done;
131 lock_release(j);
134 ret = 0;
135 done:
136 free(thread_ids);
138 return ret;
141 /* Free what needs to be freed */
142 static void clean_post_cmd(struct post_cmd *p)
144 if (!p) {
145 return;
148 free(p->raw.action);
149 free(p->raw.board);
150 free(p->raw.thread);
151 free(p->raw.post);
152 free(p->raw.name);
153 free(p->raw.email);
154 free(p->raw.tripcode);
155 free(p->raw.subject);
156 free(p->raw.comment);
157 free(p->raw.file_name);
158 free(p->raw.file_contents);
159 free(p->raw.challenge_id);
160 free(p->raw.challenge_response);
161 free(p->prepared.name);
162 free(p->prepared.email);
163 free(p->prepared.tripcode);
164 free(p->prepared.subject);
165 free(p->prepared.comment);
166 free(p->prepared.ext);
167 free(p->prepared.file_name);
168 free(p->prepared.system_full_path);
169 free(p->prepared.system_thumb_path);
170 free(p->prepared.file_info);
171 free(p->scannable_comment);
172 free(p->position_map);
173 *p = (struct post_cmd) { 0 };
176 /* The bulk of work for processing a post */
177 static void handle_op_or_reply(struct configuration *conf, FCGX_Request *r,
178 struct post_cmd *pc, const char *ip, size_t
179 parent_thread)
181 char *buf = 0;
182 char *abs_file_path = 0;
183 size_t len = 0;
184 int our_fault = 0;
185 uintmax_t real_thread = 0;
186 int cooldown = 0;
187 int thread_dne = 0;
188 int thread_full = 0;
189 int thread_closed = 0;
190 const struct filetype *f;
191 size_t board_pages_num = 0;
192 uintmax_t *thread_ids = 0;
193 size_t thread_ids_num = 0;
194 uint_fast8_t need_to_unlock = 0;
196 if (!parent_thread &&
197 (!pc->raw.file_contents ||
198 !pc->raw.file_contents_len)) {
199 LOG("New thread, yet no file (400)");
200 report_bad_request(r, "New threads must have a file");
201 goto done;
204 /* pc comes in with a bunch of these lens set not-as-desired */
205 if (pc->raw.file_name_len > conf->max_text_len) {
206 LOG("File name length (%zu) larger than max (%zu) (413)",
207 pc->raw.file_name_len, conf->max_text_len);
208 report_too_large(r, "Filename");
209 goto done;
212 if (pc->raw.subject_len > conf->max_text_len) {
213 LOG("Subject length (%zu) larger than max (%zu) (413)",
214 pc->raw.subject_len, conf->max_text_len);
215 report_too_large(r, "Subject text");
216 goto done;
219 if (pc->raw.email_len > conf->max_text_len) {
220 LOG("Email length (%zu) larger than max (%zu) (413)",
221 pc->raw.email_len, conf->max_text_len);
222 report_too_large(r, "Email address");
223 goto done;
226 if (pc->raw.comment_len > conf->max_text_len) {
227 LOG("Comment length (%zu) larger than max (%zu) (413)",
228 pc->raw.comment_len, conf->max_text_len);
229 report_too_large(r, "Comment text");
230 goto done;
233 if (pc->raw.file_contents_len > conf->max_file_size) {
234 LOG("File size (%zu) larger than max (%zu) (413)",
235 pc->raw.file_contents_len, conf->max_file_size);
236 report_too_large(r, "File size");
237 goto done;
240 if (sf_check_mime_type(pc->raw.file_contents, pc->raw.file_contents_len,
241 &f) < 0) {
242 LOG("Bad MIME check (400)");
243 report_bad_request(r, "Unsupported file type");
244 goto done;
247 /* Calculate tripcodes before HTML-escaping everything */
248 if (tripcodes_calculate(pc) < 0) {
249 LOG("Error in tripcodes_calculate (500)");
250 report_internal_error(r);
251 goto done;
254 /* HTML-escape, wordfilter, linkify */
255 if (st_sanitize_text(pc, &our_fault) < 0) {
256 if (our_fault) {
257 LOG("Error in st_sanitize_text (500)");
258 report_internal_error(r);
259 goto done;
262 LOG("Bad text (400)");
263 report_bad_request(r, "Disallowed text");
264 goto done;
267 cooldown = pc->prepared.comment_len ?
268 conf->boards[pc->board_idx].text_cooldown :
269 conf->boards[pc->board_idx].blank_cooldown;
272 * From now on, everything must be under lock, since we
273 * could be touching the filesystem. Strictly, we don't
274 * need to worry about locking for db-only operations, so
275 * this could be delayed a bit.
277 if (lock_acquire(pc->board_idx) < 0) {
278 LOG("Error in lock_acquire (500)");
279 report_internal_error(r);
280 goto done;
283 need_to_unlock = 1;
285 if (db_insert_post(ip, parent_thread, cooldown, pc, &thread_dne,
286 &thread_closed, &thread_full, &pc->prepared.id) <
287 0) {
288 LOG("Error in insert_post (500)");
289 report_internal_error(r);
290 goto done;
293 LOG("Post %ju on board /%s/", pc->prepared.id,
294 conf->boards[pc->board_idx].name);
296 if (thread_dne) {
297 LOG("Thread %zu does not exist (400)", (size_t) 0);
298 report_bad_request(r, "Thread does not exist");
299 goto done;
302 if (thread_full) {
303 LOG("Thread %zu is full (400)", (size_t) 0);
304 report_bad_request(r, "Thread is full");
305 goto done;
308 if (thread_closed) {
309 LOG("Thread %zu is closed (400)", (size_t) 0);
310 report_bad_request(r, "Thread is closed");
311 goto done;
314 /* Make thumbnails and insert them */
315 if (f) {
316 if (sf_install_files(pc->board_idx, pc->raw.file_contents,
317 pc->raw.file_contents_len,
318 &pc->prepared.now, f, &abs_file_path,
319 &pc->prepared.system_full_path,
320 &pc->prepared.system_full_path_len,
321 &pc->prepared.system_thumb_path,
322 &pc->prepared.system_thumb_path_len,
323 &our_fault) < 0) {
324 if (our_fault) {
325 LOG("Error in sf_install_files (500)");
326 report_internal_error(r);
327 goto done;
330 LOG("Couldn't install files (400)");
331 report_bad_request(r, "Bad file upload");
332 goto done;
335 /* ... and now that they're inserted, describe them ... */
336 if (sf_describe_file(f->mime_type, abs_file_path,
337 &pc->prepared.file_info,
338 &pc->prepared.file_info_len) < 0) {
339 LOG("Error in sf_describe_file (500)");
340 report_internal_error(r);
341 goto done;
344 /* ... and alert the db about that description. */
345 if (db_update_file_info(pc->board_idx, pc->prepared.id,
346 pc->prepared.file_info,
347 pc->prepared.file_info_len,
348 pc->prepared.system_full_path,
349 pc->prepared.system_full_path_len,
350 pc->prepared.system_thumb_path,
351 pc->prepared.system_thumb_path_len) <
352 0) {
353 LOG("Error in db_update_post_description (500)");
354 report_internal_error(r);
355 goto done;
360 * We're about ready to write out the threads, boards, etc.
361 * Therefore, we must now check for thread culling, and
362 * also calculate how many board pages we need.
364 if (db_cull_and_report_threads(pc->board_idx, &thread_ids,
365 &thread_ids_num, &board_pages_num) < 0) {
366 LOG("Error in db_cull_and_report_threads (500)");
367 report_internal_error(r);
368 goto done;
371 real_thread = parent_thread ? parent_thread : pc->prepared.id;
373 if (wt_write_thread(pc->board_idx, real_thread) < 0) {
374 LOG("Error in wt_write_thread (500)");
375 report_internal_error(r);
376 goto done;
379 if (wt_write_board(pc->board_idx, thread_ids, thread_ids_num,
380 board_pages_num) < 0) {
381 LOG("Error in wt_write_board (500)");
382 report_internal_error(r);
383 goto done;
386 len = snprintf(0, 0, "/%s/res/%s", pc->raw.board, pc->raw.thread);
388 if (len + 1 < len) {
389 ERROR_MESSAGE("overflow");
390 report_internal_error(r);
391 goto done;
394 if (!(buf = malloc(len + 1))) {
395 PERROR_MESSAGE("malloc");
396 report_internal_error(r);
397 goto done;
400 if (pc->raw.email &&
401 !strcmp(pc->raw.email, "noko")) {
402 sprintf(buf, "/%s/res/%s", pc->raw.board, pc->raw.thread);
403 } else {
404 sprintf(buf, "/%s", pc->raw.board);
407 report_post_successful(r, buf);
408 done:
410 if (need_to_unlock) {
411 lock_release(pc->board_idx);
414 free(buf);
415 free(abs_file_path);
416 free(thread_ids);
419 /* Rebuild every thread and every board */
420 static void handle_rebuild (struct configuration *conf, FCGX_Request *r)
422 uint_fast8_t had_errors = util_rebuild(conf);
424 FCGX_FPrintF(r->out, "Status: 200\r\nContent-type: text/plain\r\n\r\n"
425 "Rebuild complete%s\n", had_errors ?
426 " with errors" : "");
428 return;
431 /* Figure out what they want us to do */
432 static void handle(struct configuration *conf, FCGX_Request *r)
434 char *p = 0;
435 char *content_type = 0;
436 char *content_len_str = 0;
437 size_t content_len = 0;
438 char *buf_main = 0;
439 size_t buf_main_len = 0;
440 const char *content_type_prefix = "Content-Type: ";
441 struct post_cmd post_cmd = { 0 };
442 const char *ip_raw = FCGX_GetParam("REMOTE_ADDR", r->envp);
443 char *ip = 0;
444 char *ban_reason = 0;
445 char *ban_until = 0;
446 char *cooldown_length = 0;
447 uint_fast8_t found_idx = 0;
449 /* In case someone is trying for a time GET, prioritize that */
450 time(&post_cmd.prepared.now);
451 LOG("-----------------------------------------");
452 LOG("Handling post at %zu from %s", (size_t) post_cmd.prepared.now,
453 UBSAFES(ip_raw));
455 if (!ip_raw) {
456 LOG("Couldn't get REMOTE_ADDR (500)");
457 report_internal_error(r);
458 goto done;
461 if (util_normalize_ip(ip_raw, &ip) < 0) {
462 LOG("Couldn't normalize ip (500)");
463 report_internal_error(r);
464 goto done;
467 /* You can only POST to /action */
468 if (!(p = FCGX_GetParam("REQUEST_METHOD", r->envp))) {
469 LOG("Couldn't get request method (500)");
470 report_internal_error(r);
471 goto done;
474 if (strcmp(p, "POST")) {
475 LOG("request method was not POST (405)");
476 report_bad_method(r);
477 goto done;
480 /* We have to somehow feed this into multipart */
481 if (!(content_type = FCGX_GetParam("CONTENT_TYPE", r->envp))) {
482 LOG("Can't get CONTENT_TYPE (500)");
483 report_internal_error(r);
484 goto done;
487 if (!(content_len_str = FCGX_GetParam("CONTENT_LENGTH", r->envp))) {
488 LOG("Can't get CONTENT_LENGTH (500)");
489 report_internal_error(r);
490 goto done;
493 content_len = (size_t) strtoll(content_len_str, 0, 0);
495 if (content_len > max_form_data_size) {
496 LOG("Buffer would have exceeded %zuB (413)",
497 max_form_data_size);
498 report_too_large(r, "Total POST");
499 goto done;
502 buf_main_len = strlen(content_type_prefix) + strlen(content_type) +
503 strlen("\r\n\r\n") + content_len;
505 if (buf_main_len + 1 < buf_main_len) {
506 ERROR_MESSAGE("overflow");
507 goto done;
510 if (!(buf_main = malloc(buf_main_len + 1))) {
511 PERROR_MESSAGE("malloc");
512 goto done;
515 size_t offset = sprintf(buf_main, "%s%s\r\n\r\n", content_type_prefix,
516 content_type);
518 /* Try and swallow this thing into a buffer */
519 FCGX_GetStr(buf_main + offset, content_len, r->in);
521 /* Okay, we've got it in the buffer */
522 if (multipart_decompose(buf_main, buf_main_len, &post_cmd) < 0) {
523 LOG("Decoding message failed, returning (400)");
524 report_bad_request(r, "Invalid multipart/form-data");
525 goto done;
528 /* Now we can check what they actually wanted us to DO */
529 if (!post_cmd.raw.action) {
530 LOG("No action specified (400)");
531 report_bad_request(r, "You have to give action=something");
532 goto done;
533 } else if (!(strcmp(post_cmd.raw.action, "reply"))) {
534 post_cmd.action_id = REPLY;
535 } else if (!(strcmp(post_cmd.raw.action, "newthread"))) {
536 post_cmd.action_id = NEWTHREAD;
537 } else if (!(strcmp(post_cmd.raw.action, "rebuild"))) {
538 post_cmd.action_id = REBUILD;
541 if (post_cmd.raw.thread) {
542 post_cmd.thread_id = strtoll(post_cmd.raw.thread, 0, 0);
545 if (post_cmd.action_id == NONE) {
546 LOG("Invalid action \"%s\" (400)", post_cmd.raw.action);
547 report_bad_request(r, "That's not a valid action");
548 goto done;
552 * XXX: the idea is to only accept REBUILD commmands from
553 * the local machine. Is this necessary and sufficient in
554 * the world of ipv6?
556 if (post_cmd.action_id == REBUILD) {
557 /* Note that the IP is normalized so we can sort it */
558 if (strcmp(ip, "127.000.000.001") &&
559 strcmp(ip, "000.000.000.000") &&
560 strcmp(ip, "0000:0000:0000:0000:0000:0000:0000:0001")) {
561 LOG("REBUILD requested from invalid ip %s", ip);
562 report_bad_request(r, "You can(not) rebuild");
563 goto done;
566 goto take_action;
569 /* And we can find where they wanted to do it */
570 found_idx = 0;
572 if (!post_cmd.raw.board) {
573 LOG("No board specified (400)");
574 report_bad_request(r, "You have to give board=something");
575 goto done;
578 if (post_cmd.action_id == REPLY &&
579 !post_cmd.thread_id) {
580 LOG("Reply, yet no thread (400)");
581 report_bad_request(r, "You have to give thread=something");
582 goto done;
585 for (size_t j = 0; j < conf->boards_num; ++j) {
586 if (!strcmp(post_cmd.raw.board, conf->boards[j].name)) {
587 post_cmd.board_idx = j;
588 found_idx = 1;
589 break;
593 if (!found_idx) {
594 LOG("Invalid board \"%s\" (400)", post_cmd.raw.board);
595 report_bad_request(r, "That's not a valid board");
596 goto done;
599 int is_banned = 0;
601 if (db_check_bans(ip, post_cmd.board_idx, post_cmd.prepared.now,
602 &is_banned, &ban_until, &ban_reason) < 0) {
603 LOG("Couldn't determine ban status (500)");
604 report_internal_error(r);
605 goto done;
608 if (is_banned) {
609 /* This should give HTTP 403 */
610 LOG("Ban detected (until=\"%s\", reason=\"%s\") (403)",
611 ban_until, ban_reason);
612 report_ban(r, ban_until, ban_reason);
613 goto done;
616 if (post_cmd.action_id == REPLY ||
617 post_cmd.action_id == NEWTHREAD) {
618 int is_cooled = 0;
620 if (db_check_cooldowns(ip, post_cmd.board_idx,
621 post_cmd.prepared.now, &is_cooled,
622 &cooldown_length) < 0) {
623 LOG("Couldn't determine cooldown status (500)");
624 report_internal_error(r);
625 goto done;
628 if (is_cooled) {
629 /* This should give HTTP 429 */
630 LOG("Cooldown triggered (length=\"%s\") (429)",
631 cooldown_length);
632 report_cooldown(r, cooldown_length);
633 goto done;
636 int correct_challenge = 0;
638 if (!post_cmd.raw.challenge_id) {
639 LOG("No challenge id given (403)");
640 report_bad_challenge(r);
641 goto done;
644 char *e = 0;
645 size_t challenge_idx = (size_t) strtoll(
646 post_cmd.raw.challenge_id, &e, 0);
648 if (e &&
649 *e) {
650 challenge_idx = conf->challenges_num;
653 if (challenge_idx >= conf->challenges_num) {
654 LOG("Bad challenge id \"%s\" given (403)",
655 post_cmd.raw.challenge_id);
656 report_bad_challenge(r);
657 goto done;
660 if (!post_cmd.raw.challenge_response) {
661 LOG("No challenge response given (403)");
662 report_bad_challenge(r);
663 goto done;
666 for (size_t j = 0; j < NUM_CHALLENGE_ANSWERS; ++j) {
667 if (!conf->challenges[challenge_idx].answers[j]) {
668 continue;
671 if (!strcasecmp(post_cmd.raw.challenge_response,
672 conf->challenges[challenge_idx].answers[
673 j])) {
674 correct_challenge = 1;
678 if (!correct_challenge) {
679 LOG("Incorrect response \"%s\" to challenge %s (403)",
680 post_cmd.raw.challenge_response,
681 post_cmd.raw.challenge_id);
682 LOG("Comment was \"%s\"", UBSAFES(post_cmd.raw.comment));
683 report_bad_challenge(r);
684 goto done;
688 take_action:
690 /* Now we split into specific actions */
691 switch (post_cmd.action_id) {
692 case REPLY:
693 LOG("reply to /%s/%ju", UBSAFES(post_cmd.raw.board),
694 post_cmd.thread_id);
695 handle_op_or_reply(conf, r, &post_cmd, ip, post_cmd.thread_id);
696 break;
697 case NEWTHREAD:
698 LOG("newthread on /%s/", UBSAFES(post_cmd.raw.board));
699 handle_op_or_reply(conf, r, &post_cmd, ip, 0);
700 break;
701 case REBUILD:
702 LOG("rebuild");
703 handle_rebuild(conf, r);
704 break;
705 case NONE:
706 ERROR_MESSAGE("Impossible");
707 report_internal_error(r);
708 break;
711 done:
712 clean_post_cmd(&post_cmd);
713 free(buf_main);
714 free(ban_reason);
715 free(ban_until);
716 free(cooldown_length);
719 /* Do the thing */
720 int main(void)
722 int ret = 1;
723 FCGX_Request r = { 0 };
724 struct configuration conf = { 0 };
726 setlocale(LC_ALL, "");
728 /* tedu@ is probably laughing at me right now. Hi! */
729 srand(time(0));
730 conf = (struct configuration) {
731 /* */
732 .static_www_folder = static_www_folder, /* */
733 .work_path = work_path, /* */
734 .trip_salt = trip_salt, /* */
735 .trip_salt_len = strlen(trip_salt), /* */
736 .boards = boards, /* */
737 .boards_num = NUM_OF(boards), /* */
738 .max_form_data_size = max_form_data_size, /* */
739 .max_file_size = max_file_size, /* */
740 .max_text_len = max_text_len, /* */
741 .filetypes = filetypes, /* */
742 .filetypes_num = NUM_OF(filetypes), /* */
743 .file_description_prog = file_description_prog, /* */
744 .headers = headers, /* */
745 .headers_num = NUM_OF(headers), /* */
746 .challenges = challenges, /* */
747 .challenges_num = NUM_OF(challenges), /* */
748 .wordfilter_inputs = wordfilter_inputs, /* */
749 .wordfilter_inputs_num = NUM_OF(wordfilter_inputs), /* */
752 if (preconditions_check(&conf) < 0) {
753 goto done;
756 if (board_pages_init(&conf) < 0) {
757 goto done;
760 FCGX_Init();
761 FCGX_InitRequest(&r, 0, 0);
763 while (FCGX_Accept_r(&r) == 0) {
764 handle(&conf, &r);
765 FCGX_Finish_r(&r);
768 ret = 0;
769 done:
770 clean_dbs();
771 clean_locks();
772 clean_multipart();
773 clean_sanitize_comment();
774 clean_sanitize_file();
775 clean_tripcodes();
776 clean_write_thread();
778 return ret;