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