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