httpd.c (30226B)
1 /* snac - A simple, minimalistic ActivityPub instance */ 2 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 3 4 #include "xs.h" 5 #include "xs_io.h" 6 #include "xs_json.h" 7 #include "xs_socket.h" 8 #include "xs_unix_socket.h" 9 #include "xs_httpd.h" 10 #include "xs_mime.h" 11 #include "xs_time.h" 12 #include "xs_openssl.h" 13 #include "xs_fcgi.h" 14 #include "xs_html.h" 15 16 #include "snac.h" 17 18 #include <setjmp.h> 19 #include <pthread.h> 20 #include <semaphore.h> 21 #include <fcntl.h> 22 #include <stdint.h> 23 #include <unistd.h> 24 25 #include <sys/resource.h> // for getrlimit() 26 #include <sys/mman.h> 27 28 #ifdef USE_POLL_FOR_SLEEP 29 #include <poll.h> 30 #endif 31 32 /** server state **/ 33 srv_state *p_state = NULL; 34 35 36 /** job control **/ 37 38 /* mutex to access the lists of jobs */ 39 static pthread_mutex_t job_mutex; 40 41 /* semaphore to trigger job processing */ 42 static sem_t *job_sem; 43 44 typedef struct job_fifo_item { 45 struct job_fifo_item *next; 46 xs_val *job; 47 } job_fifo_item; 48 49 static job_fifo_item *job_fifo_first = NULL; 50 static job_fifo_item *job_fifo_last = NULL; 51 52 53 /** other global data **/ 54 55 static jmp_buf on_break; 56 57 58 /** code **/ 59 60 /* nodeinfo 2.0 template */ 61 const char *nodeinfo_2_0_template = "" 62 "{\"version\":\"2.0\"," 63 "\"software\":{\"name\":\"snac\",\"version\":\"" VERSION "\"}," 64 "\"protocols\":[\"activitypub\"]," 65 "\"services\":{\"outbound\":[],\"inbound\":[]}," 66 "\"usage\":{\"users\":{\"total\":%d,\"activeMonth\":%d,\"activeHalfyear\":%d}," 67 "\"localPosts\":%d}," 68 "\"openRegistrations\":false,\"metadata\":{}}"; 69 70 xs_str *nodeinfo_2_0(void) 71 /* builds a nodeinfo json object */ 72 { 73 int n_utotal = 0; 74 int n_umonth = 0; 75 int n_uhyear = 0; 76 int n_posts = 0; 77 xs *users = user_list(); 78 xs_list *p = users; 79 const char *v; 80 double now = (double)time(NULL); 81 82 while (xs_list_iter(&p, &v)) { 83 /* build the full path name to the last usage log */ 84 xs *llfn = xs_fmt("%s/user/%s/lastlog.txt", srv_basedir, v); 85 double llsecs = now - mtime(llfn); 86 87 if (llsecs < 60 * 60 * 24 * 30 * 6) { 88 n_uhyear++; 89 90 if (llsecs < 60 * 60 * 24 * 30) 91 n_umonth++; 92 } 93 94 n_utotal++; 95 96 /* build the file to each user public.idx */ 97 xs *pidxfn = xs_fmt("%s/user/%s/public.idx", srv_basedir, v); 98 n_posts += index_len(pidxfn); 99 } 100 101 return xs_fmt(nodeinfo_2_0_template, n_utotal, n_umonth, n_uhyear, n_posts); 102 } 103 104 105 static xs_str *greeting_html(void) 106 /* processes and returns greeting.html */ 107 { 108 /* try to open greeting.html */ 109 xs *fn = xs_fmt("%s/greeting.html", srv_basedir); 110 FILE *f; 111 xs_str *s = NULL; 112 113 if ((f = fopen(fn, "r")) != NULL) { 114 s = xs_readall(f); 115 fclose(f); 116 117 /* replace %host% */ 118 s = xs_replace_i(s, "%host%", xs_dict_get(srv_config, "host")); 119 120 const char *adm_email = xs_dict_get(srv_config, "admin_email"); 121 if (xs_is_null(adm_email) || *adm_email == '\0') 122 adm_email = "the administrator of this instance"; 123 124 /* replace %admin_email */ 125 s = xs_replace_i(s, "%admin_email%", adm_email); 126 127 /* does it have a %userlist% mark? */ 128 if (xs_str_in(s, "%userlist%") != -1) { 129 const char *host = xs_dict_get(srv_config, "host"); 130 xs *list = user_list(); 131 xs_list *p = list; 132 const xs_str *uid; 133 134 xs_html *ul = xs_html_tag("ul", 135 xs_html_attr("class", "snac-user-list")); 136 137 p = list; 138 while (xs_list_iter(&p, &uid)) { 139 snac user; 140 141 if (strcmp(uid, "relay") && user_open(&user, uid)) { 142 xs *formatted_name = format_text_with_emoji(NULL, xs_dict_get(user.config, "name"), 1, NULL); 143 144 xs_html_add(ul, 145 xs_html_tag("li", 146 xs_html_tag("a", 147 xs_html_attr("href", user.actor), 148 xs_html_text("@"), 149 xs_html_text(uid), 150 xs_html_text("@"), 151 xs_html_text(host), 152 xs_html_text(" ("), 153 xs_html_raw(formatted_name), 154 xs_html_text(")")))); 155 156 user_free(&user); 157 } 158 } 159 160 xs *s1 = xs_html_render(ul); 161 s = xs_replace_i(s, "%userlist%", s1); 162 } 163 } 164 165 return s; 166 } 167 168 169 const char *share_page = "" 170 "<!DOCTYPE html>\n" 171 "<html>\n" 172 "<head>\n" 173 "<title>%s - snac</title>\n" 174 "<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">\n" 175 "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/style.css\"/>\n" 176 "<style>:root {color-scheme: light dark}</style>\n" 177 "</head>\n" 178 "<body><h1>%s link share</h1>\n" 179 "<form method=\"get\" action=\"%s/share-bridge\">\n" 180 "<textarea name=\"content\" rows=\"6\" wrap=\"virtual\" required=\"required\" style=\"width: 50em\">%s</textarea>\n" 181 "<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\" required=\"required\"></p>\n" 182 "<input type=\"submit\" value=\"OK\">\n" 183 "</form><p>%s</p></body></html>\n" 184 ""; 185 186 187 const char *authorize_interaction_page = "" 188 "<!DOCTYPE html>\n" 189 "<html>\n" 190 "<head>\n" 191 "<title>%s - snac</title>\n" 192 "<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">\n" 193 "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/style.css\"/>\n" 194 "<style>:root {color-scheme: light dark}</style>\n" 195 "</head>\n" 196 "<body><h1>%s authorize interaction</h1>\n" 197 "<form method=\"get\" action=\"%s/auth-int-bridge\">\n" 198 "<select name=\"action\">\n" 199 "<option value=\"Follow\">Follow</option>\n" 200 "<option value=\"Boost\">Boost</option>\n" 201 "<option value=\"Like\">Like</option>\n" 202 "</select> %s\n" 203 "<input type=\"hidden\" name=\"id\" value=\"%s\">\n" 204 "<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\" required=\"required\"></p>\n" 205 "<input type=\"submit\" value=\"OK\">\n" 206 "</form><p>%s</p></body></html>\n" 207 ""; 208 209 210 int server_get_handler(xs_dict *req, const char *q_path, 211 char **body, int *b_size, char **ctype) 212 /* basic server services */ 213 { 214 int status = 0; 215 216 const snac *user = NULL; 217 218 /* is it the server root? */ 219 if (*q_path == '\0' || strcmp(q_path, "/") == 0) { 220 const xs_dict *q_vars = xs_dict_get(req, "q_vars"); 221 const char *t = NULL; 222 223 if (xs_type(q_vars) == XSTYPE_DICT && (t = xs_dict_get(q_vars, "t"))) { 224 /** search by tag **/ 225 int skip = 0; 226 int show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", 227 xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); 228 const char *v; 229 230 if ((v = xs_dict_get(q_vars, "skip")) != NULL) 231 skip = atoi(v); 232 if ((v = xs_dict_get(q_vars, "show")) != NULL) 233 show = atoi(v); 234 235 xs *tl = tag_search(t, skip, show + 1); 236 int more = 0; 237 if (xs_list_len(tl) >= show + 1) { 238 /* drop the last one */ 239 tl = xs_list_del(tl, -1); 240 more = 1; 241 } 242 243 const char *accept = xs_dict_get(req, "accept"); 244 if (!xs_is_null(accept) && strcmp(accept, "application/rss+xml") == 0) { 245 xs *link = xs_fmt("%s/?t=%s", srv_baseurl, t); 246 247 *body = rss_from_timeline(NULL, tl, link, link, link); 248 *ctype = "application/rss+xml; charset=utf-8"; 249 } 250 else { 251 xs *page = xs_fmt("?t=%s", t); 252 xs *title = xs_fmt(L("Search results for tag #%s"), t); 253 *body = html_timeline(NULL, tl, 0, skip, show, more, title, page, 0, NULL); 254 } 255 } 256 else 257 if (xs_type(xs_dict_get(srv_config, "show_instance_timeline")) == XSTYPE_TRUE) { 258 /** instance timeline **/ 259 xs *tl = timeline_instance_list(0, 30); 260 *body = html_timeline(NULL, tl, 0, 0, 0, 0, 261 L("Recent posts by users in this instance"), NULL, 0, NULL); 262 } 263 else 264 *body = greeting_html(); 265 266 if (*body) 267 status = HTTP_STATUS_OK; 268 } 269 else 270 if (strcmp(q_path, "/susie.png") == 0 || strcmp(q_path, "/favicon.ico") == 0 ) { 271 status = HTTP_STATUS_OK; 272 *body = xs_base64_dec(default_avatar_base64(), b_size); 273 *ctype = "image/png"; 274 } 275 else 276 if (strcmp(q_path, "/.well-known/nodeinfo") == 0) { 277 status = HTTP_STATUS_OK; 278 *ctype = "application/json; charset=utf-8"; 279 *body = xs_fmt("{\"links\":[" 280 "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\"," 281 "\"href\":\"%s/nodeinfo_2_0\"}]}", 282 srv_baseurl); 283 } 284 else 285 if (strcmp(q_path, "/.well-known/host-meta") == 0) { 286 status = HTTP_STATUS_OK; 287 *ctype = "application/xrd+xml"; 288 *body = xs_fmt("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 289 "<XRD>" 290 "<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\"https://%s/.well-known/webfinger?resource={uri}\"/>" 291 "</XRD>", xs_dict_get(srv_config, "host")); 292 } 293 else 294 if (strcmp(q_path, "/nodeinfo_2_0") == 0) { 295 status = HTTP_STATUS_OK; 296 *ctype = "application/json; charset=utf-8"; 297 *body = nodeinfo_2_0(); 298 } 299 else 300 if (strcmp(q_path, "/robots.txt") == 0) { 301 status = HTTP_STATUS_OK; 302 *ctype = "text/plain"; 303 *body = xs_str_new("User-agent: *\n" 304 "Disallow: /\n"); 305 } 306 else 307 if (strcmp(q_path, "/style.css") == 0) { 308 FILE *f; 309 xs *css_fn = xs_fmt("%s/style.css", srv_basedir); 310 311 if ((f = fopen(css_fn, "r")) != NULL) { 312 *body = xs_readall(f); 313 fclose(f); 314 315 status = HTTP_STATUS_OK; 316 *ctype = "text/css"; 317 } 318 } 319 else 320 if (strcmp(q_path, "/share") == 0) { 321 const xs_dict *q_vars = xs_dict_get(req, "q_vars"); 322 const char *url = xs_dict_get(q_vars, "url"); 323 const char *text = xs_dict_get(q_vars, "text"); 324 xs *s = NULL; 325 326 if (xs_type(text) == XSTYPE_STRING) { 327 if (xs_type(url) == XSTYPE_STRING) 328 s = xs_fmt("%s:\n\n%s\n", text, url); 329 else 330 s = xs_fmt("%s\n", text); 331 } 332 else 333 if (xs_type(url) == XSTYPE_STRING) 334 s = xs_fmt("%s\n", url); 335 else 336 s = xs_str_new(NULL); 337 338 status = HTTP_STATUS_OK; 339 *ctype = "text/html; charset=utf-8"; 340 *body = xs_fmt(share_page, 341 xs_dict_get(srv_config, "host"), 342 srv_baseurl, 343 xs_dict_get(srv_config, "host"), 344 srv_baseurl, 345 s, 346 USER_AGENT 347 ); 348 } 349 else 350 if (strcmp(q_path, "/authorize_interaction") == 0) { 351 const xs_dict *q_vars = xs_dict_get(req, "q_vars"); 352 const char *uri = xs_dict_get(q_vars, "uri"); 353 354 if (xs_is_string(uri)) { 355 status = HTTP_STATUS_OK; 356 *ctype = "text/html; charset=utf-8"; 357 *body = xs_fmt(authorize_interaction_page, 358 xs_dict_get(srv_config, "host"), 359 srv_baseurl, 360 xs_dict_get(srv_config, "host"), 361 srv_baseurl, 362 uri, 363 uri, 364 USER_AGENT 365 ); 366 } 367 } 368 369 if (status != 0) 370 srv_debug(1, xs_fmt("server_get_handler serving '%s' %d", q_path, status)); 371 372 return status; 373 } 374 375 376 void httpd_connection(FILE *f) 377 /* the connection processor */ 378 { 379 xs *req; 380 const char *method; 381 int status = 0; 382 xs_str *body = NULL; 383 int b_size = 0; 384 char *ctype = NULL; 385 xs *headers = xs_dict_new(); 386 xs *q_path = NULL; 387 xs *payload = NULL; 388 xs *etag = NULL; 389 xs *last_modified = NULL; 390 xs *link = NULL; 391 int p_size = 0; 392 const char *p; 393 int fcgi_id; 394 int mmapped = 0; 395 396 if (p_state->use_fcgi) 397 req = xs_fcgi_request(f, &payload, &p_size, &fcgi_id); 398 else 399 req = xs_httpd_request(f, &payload, &p_size); 400 401 if (req == NULL) { 402 /* probably because a timeout */ 403 fclose(f); 404 return; 405 } 406 407 if (!(method = xs_dict_get(req, "method")) || !(p = xs_dict_get(req, "path"))) { 408 /* missing needed headers; discard */ 409 fclose(f); 410 return; 411 } 412 413 q_path = xs_dup(p); 414 415 /* crop the q_path from leading / and the prefix */ 416 if (xs_endswith(q_path, "/")) 417 q_path = xs_crop_i(q_path, 0, -1); 418 419 p = xs_dict_get(srv_config, "prefix"); 420 if (xs_startswith(q_path, p)) 421 q_path = xs_crop_i(q_path, strlen(p), 0); 422 423 if (strcmp(method, "GET") == 0 || strcmp(method, "HEAD") == 0) { 424 /* cascade through */ 425 if (status == 0) 426 status = server_get_handler(req, q_path, &body, &b_size, &ctype); 427 428 if (status == 0) 429 status = webfinger_get_handler(req, q_path, &body, &b_size, &ctype); 430 431 if (status == 0) 432 status = activitypub_get_handler(req, q_path, &body, &b_size, &ctype); 433 434 #ifndef NO_MASTODON_API 435 if (status == 0) 436 status = oauth_get_handler(req, q_path, &body, &b_size, &ctype); 437 438 if (status == 0) 439 status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype, &link); 440 #endif /* NO_MASTODON_API */ 441 442 if (status == 0) 443 status = html_get_handler(req, q_path, &body, &b_size, &ctype, &etag, &last_modified, &headers, &mmapped); 444 } 445 else 446 if (strcmp(method, "POST") == 0) { 447 448 #ifndef NO_MASTODON_API 449 if (status == 0) 450 status = oauth_post_handler(req, q_path, 451 payload, p_size, &body, &b_size, &ctype); 452 453 if (status == 0) 454 status = mastoapi_post_handler(req, q_path, 455 payload, p_size, &body, &b_size, &ctype); 456 #endif 457 458 if (status == 0) 459 status = activitypub_post_handler(req, q_path, 460 payload, p_size, &body, &b_size, &ctype); 461 462 if (status == 0) 463 status = html_post_handler(req, q_path, 464 payload, p_size, &body, &b_size, &ctype); 465 } 466 else 467 if (strcmp(method, "PUT") == 0) { 468 469 #ifndef NO_MASTODON_API 470 if (status == 0) 471 status = mastoapi_put_handler(req, q_path, 472 payload, p_size, &body, &b_size, &ctype); 473 #endif 474 475 } 476 else 477 if (strcmp(method, "PATCH") == 0) { 478 479 #ifndef NO_MASTODON_API 480 if (status == 0) 481 status = mastoapi_patch_handler(req, q_path, 482 payload, p_size, &body, &b_size, &ctype); 483 #endif 484 485 } 486 else 487 if (strcmp(method, "OPTIONS") == 0) { 488 const char *methods = "OPTIONS, GET, HEAD, POST, PUT, DELETE"; 489 headers = xs_dict_append(headers, "allow", methods); 490 headers = xs_dict_append(headers, "access-control-allow-methods", methods); 491 status = HTTP_STATUS_OK; 492 } 493 else 494 if (strcmp(method, "DELETE") == 0) { 495 #ifndef NO_MASTODON_API 496 if (status == 0) 497 status = mastoapi_delete_handler(req, q_path, 498 payload, p_size, &body, &b_size, &ctype); 499 #endif 500 } 501 502 /* unattended? it's an error */ 503 if (status == 0) { 504 srv_archive_error("unattended_method", "unattended method", req, payload); 505 srv_debug(1, xs_fmt("httpd_connection unattended %s %s", method, q_path)); 506 status = HTTP_STATUS_NOT_FOUND; 507 } 508 509 if (status == HTTP_STATUS_FORBIDDEN) 510 body = xs_str_new("<h1>403 Forbidden (" USER_AGENT ")</h1>"); 511 512 if (status == HTTP_STATUS_NOT_FOUND) 513 body = xs_str_new("<h1>404 Not Found (" USER_AGENT ")</h1>"); 514 515 if (status == HTTP_STATUS_BAD_REQUEST && body != NULL) 516 body = xs_str_new("<h1>400 Bad Request (" USER_AGENT ")</h1>"); 517 518 if (status == HTTP_STATUS_SEE_OTHER) 519 headers = xs_dict_append(headers, "location", body); 520 521 if (status == HTTP_STATUS_UNAUTHORIZED && body) { 522 xs *www_auth = xs_fmt("Basic realm=\"@%s@%s snac login\"", 523 body, xs_dict_get(srv_config, "host")); 524 525 headers = xs_dict_append(headers, "WWW-Authenticate", www_auth); 526 headers = xs_dict_append(headers, "Cache-Control", "no-cache, must-revalidate, max-age=0"); 527 } 528 529 if (ctype == NULL) 530 ctype = "text/html; charset=utf-8"; 531 532 headers = xs_dict_append(headers, "content-type", ctype); 533 headers = xs_dict_append(headers, "x-creator", USER_AGENT); 534 535 if (!xs_is_null(etag)) 536 headers = xs_dict_append(headers, "etag", etag); 537 if (!xs_is_null(last_modified)) 538 headers = xs_dict_append(headers, "last-modified", last_modified); 539 if (!xs_is_null(link)) 540 headers = xs_dict_append(headers, "Link", link); 541 542 /* if there are any additional headers, add them */ 543 const xs_dict *more_headers = xs_dict_get(srv_config, "http_headers"); 544 if (xs_type(more_headers) == XSTYPE_DICT) { 545 const char *k, *v; 546 int c = 0; 547 while (xs_dict_next(more_headers, &k, &v, &c)) 548 headers = xs_dict_set(headers, k, v); 549 } 550 551 if (b_size == 0 && body != NULL) 552 b_size = strlen(body); 553 554 /* if it was a HEAD, no body will be sent */ 555 if (strcmp(method, "HEAD") == 0) { 556 if (mmapped) { 557 munmap(body, b_size); 558 body = NULL; 559 mmapped = 0; 560 } 561 else 562 body = xs_free(body); 563 } 564 565 headers = xs_dict_append(headers, "access-control-allow-origin", "*"); 566 headers = xs_dict_append(headers, "access-control-allow-headers", "*"); 567 headers = xs_dict_append(headers, "access-control-expose-headers", "Link"); 568 569 /* disable any form of fucking JavaScript */ 570 headers = xs_dict_append(headers, "Content-Security-Policy", "script-src ;"); 571 572 if (p_state->use_fcgi) 573 xs_fcgi_response(f, status, headers, body, b_size, fcgi_id); 574 else 575 xs_httpd_response(f, status, http_status_text(status), headers, body, b_size); 576 577 fclose(f); 578 579 srv_archive("RECV", NULL, req, payload, p_size, status, headers, body, b_size); 580 581 /* JSON validation check */ 582 if (!xs_is_null(body) && strcmp(ctype, "application/json") == 0) { 583 xs *j = xs_json_loads(body); 584 585 if (j == NULL) { 586 srv_log(xs_fmt("bad JSON")); 587 srv_archive_error("bad_json", "bad JSON", req, body); 588 } 589 } 590 591 if (mmapped) { 592 munmap(body, b_size); 593 body = NULL; 594 mmapped = 0; 595 } 596 else 597 body = xs_free(body); 598 } 599 600 601 void job_post(const xs_val *job, int urgent) 602 /* posts a job for the threads to process it */ 603 { 604 if (job != NULL) { 605 /* lock the mutex */ 606 pthread_mutex_lock(&job_mutex); 607 608 job_fifo_item *i = xs_realloc(NULL, sizeof(job_fifo_item)); 609 *i = (job_fifo_item){ NULL, xs_dup(job) }; 610 611 if (job_fifo_first == NULL) 612 job_fifo_first = job_fifo_last = i; 613 else 614 if (urgent) { 615 /* prepend */ 616 i->next = job_fifo_first; 617 job_fifo_first = i; 618 } 619 else { 620 /* append */ 621 job_fifo_last->next = i; 622 job_fifo_last = i; 623 } 624 625 p_state->job_fifo_size++; 626 627 if (p_state->job_fifo_size > p_state->peak_job_fifo_size) 628 p_state->peak_job_fifo_size = p_state->job_fifo_size; 629 630 /* unlock the mutex */ 631 pthread_mutex_unlock(&job_mutex); 632 633 /* ask for someone to attend it */ 634 sem_post(job_sem); 635 } 636 } 637 638 639 void job_wait(xs_val **job) 640 /* waits for an available job */ 641 { 642 *job = NULL; 643 644 if (sem_wait(job_sem) == 0) { 645 /* lock the mutex */ 646 pthread_mutex_lock(&job_mutex); 647 648 /* dequeue */ 649 job_fifo_item *i = job_fifo_first; 650 651 if (i != NULL) { 652 job_fifo_first = i->next; 653 654 if (job_fifo_first == NULL) 655 job_fifo_last = NULL; 656 657 *job = i->job; 658 xs_free(i); 659 660 p_state->job_fifo_size--; 661 } 662 663 /* unlock the mutex */ 664 pthread_mutex_unlock(&job_mutex); 665 } 666 } 667 668 669 static void *job_thread(void *arg) 670 /* job thread */ 671 { 672 int pid = (int)(uintptr_t)arg; 673 674 srv_debug(1, xs_fmt("job thread %d started", pid)); 675 676 for (;;) { 677 xs *job = NULL; 678 679 p_state->th_state[pid] = THST_WAIT; 680 681 job_wait(&job); 682 683 if (job == NULL) /* corrupted message? */ 684 continue; 685 686 if (xs_type(job) == XSTYPE_FALSE) /* special message: exit */ 687 break; 688 else 689 if (xs_type(job) == XSTYPE_DATA) { 690 /* it's a socket */ 691 FILE *f = NULL; 692 693 p_state->th_state[pid] = THST_IN; 694 695 xs_data_get(&f, job); 696 697 if (f != NULL) 698 httpd_connection(f); 699 } 700 else { 701 /* it's a q_item */ 702 p_state->th_state[pid] = THST_QUEUE; 703 704 process_queue_item(job); 705 } 706 } 707 708 p_state->th_state[pid] = THST_STOP; 709 710 srv_debug(1, xs_fmt("job thread %d stopped", pid)); 711 712 return NULL; 713 } 714 715 /* background thread sleep control */ 716 static pthread_mutex_t sleep_mutex; 717 static pthread_cond_t sleep_cond; 718 719 static void *background_thread(void *arg) 720 /* background thread (queue management and other things) */ 721 { 722 time_t t, purge_time, rss_time; 723 724 (void)arg; 725 726 t = time(NULL); 727 728 /* first purge time */ 729 purge_time = t + 10 * 60; 730 731 /* first RSS polling time */ 732 rss_time = t + 15 * 60; 733 734 srv_log(xs_fmt("background thread started")); 735 736 while (p_state->srv_running) { 737 int cnt = 0; 738 739 p_state->th_state[0] = THST_QUEUE; 740 741 { 742 xs *list = user_list(); 743 const char *uid; 744 745 /* process queues for all users */ 746 xs_list_foreach(list, uid) { 747 snac user; 748 749 if (user_open(&user, uid)) { 750 cnt += process_user_queue(&user); 751 user_free(&user); 752 } 753 } 754 } 755 756 /* global queue */ 757 cnt += process_queue(); 758 759 t = time(NULL); 760 761 /* time to purge? */ 762 if (t > purge_time) { 763 /* next purge time is tomorrow */ 764 purge_time = t + 24 * 60 * 60; 765 766 xs *q_item = xs_dict_new(); 767 q_item = xs_dict_append(q_item, "type", "purge"); 768 job_post(q_item, 0); 769 } 770 771 /* time to poll the RSS? */ 772 if (t > rss_time) { 773 /* next RSS poll time */ 774 int hours = xs_number_get(xs_dict_get_def(srv_config, "rss_hashtag_poll_hours", "4")); 775 776 /* don't hammer servers too much */ 777 if (hours < 1) 778 hours = 1; 779 780 rss_time = t + 60 * 60 * hours; 781 782 xs *q_item = xs_dict_new(); 783 q_item = xs_dict_append(q_item, "type", "rss_hashtag_poll"); 784 job_post(q_item, 0); 785 } 786 787 if (cnt == 0) { 788 /* sleep 3 seconds */ 789 790 p_state->th_state[0] = THST_WAIT; 791 792 #ifdef USE_POLL_FOR_SLEEP 793 poll(NULL, 0, 3 * 1000); 794 #else 795 struct timespec ts; 796 797 clock_gettime(CLOCK_REALTIME, &ts); 798 ts.tv_sec += 3; 799 800 pthread_mutex_lock(&sleep_mutex); 801 while (pthread_cond_timedwait(&sleep_cond, &sleep_mutex, &ts) == 0); 802 pthread_mutex_unlock(&sleep_mutex); 803 #endif 804 } 805 } 806 807 p_state->th_state[0] = THST_STOP; 808 809 srv_log(xs_fmt("background thread stopped")); 810 811 return NULL; 812 } 813 814 815 void term_handler(int s) 816 { 817 (void)s; 818 819 longjmp(on_break, 1); 820 } 821 822 823 srv_state *srv_state_op(xs_str **fname, int op) 824 /* opens or deletes the shared memory object */ 825 { 826 int fd; 827 srv_state *ss = NULL; 828 829 if (*fname == NULL) 830 *fname = xs_fmt("/%s_snac_state", xs_dict_get(srv_config, "host")); 831 832 switch (op) { 833 case 0: /* open for writing */ 834 835 #ifdef WITHOUT_SHM 836 837 errno = ENOTSUP; 838 839 #else 840 841 if ((fd = shm_open(*fname, O_CREAT | O_RDWR, 0666)) != -1) { 842 if (ftruncate(fd, sizeof(*ss)) == -1 || 843 (ss = mmap(0, sizeof(*ss), PROT_READ | PROT_WRITE, 844 MAP_SHARED, fd, 0)) == MAP_FAILED) 845 ss = NULL; 846 847 close(fd); 848 } 849 850 #endif 851 852 if (ss == NULL) { 853 /* shared memory error: just create a plain structure */ 854 srv_log(xs_fmt("warning: shm object error (%s)", strerror(errno))); 855 ss = malloc(sizeof(*ss)); 856 } 857 858 /* init structure */ 859 *ss = (srv_state){0}; 860 ss->s_size = sizeof(*ss); 861 862 break; 863 864 case 1: /* open for reading */ 865 866 #ifdef WITHOUT_SHM 867 868 errno = ENOTSUP; 869 870 #else 871 872 if ((fd = shm_open(*fname, O_RDONLY, 0666)) != -1) { 873 if ((ss = mmap(0, sizeof(*ss), PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) 874 ss = NULL; 875 876 close(fd); 877 } 878 879 #endif 880 881 if (ss == NULL) { 882 /* shared memory error */ 883 srv_log(xs_fmt("error: shm object error (%s) server not running?", strerror(errno))); 884 } 885 else 886 if (ss->s_size != sizeof(*ss)) { 887 srv_log(xs_fmt("error: struct size mismatch (%d != %d)", 888 ss->s_size, sizeof(*ss))); 889 890 munmap(ss, sizeof(*ss)); 891 892 ss = NULL; 893 } 894 895 break; 896 897 case 2: /* unlink */ 898 899 #ifndef WITHOUT_SHM 900 901 if (*fname) 902 shm_unlink(*fname); 903 904 #endif 905 906 break; 907 } 908 909 return ss; 910 } 911 912 913 void httpd(void) 914 /* starts the server */ 915 { 916 const char *address = NULL; 917 const char *port = NULL; 918 xs *full_address = NULL; 919 volatile int rs; 920 pthread_t threads[MAX_THREADS] = {0}; 921 int n; 922 xs *sem_name = NULL; 923 xs *shm_name = NULL; 924 sem_t anon_job_sem; 925 xs *pidfile = xs_fmt("%s/server.pid", srv_basedir); 926 int pidfd; 927 928 { 929 /* do some pidfile locking acrobatics */ 930 if ((pidfd = open(pidfile, O_RDWR | O_CREAT, 0660)) == -1) { 931 srv_log(xs_fmt("Cannot create pidfile %s -- cannot continue", pidfile)); 932 return; 933 } 934 935 if (lockf(pidfd, F_TLOCK, 1) == -1) { 936 srv_log(xs_fmt("Cannot lock pidfile %s -- server already running?", pidfile)); 937 close(pidfd); 938 return; 939 } 940 941 (void)(ftruncate(pidfd, 0) + 1); 942 943 xs *s = xs_fmt("%d\n", (int)getpid()); 944 if (write(pidfd, s, strlen(s)) != (ssize_t)strlen(s)) { 945 srv_log(xs_fmt("Wrting to pidfile %s failed -- cannot continue", pidfile)); 946 return; 947 } 948 } 949 950 address = xs_dict_get(srv_config, "address"); 951 952 if (*address == '/') { 953 rs = xs_unix_socket_server(address, NULL); 954 full_address = xs_fmt("unix:%s", address); 955 } 956 else { 957 port = xs_number_str(xs_dict_get(srv_config, "port")); 958 full_address = xs_fmt("%s:%s", address, port); 959 960 rs = xs_socket_server(address, port); 961 } 962 963 if (rs == -1) { 964 srv_log(xs_fmt("cannot bind socket to %s", full_address)); 965 return; 966 } 967 968 /* setup the server stat structure */ 969 p_state = srv_state_op(&shm_name, 0); 970 971 p_state->srv_start_time = time(NULL); 972 973 p_state->use_fcgi = xs_type(xs_dict_get(srv_config, "fastcgi")) == XSTYPE_TRUE; 974 975 p_state->srv_running = 1; 976 977 signal(SIGPIPE, SIG_IGN); 978 signal(SIGTERM, term_handler); 979 signal(SIGINT, term_handler); 980 981 srv_log(xs_fmt("httpd%s start %s %s", p_state->use_fcgi ? " (FastCGI)" : "", 982 full_address, USER_AGENT)); 983 984 /* show the number of usable file descriptors */ 985 struct rlimit r; 986 getrlimit(RLIMIT_NOFILE, &r); 987 srv_debug(1, xs_fmt("available (rlimit) fds: %d (cur) / %d (max)", 988 (int) r.rlim_cur, (int) r.rlim_max)); 989 990 /* initialize the job control engine */ 991 pthread_mutex_init(&job_mutex, NULL); 992 sem_name = xs_fmt("/job_%d", getpid()); 993 job_sem = sem_open(sem_name, O_CREAT, 0644, 0); 994 995 if (job_sem == NULL) { 996 /* error opening a named semaphore; try with an anonymous one */ 997 if (sem_init(&anon_job_sem, 0, 0) != -1) 998 job_sem = &anon_job_sem; 999 } 1000 1001 if (job_sem == NULL) { 1002 srv_log(xs_fmt("fatal error: cannot create semaphore -- cannot continue")); 1003 return; 1004 } 1005 1006 /* initialize sleep control */ 1007 pthread_mutex_init(&sleep_mutex, NULL); 1008 pthread_cond_init(&sleep_cond, NULL); 1009 1010 p_state->n_threads = xs_number_get(xs_dict_get(srv_config, "num_threads")); 1011 1012 #ifdef _SC_NPROCESSORS_ONLN 1013 if (p_state->n_threads == 0) { 1014 /* get number of CPUs on the machine */ 1015 p_state->n_threads = sysconf(_SC_NPROCESSORS_ONLN); 1016 } 1017 #endif 1018 1019 if (p_state->n_threads < 4) 1020 p_state->n_threads = 4; 1021 1022 if (p_state->n_threads > MAX_THREADS) 1023 p_state->n_threads = MAX_THREADS; 1024 1025 srv_debug(0, xs_fmt("using %d threads", p_state->n_threads)); 1026 1027 /* thread #0 is the background thread */ 1028 pthread_create(&threads[0], NULL, background_thread, NULL); 1029 1030 /* the rest of threads are for job processing */ 1031 char *ptr = (char *) 0x1; 1032 for (n = 1; n < p_state->n_threads; n++) 1033 pthread_create(&threads[n], NULL, job_thread, ptr++); 1034 1035 if (setjmp(on_break) == 0) { 1036 for (;;) { 1037 int cs = xs_socket_accept(rs); 1038 1039 if (cs != -1) { 1040 FILE *f = fdopen(cs, "r+"); 1041 xs *job = xs_data_new(&f, sizeof(FILE *)); 1042 job_post(job, 1); 1043 } 1044 else 1045 break; 1046 } 1047 } 1048 1049 p_state->srv_running = 0; 1050 1051 /* send as many exit jobs as working threads */ 1052 for (n = 1; n < p_state->n_threads; n++) 1053 job_post(xs_stock(XSTYPE_FALSE), 0); 1054 1055 /* wait for all the threads to exit */ 1056 for (n = 0; n < p_state->n_threads; n++) 1057 pthread_join(threads[n], NULL); 1058 1059 sem_close(job_sem); 1060 sem_unlink(sem_name); 1061 1062 srv_state_op(&shm_name, 2); 1063 1064 xs *uptime = xs_str_time_diff(time(NULL) - p_state->srv_start_time); 1065 1066 srv_log(xs_fmt("httpd%s stop %s (run time: %s)", 1067 p_state->use_fcgi ? " (FastCGI)" : "", 1068 full_address, uptime)); 1069 1070 unlink(pidfile); 1071 }