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
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.
35 const char *program_name
= "rb79-server";
37 /* Print out a page saying the request was malformed (400) */
39 report_bad_request(FCGX_Request
*r
, const char *reason
)
42 reason
= "That's not a real request. That's all we know.";
45 FCGX_FPrintF(r
->out
, BAD_REQUEST_FMT
, reason
);
50 /* Print out a page saying they failed the CAPTCHA (403) */
52 report_bad_challenge(FCGX_Request
*r
)
54 FCGX_FPrintF(r
->out
, BAD_CHALLENGE_FMT
);
59 /* Print out a BANNED page (403) */
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
));
68 /* Print out a BAD METHOD page (405) */
70 report_bad_method(FCGX_Request
*r
)
72 FCGX_FPrintF(r
->out
, BAD_METHOD_FMT
);
77 /* Print out a FILE TOO LARGE page (413) */
79 report_too_large(FCGX_Request
*r
, const char *large_thing
)
81 FCGX_FPrintF(r
->out
, TOO_LARGE_FMT
, UBSAFES(large_thing
));
86 /* Print out a page saying they're posting too fast (429) */
88 report_cooldown(FCGX_Request
*r
, char *cooldown_length
)
90 FCGX_FPrintF(r
->out
, COOLDOWN_FMT
, UBSAFES(cooldown_length
));
95 /* Print out an INTERNAL ERROR page (500) */
97 report_internal_error(FCGX_Request
*r
)
99 FCGX_FPrintF(r
->out
, INTERNAL_ERROR_FMT
);
104 /* Print out a POST SUCCESSFUL page (200) */
106 report_post_successful(FCGX_Request
*r
, const char *buf
)
108 FCGX_FPrintF(r
->out
, POST_SUCCESSFUL_FMT
, buf
);
113 /* Normal POST SUCCESSFUL page, with a redirect back to the given board */
115 report_post_successful_with_redir(FCGX_Request
*r
, const char *board_name
)
118 size_t len
= snprintf(0, 0, "/%s", board_name
);
122 ERROR_MESSAGE("overflow");
123 report_internal_error(r
);
127 if (!(buf
= malloc(len
+ 1))) {
128 PERROR_MESSAGE("malloc");
129 report_internal_error(r
);
133 sprintf(buf
, "/%s", board_name
);
134 ret
= report_post_successful(r
, buf
);
141 /* Normal POST SUCCESSFUL page, with redirect back to the given thread */
143 report_post_successful_no_redir(FCGX_Request
*r
, const char *board_name
, const
147 size_t len
= snprintf(0, 0, "/%s/res/%s", board_name
, thread
);
151 ERROR_MESSAGE("overflow");
152 report_internal_error(r
);
156 if (!(buf
= malloc(len
+ 1))) {
157 PERROR_MESSAGE("malloc");
158 report_internal_error(r
);
162 sprintf(buf
, "/%s/res/%s", board_name
, thread
);
163 ret
= report_post_successful(r
, buf
);
170 /* Make sure every board has a page (really only for brand-new boards) */
172 board_pages_init(struct configuration
*conf
)
175 uintmax_t *thread_ids
= 0;
176 size_t thread_ids_num
= 0;
177 size_t board_pages_num
= 0;
179 for (size_t j
= 0; j
< conf
->boards_num
; ++j
) {
183 if (lock_acquire(j
) < 0) {
187 if (db_cull_and_report_threads(j
, &thread_ids
, &thread_ids_num
,
188 &board_pages_num
) < 0) {
192 if (wt_write_board(j
, thread_ids
, thread_ids_num
,
193 board_pages_num
) < 0) {
207 /* Free what needs to be freed */
209 clean_post_cmd(struct post_cmd
*p
)
221 free(p
->raw
.tripcode
);
222 free(p
->raw
.subject
);
223 free(p
->raw
.comment
);
224 free(p
->raw
.file_name
);
225 free(p
->raw
.file_contents
);
226 free(p
->raw
.challenge_id
);
227 free(p
->raw
.challenge_response
);
228 free(p
->prepared
.name
);
229 free(p
->prepared
.email
);
230 free(p
->prepared
.tripcode
);
231 free(p
->prepared
.subject
);
232 free(p
->prepared
.comment
);
233 free(p
->prepared
.ext
);
234 free(p
->prepared
.file_name
);
235 free(p
->prepared
.system_full_path
);
236 free(p
->prepared
.system_thumb_path
);
237 free(p
->prepared
.file_info
);
238 free(p
->scannable_comment
);
239 free(p
->scannable_name
);
240 free(p
->scannable_email
);
241 free(p
->scannable_subject
);
242 free(p
->scannable_filename
);
243 free(p
->comment_position_map
);
244 free(p
->name_position_map
);
245 free(p
->email_position_map
);
246 free(p
->subject_position_map
);
247 free(p
->filename_position_map
);
248 *p
= (struct post_cmd
) { 0 };
251 /* The bulk of work for processing a post */
253 handle_op_or_reply(struct configuration
*conf
, FCGX_Request
*r
, struct
254 post_cmd
*pc
, const char *ip
, size_t parent_thread
)
257 char *abs_file_path
= 0;
259 uintmax_t real_thread
= 0;
263 int thread_closed
= 0;
264 const struct filetype
*f
;
265 size_t board_pages_num
= 0;
266 uintmax_t *thread_ids
= 0;
267 size_t thread_ids_num
= 0;
268 uint_fast8_t need_to_unlock
= 0;
270 if (!parent_thread
&&
271 (!pc
->raw
.file_contents
||
272 !pc
->raw
.file_contents_len
)) {
273 LOG("New thread, yet no file (400)");
274 report_bad_request(r
, "New threads must have a file");
278 /* pc comes in with a bunch of these lens set not-as-desired */
279 if (pc
->raw
.file_name_len
> conf
->max_text_len
) {
280 LOG("File name length (%zu) larger than max (%zu) (413)",
281 pc
->raw
.file_name_len
, conf
->max_text_len
);
282 report_too_large(r
, "Filename");
286 if (pc
->raw
.subject_len
> conf
->max_text_len
) {
287 LOG("Subject length (%zu) larger than max (%zu) (413)",
288 pc
->raw
.subject_len
, conf
->max_text_len
);
289 report_too_large(r
, "Subject text");
293 if (pc
->raw
.email_len
> conf
->max_text_len
) {
294 LOG("Email length (%zu) larger than max (%zu) (413)",
295 pc
->raw
.email_len
, conf
->max_text_len
);
296 report_too_large(r
, "Email address");
300 if (pc
->raw
.comment_len
> conf
->max_text_len
) {
301 LOG("Comment length (%zu) larger than max (%zu) (413)",
302 pc
->raw
.comment_len
, conf
->max_text_len
);
303 report_too_large(r
, "Comment text");
307 if (pc
->raw
.file_contents_len
> conf
->max_file_size
) {
308 LOG("File size (%zu) larger than max (%zu) (413)",
309 pc
->raw
.file_contents_len
, conf
->max_file_size
);
310 report_too_large(r
, "File size");
314 if (sf_check_mime_type(pc
->raw
.file_contents
, pc
->raw
.file_contents_len
,
316 LOG("Bad MIME check (400)");
317 report_bad_request(r
, "Unsupported file type");
321 /* Calculate tripcodes before HTML-escaping everything */
322 if (tripcodes_calculate(pc
) < 0) {
323 LOG("Error in tripcodes_calculate (500)");
324 report_internal_error(r
);
328 /* HTML-escape, wordfilter, linkify */
329 uint_fast8_t is_forbidden
= 0;
330 int ban_duration
= 0;
331 const char *ban_reason
= 0;
333 if (st_sanitize_text(pc
, &our_fault
, &is_forbidden
, &ban_duration
,
336 LOG("Error in st_sanitize_text (500)");
337 report_internal_error(r
);
342 LOG("Bad text (400)");
343 LOG("Comment was \"%s\"", UBSAFES(pc
->raw
.comment
));
346 time_t start
= time(0);
347 time_t end
= start
+ ban_duration
;
348 int is_secret
= !(ban_reason
);
350 if (db_insert_ban(1, 0, ip
, ip
, ban_reason
,
351 is_secret
, start
, end
) < 0) {
352 LOG("Error in db_insert_ban (500)");
353 report_internal_error(r
);
358 report_bad_request(r
, "Disallowed text");
361 LOG("Unknown error (400)");
362 LOG("Comment was \"%s\"", UBSAFES(pc
->raw
.comment
));
363 report_bad_request(r
, "Disallowed text");
368 cooldown
= pc
->prepared
.comment_len
?
369 conf
->boards
[pc
->board_idx
].text_cooldown
:
370 conf
->boards
[pc
->board_idx
].blank_cooldown
;
373 * From now on, everything must be under lock, since we
374 * could be touching the filesystem. Strictly, we don't
375 * need to worry about locking for db-only operations, so
376 * this could be delayed a bit.
378 if (lock_acquire(pc
->board_idx
) < 0) {
379 LOG("Error in lock_acquire (500)");
380 report_internal_error(r
);
386 if (db_insert_post(ip
, parent_thread
, cooldown
, pc
, &thread_dne
,
387 &thread_closed
, &thread_full
, &pc
->prepared
.id
) <
389 LOG("Error in insert_post (500)");
390 report_internal_error(r
);
394 LOG("Post %ju on board /%s/", pc
->prepared
.id
,
395 conf
->boards
[pc
->board_idx
].name
);
398 LOG("Thread %zu does not exist (400)", (size_t) 0);
399 report_bad_request(r
, "Thread does not exist");
404 LOG("Thread %zu is full (400)", (size_t) 0);
405 report_bad_request(r
, "Thread is full");
410 LOG("Thread %zu is closed (400)", (size_t) 0);
411 report_bad_request(r
, "Thread is closed");
415 /* Make thumbnails and insert them */
417 if (sf_install_files(pc
->board_idx
, pc
->raw
.file_contents
,
418 pc
->raw
.file_contents_len
,
419 &pc
->prepared
.now
, f
, &abs_file_path
,
420 &pc
->prepared
.system_full_path
,
421 &pc
->prepared
.system_full_path_len
,
422 &pc
->prepared
.system_thumb_path
,
423 &pc
->prepared
.system_thumb_path_len
,
426 LOG("Error in sf_install_files (500)");
427 report_internal_error(r
);
431 LOG("Couldn't install files (400)");
432 report_bad_request(r
, "Bad file upload");
436 /* ... and now that they're inserted, describe them ... */
437 if (sf_describe_file(f
->mime_type
, abs_file_path
,
438 &pc
->prepared
.file_info
,
439 &pc
->prepared
.file_info_len
) < 0) {
440 LOG("Error in sf_describe_file (500)");
441 report_internal_error(r
);
445 /* ... and alert the db about that description. */
446 if (db_update_file_info(pc
->board_idx
, pc
->prepared
.id
,
447 pc
->prepared
.file_info
,
448 pc
->prepared
.file_info_len
,
449 pc
->prepared
.system_full_path
,
450 pc
->prepared
.system_full_path_len
,
451 pc
->prepared
.system_thumb_path
,
452 pc
->prepared
.system_thumb_path_len
) <
454 LOG("Error in db_update_post_description (500)");
455 report_internal_error(r
);
461 * We're about ready to write out the threads, boards, etc.
462 * Therefore, we must now check for thread culling, and
463 * also calculate how many board pages we need.
465 if (db_cull_and_report_threads(pc
->board_idx
, &thread_ids
,
466 &thread_ids_num
, &board_pages_num
) < 0) {
467 LOG("Error in db_cull_and_report_threads (500)");
468 report_internal_error(r
);
472 real_thread
= parent_thread
? parent_thread
: pc
->prepared
.id
;
474 if (wt_write_thread(pc
->board_idx
, real_thread
) < 0) {
475 LOG("Error in wt_write_thread (500)");
476 report_internal_error(r
);
480 if (wt_write_board(pc
->board_idx
, thread_ids
, thread_ids_num
,
481 board_pages_num
) < 0) {
482 LOG("Error in wt_write_board (500)");
483 report_internal_error(r
);
488 !strcmp(pc
->raw
.email
, "noko")) {
489 report_post_successful_no_redir(r
, pc
->raw
.board
,
492 report_post_successful_with_redir(r
, pc
->raw
.board
);
497 if (need_to_unlock
) {
498 lock_release(pc
->board_idx
);
506 /* Rebuild every thread and every board */
508 handle_rebuild (struct configuration
*conf
, FCGX_Request
*r
)
510 uint_fast8_t had_errors
= util_rebuild(conf
);
512 FCGX_FPrintF(r
->out
, "Status: 200\r\nContent-type: text/plain\r\n\r\n"
513 "Rebuild complete%s\n", had_errors
?
514 " with errors" : "");
519 /* Figure out what they want us to do */
521 handle(struct configuration
*conf
, FCGX_Request
*r
)
524 char *content_type
= 0;
525 char *content_len_str
= 0;
526 size_t content_len
= 0;
528 size_t buf_main_len
= 0;
529 const char *content_type_prefix
= "Content-Type: ";
530 struct post_cmd post_cmd
= { 0 };
531 const char *ip_raw
= FCGX_GetParam("REMOTE_ADDR", r
->envp
);
533 char *ban_reason
= 0;
535 char *cooldown_length
= 0;
536 uint_fast8_t found_idx
= 0;
538 /* In case someone is trying for a time GET, prioritize that */
539 time(&post_cmd
.prepared
.now
);
540 LOG("-----------------------------------------");
541 LOG("Handling post at %zu from %s", (size_t) post_cmd
.prepared
.now
,
545 LOG("Couldn't get REMOTE_ADDR (500)");
546 report_internal_error(r
);
550 if (util_normalize_ip(ip_raw
, &ip
) < 0) {
551 LOG("Couldn't normalize ip (500)");
552 report_internal_error(r
);
556 /* You can only POST to /action */
557 if (!(p
= FCGX_GetParam("REQUEST_METHOD", r
->envp
))) {
558 LOG("Couldn't get request method (500)");
559 report_internal_error(r
);
563 if (strcmp(p
, "POST")) {
564 LOG("request method was not POST (405)");
565 report_bad_method(r
);
569 /* We have to somehow feed this into multipart */
570 if (!(content_type
= FCGX_GetParam("CONTENT_TYPE", r
->envp
))) {
571 LOG("Can't get CONTENT_TYPE (500)");
572 report_internal_error(r
);
576 if (!(content_len_str
= FCGX_GetParam("CONTENT_LENGTH", r
->envp
))) {
577 LOG("Can't get CONTENT_LENGTH (500)");
578 report_internal_error(r
);
582 content_len
= (size_t) strtoll(content_len_str
, 0, 0);
584 if (content_len
> max_form_data_size
) {
585 LOG("Buffer would have exceeded %zuB (413)",
587 report_too_large(r
, "Total POST");
591 buf_main_len
= strlen(content_type_prefix
) + strlen(content_type
) +
592 strlen("\r\n\r\n") + content_len
;
594 if (buf_main_len
+ 1 < buf_main_len
) {
595 ERROR_MESSAGE("overflow");
599 if (!(buf_main
= malloc(buf_main_len
+ 1))) {
600 PERROR_MESSAGE("malloc");
604 size_t offset
= sprintf(buf_main
, "%s%s\r\n\r\n", content_type_prefix
,
607 /* Try and swallow this thing into a buffer */
608 FCGX_GetStr(buf_main
+ offset
, content_len
, r
->in
);
610 /* Okay, we've got it in the buffer */
611 if (multipart_decompose(buf_main
, buf_main_len
, &post_cmd
) < 0) {
612 LOG("Decoding message failed, returning (400)");
613 report_bad_request(r
, "Invalid multipart/form-data");
617 /* Now we can check what they actually wanted us to DO */
618 if (!post_cmd
.raw
.action
) {
619 LOG("No action specified (400)");
620 report_bad_request(r
, "You have to give action=something");
622 } else if (!(strcmp(post_cmd
.raw
.action
, "reply"))) {
623 post_cmd
.action_id
= REPLY
;
624 } else if (!(strcmp(post_cmd
.raw
.action
, "newthread"))) {
625 post_cmd
.action_id
= NEWTHREAD
;
626 } else if (!(strcmp(post_cmd
.raw
.action
, "rebuild"))) {
627 post_cmd
.action_id
= REBUILD
;
630 if (post_cmd
.raw
.thread
) {
631 post_cmd
.thread_id
= strtoll(post_cmd
.raw
.thread
, 0, 0);
634 if (post_cmd
.action_id
== NONE
) {
635 LOG("Invalid action \"%s\" (400)", post_cmd
.raw
.action
);
636 report_bad_request(r
, "That's not a valid action");
641 * XXX: the idea is to only accept REBUILD commmands from
642 * the local machine. Is this necessary and sufficient in
645 if (post_cmd
.action_id
== REBUILD
) {
646 /* Note that the IP is normalized so we can sort it */
647 if (strcmp(ip
, "127.000.000.001") &&
648 strcmp(ip
, "000.000.000.000") &&
649 strcmp(ip
, "0000:0000:0000:0000:0000:0000:0000:0001")) {
650 LOG("REBUILD requested from invalid ip %s", ip
);
651 report_bad_request(r
, "You can(not) rebuild");
658 /* And we can find where they wanted to do it */
661 if (!post_cmd
.raw
.board
) {
662 LOG("No board specified (400)");
663 report_bad_request(r
, "You have to give board=something");
667 if (post_cmd
.action_id
== REPLY
&&
668 !post_cmd
.thread_id
) {
669 LOG("Reply, yet no thread (400)");
670 report_bad_request(r
, "You have to give thread=something");
674 for (size_t j
= 0; j
< conf
->boards_num
; ++j
) {
675 if (!strcmp(post_cmd
.raw
.board
, conf
->boards
[j
].name
)) {
676 post_cmd
.board_idx
= j
;
683 LOG("Invalid board \"%s\" (400)", post_cmd
.raw
.board
);
684 report_bad_request(r
, "That's not a valid board");
691 if (db_check_bans(ip
, post_cmd
.board_idx
, post_cmd
.prepared
.now
,
692 &is_banned
, &ban_until
, &is_secret
, &ban_reason
) <
694 LOG("Couldn't determine ban status (500)");
695 report_internal_error(r
);
701 LOG("Ban[s] (until=\"%s\", reason=\"%s\") (200)",
702 ban_until
, UBSAFES(ban_reason
));
703 report_post_successful_with_redir(r
,
707 /* This should give HTTP 403 */
708 LOG("Ban (until=\"%s\", reason=\"%s\") (403)",
709 ban_until
, UBSAFES(ban_reason
));
710 report_ban(r
, ban_until
, ban_reason
);
715 if (post_cmd
.action_id
== REPLY
||
716 post_cmd
.action_id
== NEWTHREAD
) {
719 if (db_check_cooldowns(ip
, post_cmd
.board_idx
,
720 post_cmd
.prepared
.now
, &is_cooled
,
721 &cooldown_length
) < 0) {
722 LOG("Couldn't determine cooldown status (500)");
723 report_internal_error(r
);
728 /* This should give HTTP 429 */
729 LOG("Cooldown triggered (length=\"%s\") (429)",
731 report_cooldown(r
, cooldown_length
);
735 int correct_challenge
= 0;
737 if (!post_cmd
.raw
.challenge_id
) {
738 LOG("No challenge id given (403)");
739 report_bad_challenge(r
);
744 size_t challenge_idx
= (size_t) strtoll(
745 post_cmd
.raw
.challenge_id
, &e
, 0);
749 challenge_idx
= conf
->challenges_num
;
752 if (challenge_idx
>= conf
->challenges_num
) {
753 LOG("Bad challenge id \"%s\" given (403)",
754 post_cmd
.raw
.challenge_id
);
755 report_bad_challenge(r
);
759 if (!post_cmd
.raw
.challenge_response
) {
760 LOG("No challenge response given (403)");
761 report_bad_challenge(r
);
765 for (size_t j
= 0; j
< NUM_CHALLENGE_ANSWERS
; ++j
) {
766 if (!conf
->challenges
[challenge_idx
].answers
[j
]) {
770 if (!strcasecmp(post_cmd
.raw
.challenge_response
,
771 conf
->challenges
[challenge_idx
].answers
[
773 correct_challenge
= 1;
777 if (!correct_challenge
) {
778 LOG("Incorrect response \"%s\" to challenge %s (403)",
779 post_cmd
.raw
.challenge_response
,
780 post_cmd
.raw
.challenge_id
);
781 LOG("Comment was \"%s\"", UBSAFES(
782 post_cmd
.raw
.comment
));
783 report_bad_challenge(r
);
790 /* Now we split into specific actions */
791 switch (post_cmd
.action_id
) {
793 LOG("reply to /%s/%ju", UBSAFES(post_cmd
.raw
.board
),
795 handle_op_or_reply(conf
, r
, &post_cmd
, ip
, post_cmd
.thread_id
);
798 LOG("newthread on /%s/", UBSAFES(post_cmd
.raw
.board
));
799 handle_op_or_reply(conf
, r
, &post_cmd
, ip
, 0);
803 handle_rebuild(conf
, r
);
806 ERROR_MESSAGE("Impossible");
807 report_internal_error(r
);
812 clean_post_cmd(&post_cmd
);
816 free(cooldown_length
);
825 FCGX_Request r
= { 0 };
826 struct configuration conf
= { 0 };
828 setlocale(LC_ALL
, "");
830 /* tedu@ is probably laughing at me right now. Hi! */
832 conf
= (struct configuration
) {
834 .static_www_folder
= static_www_folder
, /* */
835 .work_path
= work_path
, /* */
836 .temp_dir_template
= temp_dir_template
, /* */
837 .trip_salt
= trip_salt
, /* */
838 .trip_salt_len
= strlen(trip_salt
), /* */
839 .boards
= boards
, /* */
840 .boards_num
= NUM_OF(boards
), /* */
841 .max_form_data_size
= max_form_data_size
, /* */
842 .max_file_size
= max_file_size
, /* */
843 .max_text_len
= max_text_len
, /* */
844 .filetypes
= filetypes
, /* */
845 .filetypes_num
= NUM_OF(filetypes
), /* */
846 .file_description_prog
= file_description_prog
, /* */
847 .headers
= headers
, /* */
848 .headers_num
= NUM_OF(headers
), /* */
849 .challenges
= challenges
, /* */
850 .challenges_num
= NUM_OF(challenges
), /* */
851 .wordfilter_inputs
= wordfilter_inputs
, /* */
852 .wordfilter_inputs_num
= NUM_OF(wordfilter_inputs
), /* */
853 .forbidden_inputs
= forbidden_inputs
, /* */
854 .forbidden_inputs_num
= NUM_OF(forbidden_inputs
), /* */
857 if (preconditions_check(&conf
) < 0) {
861 if (board_pages_init(&conf
) < 0) {
866 FCGX_InitRequest(&r
, 0, 0);
868 while (FCGX_Accept_r(&r
) == 0) {
878 clean_sanitize_comment();
879 clean_sanitize_file();
881 clean_write_thread();