2.88 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    if (strcmp(method, "GET") == 0 || strcmp(method, "HEAD") == 0) {
 506        /* cascade through */
 507        if (status == 0)
 508            status = server_get_handler(req, q_path, &body, &b_size, &ctype);
 509
 510        if (status == 0)
 511            status = webfinger_get_handler(req, q_path, &body, &b_size, &ctype);
 512
 513        if (status == 0)
 514            status = activitypub_get_handler(req, q_path, &body, &b_size, &ctype);
 515
 516#ifndef NO_MASTODON_API
 517        if (status == 0)
 518            status = oauth_get_handler(req, q_path, &body, &b_size, &ctype);
 519
 520        if (status == 0)
 521            status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype, &link);
 522#endif /* NO_MASTODON_API */
 523
 524        if (status == 0)
 525            status = html_get_handler(req, q_path, &body, &b_size, &ctype, &etag, &last_modified);
 526    }
 527    else
 528    if (strcmp(method, "POST") == 0) {
 529
 530        if (status == 0)
 531            status = server_post_handler(req, q_path,
 532                        payload, p_size, &body, &b_size, &ctype);
 533
 534#ifndef NO_MASTODON_API
 535        if (status == 0)
 536            status = oauth_post_handler(req, q_path,
 537                        payload, p_size, &body, &b_size, &ctype);
 538
 539        if (status == 0)
 540            status = mastoapi_post_handler(req, q_path,
 541                        payload, p_size, &body, &b_size, &ctype);
 542#endif
 543
 544        if (status == 0)
 545            status = activitypub_post_handler(req, q_path,
 546                        payload, p_size, &body, &b_size, &ctype);
 547
 548        if (status == 0)
 549            status = html_post_handler(req, q_path,
 550                        payload, p_size, &body, &b_size, &ctype);
 551    }
 552    else
 553    if (strcmp(method, "PUT") == 0) {
 554
 555#ifndef NO_MASTODON_API
 556        if (status == 0)
 557            status = mastoapi_put_handler(req, q_path,
 558                        payload, p_size, &body, &b_size, &ctype);
 559#endif
 560
 561    }
 562    else
 563    if (strcmp(method, "PATCH") == 0) {
 564
 565#ifndef NO_MASTODON_API
 566        if (status == 0)
 567            status = mastoapi_patch_handler(req, q_path,
 568                        payload, p_size, &body, &b_size, &ctype);
 569#endif
 570
 571    }
 572    else
 573    if (strcmp(method, "OPTIONS") == 0) {
 574        const char *methods = "OPTIONS, GET, HEAD, POST, PUT, DELETE";
 575        headers = xs_dict_append(headers, "allow", methods);
 576        headers = xs_dict_append(headers, "access-control-allow-methods", methods);
 577        status = HTTP_STATUS_OK;
 578    }
 579    else
 580    if (strcmp(method, "DELETE") == 0) {
 581#ifndef NO_MASTODON_API
 582        if (status == 0)
 583            status = mastoapi_delete_handler(req, q_path,
 584                    payload, p_size, &body, &b_size, &ctype);
 585#endif
 586    }
 587
 588    /* unattended? it's an error */
 589    if (status == 0) {
 590        srv_archive_error("unattended_method", "unattended method", req, payload);
 591        srv_debug(1, xs_fmt("httpd_connection unattended %s %s", method, q_path));
 592        status = HTTP_STATUS_NOT_FOUND;
 593    }
 594
 595    if (status == HTTP_STATUS_FORBIDDEN)
 596        body = xs_str_new("<h1>403 Forbidden (" USER_AGENT ")</h1>");
 597
 598    if (status == HTTP_STATUS_NOT_FOUND)
 599        body = xs_str_new("<h1>404 Not Found (" USER_AGENT ")</h1>");
 600
 601    if (status == HTTP_STATUS_GONE)
 602        body = xs_str_new("<h1>410 Gone (" USER_AGENT ")</h1>");
 603
 604    if (status == HTTP_STATUS_BAD_REQUEST && body != NULL)
 605        body = xs_str_new("<h1>400 Bad Request (" USER_AGENT ")</h1>");
 606
 607    if (status == HTTP_STATUS_SEE_OTHER)
 608        headers = xs_dict_append(headers, "location", body);
 609
 610    if (status == HTTP_STATUS_UNAUTHORIZED && body) {
 611        xs *www_auth = xs_fmt("Basic realm=\"@%s@%s snac login\"",
 612                                body, xs_dict_get(srv_config, "host"));
 613
 614        headers = xs_dict_append(headers, "WWW-Authenticate", www_auth);
 615        headers = xs_dict_append(headers, "Cache-Control", "no-cache, must-revalidate, max-age=0");
 616    }
 617
 618    if (ctype == NULL)
 619        ctype = "text/html; charset=utf-8";
 620
 621    headers = xs_dict_append(headers, "content-type", ctype);
 622    headers = xs_dict_append(headers, "x-creator",    USER_AGENT);
 623
 624    if (!xs_is_null(etag))
 625        headers = xs_dict_append(headers, "etag", etag);
 626    if (!xs_is_null(last_modified))
 627        headers = xs_dict_append(headers, "last-modified", last_modified);
 628    if (!xs_is_null(link))
 629        headers = xs_dict_append(headers, "Link", link);
 630
 631    /* if there are any additional headers, add them */
 632    const xs_dict *more_headers = xs_dict_get(srv_config, "http_headers");
 633    if (xs_type(more_headers) == XSTYPE_DICT) {
 634        const char *k, *v;
 635        int c = 0;
 636        while (xs_dict_next(more_headers, &k, &v, &c))
 637            headers = xs_dict_set(headers, k, v);
 638    }
 639
 640    if (b_size == 0 && body != NULL)
 641        b_size = strlen(body);
 642
 643    /* if it was a HEAD, no body will be sent */
 644    if (strcmp(method, "HEAD") == 0)
 645        body = xs_free(body);
 646
 647    headers = xs_dict_append(headers, "access-control-allow-origin", "*");
 648    headers = xs_dict_append(headers, "access-control-allow-headers", "*");
 649    headers = xs_dict_append(headers, "access-control-expose-headers", "Link");
 650
 651    /* disable any form of fucking JavaScript */
 652    headers = xs_dict_append(headers, "Content-Security-Policy", "script-src ;");
 653
 654    if (p_state->use_fcgi)
 655        xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
 656    else
 657        xs_httpd_response(f, status, xs_http_status_text(status), headers, body, b_size);
 658
 659    fclose(f);
 660
 661    srv_archive("RECV", NULL, req, payload, p_size, status, headers, body, b_size);
 662
 663    /* JSON validation check */
 664    if (!xs_is_null(body) && strcmp(ctype, "application/json") == 0) {
 665        xs *j = xs_json_loads(body);
 666
 667        if (j == NULL) {
 668            srv_log(xs_fmt("bad JSON"));
 669            srv_archive_error("bad_json", "bad JSON", req, body);
 670        }
 671    }
 672
 673    xs_free(body);
 674}
 675
 676
 677void job_post(const xs_val *job, int urgent)
 678/* posts a job for the threads to process it */
 679{
 680    if (job != NULL) {
 681        /* lock the mutex */
 682        pthread_mutex_lock(&job_mutex);
 683
 684        job_fifo_item *i = xs_realloc(NULL, sizeof(job_fifo_item));
 685        *i = (job_fifo_item){ NULL, xs_dup(job) };
 686
 687        if (job_fifo_first == NULL)
 688            job_fifo_first = job_fifo_last = i;
 689        else
 690        if (urgent) {
 691            /* prepend */
 692            i->next = job_fifo_first;
 693            job_fifo_first = i;
 694        }
 695        else {
 696            /* append */
 697            job_fifo_last->next = i;
 698            job_fifo_last = i;
 699        }
 700
 701        p_state->job_fifo_size++;
 702
 703        if (p_state->job_fifo_size > p_state->peak_job_fifo_size)
 704            p_state->peak_job_fifo_size = p_state->job_fifo_size;
 705
 706        /* unlock the mutex */
 707        pthread_mutex_unlock(&job_mutex);
 708
 709        /* ask for someone to attend it */
 710        sem_post(job_sem);
 711    }
 712}
 713
 714
 715void job_wait(xs_val **job)
 716/* waits for an available job */
 717{
 718    *job = NULL;
 719
 720    if (sem_wait(job_sem) == 0) {
 721        /* lock the mutex */
 722        pthread_mutex_lock(&job_mutex);
 723
 724        /* dequeue */
 725        job_fifo_item *i = job_fifo_first;
 726
 727        if (i != NULL) {
 728            job_fifo_first = i->next;
 729
 730            if (job_fifo_first == NULL)
 731                job_fifo_last = NULL;
 732
 733            *job = i->job;
 734            xs_free(i);
 735
 736            p_state->job_fifo_size--;
 737        }
 738
 739        /* unlock the mutex */
 740        pthread_mutex_unlock(&job_mutex);
 741    }
 742}
 743
 744
 745static void *job_thread(void *arg)
 746/* job thread */
 747{
 748    int pid = (int)(uintptr_t)arg;
 749
 750    srv_debug(1, xs_fmt("job thread %d started", pid));
 751
 752    for (;;) {
 753        xs *job = NULL;
 754
 755        p_state->th_state[pid] = THST_WAIT;
 756
 757        job_wait(&job);
 758
 759        if (job == NULL) /* corrupted message? */
 760            continue;
 761
 762        if (xs_type(job) == XSTYPE_FALSE) /* special message: exit */
 763            break;
 764        else
 765        if (xs_type(job) == XSTYPE_DATA) {
 766            /* it's a socket */
 767            FILE *f = NULL;
 768
 769            p_state->th_state[pid] = THST_IN;
 770
 771            xs_data_get(&f, job);
 772
 773            if (f != NULL)
 774                httpd_connection(f);
 775        }
 776        else {
 777            /* it's a q_item */
 778            p_state->th_state[pid] = THST_QUEUE;
 779
 780            process_queue_item(job);
 781        }
 782    }
 783
 784    p_state->th_state[pid] = THST_STOP;
 785
 786    srv_debug(1, xs_fmt("job thread %d stopped", pid));
 787
 788    return NULL;
 789}
 790
 791/* background thread sleep control */
 792static pthread_mutex_t sleep_mutex;
 793static pthread_cond_t  sleep_cond;
 794
 795static void *background_thread(void *arg)
 796/* background thread (queue management and other things) */
 797{
 798    time_t t, purge_time, rss_time;
 799
 800    (void)arg;
 801
 802    t = time(NULL);
 803
 804    /* first purge time */
 805    purge_time = t + 10 * 60;
 806
 807    /* first RSS polling time */
 808    rss_time = t + 15 * 60;
 809
 810    srv_log(xs_fmt("background thread started"));
 811
 812    enqueue_fsck();
 813
 814    while (p_state->srv_running) {
 815        int cnt = 0;
 816
 817        p_state->th_state[0] = THST_QUEUE;
 818
 819        {
 820            xs *list = user_list();
 821            const char *uid;
 822
 823            /* process queues for all users */
 824            xs_list_foreach(list, uid) {
 825                snac user;
 826
 827                if (user_open(&user, uid)) {
 828                    cnt += process_user_queue(&user);
 829                    user_free(&user);
 830                }
 831            }
 832        }
 833
 834        /* global queue */
 835        cnt += process_queue();
 836
 837        t = time(NULL);
 838
 839        /* time to purge? */
 840        if (t > purge_time) {
 841            /* next purge time is tomorrow */
 842            purge_time = t + 24 * 60 * 60;
 843
 844            xs *q_item = xs_dict_new();
 845            q_item = xs_dict_append(q_item, "type", "purge");
 846            job_post(q_item, 0);
 847        }
 848
 849        /* time to poll the RSS? */
 850        if (t > rss_time) {
 851            /* next RSS poll time */
 852            int hours = xs_number_get(xs_dict_get_def(srv_config, "rss_hashtag_poll_hours", "4"));
 853
 854            /* don't hammer servers too much */
 855            if (hours < 1)
 856                hours = 1;
 857
 858            rss_time = t + 60 * 60 * hours;
 859
 860            xs *q_item = xs_dict_new();
 861            q_item = xs_dict_append(q_item, "type", "rss_hashtag_poll");
 862            job_post(q_item, 0);
 863        }
 864
 865        if (cnt == 0) {
 866            /* sleep 3 seconds */
 867
 868            p_state->th_state[0] = THST_WAIT;
 869
 870#ifdef USE_POLL_FOR_SLEEP
 871            poll(NULL, 0, 3 * 1000);
 872#else
 873            struct timespec ts;
 874
 875            clock_gettime(CLOCK_REALTIME, &ts);
 876            ts.tv_sec += 3;
 877
 878            pthread_mutex_lock(&sleep_mutex);
 879            while (pthread_cond_timedwait(&sleep_cond, &sleep_mutex, &ts) == 0);
 880            pthread_mutex_unlock(&sleep_mutex);
 881#endif
 882        }
 883    }
 884
 885    p_state->th_state[0] = THST_STOP;
 886
 887    srv_log(xs_fmt("background thread stopped"));
 888
 889    return NULL;
 890}
 891
 892
 893void term_handler(int s)
 894{
 895    (void)s;
 896
 897    longjmp(on_break, 1);
 898}
 899
 900
 901srv_state *srv_state_op(xs_str **fname, int op)
 902/* opens or deletes the shared memory object */
 903{
 904    int fd;
 905    srv_state *ss = NULL;
 906
 907    if (*fname == NULL)
 908        *fname = xs_fmt("/%s_snac_state", xs_dict_get(srv_config, "host"));
 909
 910    switch (op) {
 911    case 0: /* open for writing */
 912
 913#ifdef WITHOUT_SHM
 914
 915        errno = ENOTSUP;
 916
 917#else
 918
 919        if ((fd = shm_open(*fname, O_CREAT | O_RDWR, 0666)) != -1) {
 920            ftruncate(fd, sizeof(*ss));
 921
 922            if ((ss = mmap(0, sizeof(*ss), PROT_READ | PROT_WRITE,
 923                 MAP_SHARED, fd, 0)) == MAP_FAILED)
 924                ss = NULL;
 925
 926            close(fd);
 927        }
 928
 929#endif
 930
 931        if (ss == NULL) {
 932            /* shared memory error: just create a plain structure */
 933            srv_log(xs_fmt("warning: shm object error (%s)", strerror(errno)));
 934            ss = malloc(sizeof(*ss));
 935        }
 936
 937        /* init structure */
 938        *ss = (srv_state){0};
 939        ss->s_size = sizeof(*ss);
 940
 941        break;
 942
 943    case 1: /* open for reading */
 944
 945#ifdef WITHOUT_SHM
 946
 947        errno = ENOTSUP;
 948
 949#else
 950
 951        if ((fd = shm_open(*fname, O_RDONLY, 0666)) != -1) {
 952            if ((ss = mmap(0, sizeof(*ss), PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED)
 953                ss = NULL;
 954
 955            close(fd);
 956        }
 957
 958#endif
 959
 960        if (ss == NULL) {
 961            /* shared memory error */
 962            srv_log(xs_fmt("error: shm object error (%s) server not running?", strerror(errno)));
 963        }
 964        else
 965        if (ss->s_size != sizeof(*ss)) {
 966            srv_log(xs_fmt("error: struct size mismatch (%d != %d)",
 967                ss->s_size, sizeof(*ss)));
 968
 969            munmap(ss, sizeof(*ss));
 970
 971            ss = NULL;
 972        }
 973
 974        break;
 975
 976    case 2: /* unlink */
 977
 978#ifndef WITHOUT_SHM
 979
 980        if (*fname)
 981            shm_unlink(*fname);
 982
 983#endif
 984
 985        break;
 986    }
 987
 988    return ss;
 989}
 990
 991
 992void httpd(void)
 993/* starts the server */
 994{
 995    const char *address = NULL;
 996    const char *port = NULL;
 997    xs *full_address = NULL;
 998    int rs;
 999    pthread_t threads[MAX_THREADS] = {0};
1000    int n;
1001    xs *sem_name = NULL;
1002    xs *shm_name = NULL;
1003    sem_t anon_job_sem;
1004    xs *pidfile = xs_fmt("%s/server.pid", srv_basedir);
1005    int pidfd;
1006
1007    {
1008        /* do some pidfile locking acrobatics */
1009        if ((pidfd = open(pidfile, O_RDWR | O_CREAT, 0660)) == -1) {
1010            srv_log(xs_fmt("Cannot create pidfile %s -- cannot continue", pidfile));
1011            return;
1012        }
1013
1014        if (lockf(pidfd, F_TLOCK, 1) == -1) {
1015            srv_log(xs_fmt("Cannot lock pidfile %s -- server already running?", pidfile));
1016            close(pidfd);
1017            return;
1018        }
1019
1020        ftruncate(pidfd, 0);
1021
1022        xs *s = xs_fmt("%d\n", (int)getpid());
1023        write(pidfd, s, strlen(s));
1024    }
1025
1026    address = xs_dict_get(srv_config, "address");
1027
1028    if (*address == '/') {
1029        rs = xs_unix_socket_server(address, NULL);
1030        full_address = xs_fmt("unix:%s", address);
1031    }
1032    else {
1033        port = xs_number_str(xs_dict_get(srv_config, "port"));
1034        full_address = xs_fmt("%s:%s", address, port);
1035
1036        rs = xs_socket_server(address, port);
1037    }
1038
1039    if (rs == -1) {
1040        srv_log(xs_fmt("cannot bind socket to %s", full_address));
1041        return;
1042    }
1043
1044    /* setup the server stat structure */
1045    p_state = srv_state_op(&shm_name, 0);
1046
1047    p_state->srv_start_time = time(NULL);
1048
1049    p_state->use_fcgi = xs_type(xs_dict_get(srv_config, "fastcgi")) == XSTYPE_TRUE;
1050
1051    p_state->srv_running = 1;
1052
1053    signal(SIGPIPE, SIG_IGN);
1054    signal(SIGTERM, term_handler);
1055    signal(SIGINT,  term_handler);
1056
1057    srv_log(xs_fmt("httpd%s start %s %s", p_state->use_fcgi ? " (FastCGI)" : "",
1058                    full_address, USER_AGENT));
1059
1060    /* show the number of usable file descriptors */
1061    struct rlimit r;
1062    getrlimit(RLIMIT_NOFILE, &r);
1063    srv_debug(1, xs_fmt("available (rlimit) fds: %d (cur) / %d (max)",
1064                        (int) r.rlim_cur, (int) r.rlim_max));
1065
1066    /* initialize the job control engine */
1067    pthread_mutex_init(&job_mutex, NULL);
1068    sem_name = xs_fmt("/job_%d", getpid());
1069    job_sem = sem_open(sem_name, O_CREAT, 0644, 0);
1070
1071    if (job_sem == NULL) {
1072        /* error opening a named semaphore; try with an anonymous one */
1073        if (sem_init(&anon_job_sem, 0, 0) != -1)
1074            job_sem = &anon_job_sem;
1075    }
1076
1077    if (job_sem == NULL) {
1078        srv_log(xs_fmt("fatal error: cannot create semaphore -- cannot continue"));
1079        return;
1080    }
1081
1082    /* initialize sleep control */
1083    pthread_mutex_init(&sleep_mutex, NULL);
1084    pthread_cond_init(&sleep_cond, NULL);
1085
1086    p_state->n_threads = xs_number_get(xs_dict_get(srv_config, "num_threads"));
1087
1088#ifdef _SC_NPROCESSORS_ONLN
1089    if (p_state->n_threads == 0) {
1090        /* get number of CPUs on the machine */
1091        p_state->n_threads = sysconf(_SC_NPROCESSORS_ONLN);
1092    }
1093#endif
1094
1095    if (p_state->n_threads < 4)
1096        p_state->n_threads = 4;
1097
1098    if (p_state->n_threads > MAX_THREADS)
1099        p_state->n_threads = MAX_THREADS;
1100
1101    srv_debug(0, xs_fmt("using %d threads", p_state->n_threads));
1102
1103    /* thread #0 is the background thread */
1104    pthread_create(&threads[0], NULL, background_thread, NULL);
1105
1106    /* the rest of threads are for job processing */
1107    char *ptr = (char *) 0x1;
1108    for (n = 1; n < p_state->n_threads; n++)
1109        pthread_create(&threads[n], NULL, job_thread, ptr++);
1110
1111    if (setjmp(on_break) == 0) {
1112        for (;;) {
1113            int cs = xs_socket_accept(rs);
1114
1115            if (cs != -1) {
1116                FILE *f = fdopen(cs, "r+");
1117                xs *job = xs_data_new(&f, sizeof(FILE *));
1118                job_post(job, 1);
1119            }
1120            else
1121                break;
1122        }
1123    }
1124
1125    p_state->srv_running = 0;
1126
1127    /* send as many exit jobs as working threads */
1128    for (n = 1; n < p_state->n_threads; n++)
1129        job_post(xs_stock(XSTYPE_FALSE), 0);
1130
1131    /* wait for all the threads to exit */
1132    for (n = 0; n < p_state->n_threads; n++)
1133        pthread_join(threads[n], NULL);
1134
1135    sem_close(job_sem);
1136    sem_unlink(sem_name);
1137
1138    srv_state_op(&shm_name, 2);
1139
1140    xs *uptime = xs_str_time_diff(time(NULL) - p_state->srv_start_time);
1141
1142    srv_log(xs_fmt("httpd%s stop %s (run time: %s)",
1143                p_state->use_fcgi ? " (FastCGI)" : "",
1144                full_address, uptime));
1145
1146    unlink(pidfile);
1147}