main.c - quark - quark web server
 (HTM) git clone git://git.suckless.org/quark
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) LICENSE
       ---
       main.c (8711B)
       ---
            1 /* See LICENSE file for copyright and license details. */
            2 #include <errno.h>
            3 #include <grp.h>
            4 #include <limits.h>
            5 #include <pwd.h>
            6 #include <regex.h>
            7 #include <signal.h>
            8 #include <stddef.h>
            9 #include <stdlib.h>
           10 #include <string.h>
           11 #include <sys/resource.h>
           12 #include <sys/time.h>
           13 #include <sys/types.h>
           14 #include <sys/wait.h>
           15 #include <unistd.h>
           16 
           17 #include "arg.h"
           18 #include "server.h"
           19 #include "sock.h"
           20 #include "util.h"
           21 
           22 static char *udsname;
           23 
           24 static void
           25 cleanup(void)
           26 {
           27         if (udsname) {
           28                 sock_rem_uds(udsname);
           29         }
           30 }
           31 
           32 static void
           33 sigcleanup(int sig)
           34 {
           35         cleanup();
           36         kill(0, sig);
           37         _exit(1);
           38 }
           39 
           40 static void
           41 handlesignals(void(*hdl)(int))
           42 {
           43         struct sigaction sa = {
           44                 .sa_handler = hdl,
           45         };
           46 
           47         sigemptyset(&sa.sa_mask);
           48         sigaction(SIGTERM, &sa, NULL);
           49         sigaction(SIGHUP, &sa, NULL);
           50         sigaction(SIGINT, &sa, NULL);
           51         sigaction(SIGQUIT, &sa, NULL);
           52 }
           53 
           54 static void
           55 usage(void)
           56 {
           57         const char *opts = "[-u user] [-g group] [-n num] [-d dir] [-l] "
           58                            "[-i file] [-v vhost] ... [-m map] ...";
           59 
           60         die("usage: %s -p port [-h host] %s\n"
           61             "       %s -U file [-p port] %s", argv0,
           62             opts, argv0, opts);
           63 }
           64 
           65 int
           66 main(int argc, char *argv[])
           67 {
           68         struct group *grp = NULL;
           69         struct passwd *pwd = NULL;
           70         struct rlimit rlim;
           71         struct server srv = {
           72                 .docindex = "index.html",
           73         };
           74         size_t i;
           75         int insock, status = 0;
           76         const char *err;
           77         char *tok[4];
           78 
           79         /* defaults */
           80         size_t nthreads = 4;
           81         size_t nslots = 64;
           82         char *servedir = ".";
           83         char *user = "nobody";
           84         char *group = "nogroup";
           85 
           86         ARGBEGIN {
           87         case 'd':
           88                 servedir = EARGF(usage());
           89                 break;
           90         case 'g':
           91                 group = EARGF(usage());
           92                 break;
           93         case 'h':
           94                 srv.host = EARGF(usage());
           95                 break;
           96         case 'i':
           97                 srv.docindex = EARGF(usage());
           98                 if (strchr(srv.docindex, '/')) {
           99                         die("The document index must not contain '/'");
          100                 }
          101                 break;
          102         case 'l':
          103                 srv.listdirs = 1;
          104                 break;
          105         case 'm':
          106                 if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1]) {
          107                         usage();
          108                 }
          109                 if (!(srv.map = reallocarray(srv.map, ++srv.map_len,
          110                                              sizeof(struct map)))) {
          111                         die("reallocarray:");
          112                 }
          113                 srv.map[srv.map_len - 1].from  = tok[0];
          114                 srv.map[srv.map_len - 1].to    = tok[1];
          115                 srv.map[srv.map_len - 1].chost = tok[2];
          116                 break;
          117         case 's':
          118                 err = NULL;
          119                 nslots = strtonum(EARGF(usage()), 1, INT_MAX, &err);
          120                 if (err) {
          121                         die("strtonum '%s': %s", EARGF(usage()), err);
          122                 }
          123                 break;
          124         case 't':
          125                 err = NULL;
          126                 nthreads = strtonum(EARGF(usage()), 1, INT_MAX, &err);
          127                 if (err) {
          128                         die("strtonum '%s': %s", EARGF(usage()), err);
          129                 }
          130                 break;
          131         case 'p':
          132                 srv.port = EARGF(usage());
          133                 break;
          134         case 'U':
          135                 udsname = EARGF(usage());
          136                 break;
          137         case 'u':
          138                 user = EARGF(usage());
          139                 break;
          140         case 'v':
          141                 if (spacetok(EARGF(usage()), tok, 4) || !tok[0] || !tok[1] ||
          142                     !tok[2]) {
          143                         usage();
          144                 }
          145                 if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_len,
          146                                                sizeof(*srv.vhost)))) {
          147                         die("reallocarray:");
          148                 }
          149                 srv.vhost[srv.vhost_len - 1].chost  = tok[0];
          150                 srv.vhost[srv.vhost_len - 1].regex  = tok[1];
          151                 srv.vhost[srv.vhost_len - 1].dir    = tok[2];
          152                 srv.vhost[srv.vhost_len - 1].prefix = tok[3];
          153                 break;
          154         default:
          155                 usage();
          156         } ARGEND
          157 
          158         if (argc) {
          159                 usage();
          160         }
          161 
          162         /* can't have both host and UDS but must have one of port or UDS*/
          163         if ((srv.host && udsname) || !(srv.port || udsname)) {
          164                 usage();
          165         }
          166 
          167         if (udsname && (!access(udsname, F_OK) || errno != ENOENT)) {
          168                 die("UNIX-domain socket '%s': %s", udsname, errno ?
          169                     strerror(errno) : "File exists");
          170         }
          171 
          172         /* compile and check the supplied vhost regexes */
          173         for (i = 0; i < srv.vhost_len; i++) {
          174                 if (regcomp(&srv.vhost[i].re, srv.vhost[i].regex,
          175                             REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
          176                         die("regcomp '%s': invalid regex",
          177                             srv.vhost[i].regex);
          178                 }
          179         }
          180 
          181         /* validate user and group */
          182         errno = 0;
          183         if (!user || !(pwd = getpwnam(user))) {
          184                 die("getpwnam '%s': %s", user ? user : "null",
          185                     errno ? strerror(errno) : "Entry not found");
          186         }
          187         errno = 0;
          188         if (!group || !(grp = getgrnam(group))) {
          189                 die("getgrnam '%s': %s", group ? group : "null",
          190                     errno ? strerror(errno) : "Entry not found");
          191         }
          192 
          193         /* open a new process group */
          194         setpgid(0, 0);
          195 
          196         handlesignals(sigcleanup);
          197 
          198         /*
          199          * set the maximum number of open file descriptors as needed
          200          *  - 3 initial fd's
          201          *  - nthreads fd's for the listening socket
          202          *  - (nthreads * nslots) fd's for the connection-fd
          203          *  - (5 * nthreads) fd's for general purpose thread-use
          204          */
          205         rlim.rlim_cur = rlim.rlim_max = 3 + nthreads + nthreads * nslots +
          206                                         5 * nthreads;
          207         if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) {
          208                 if (errno == EPERM) {
          209                         die("You need to run as root or have "
          210                             "CAP_SYS_RESOURCE set, or are asking for more "
          211                             "file descriptors than the system can offer");
          212                 } else {
          213                         die("setrlimit:");
          214                 }
          215         }
          216 
          217         /*
          218          * create the (non-blocking) listening socket
          219          *
          220          * we could use SO_REUSEPORT and create a listening socket for
          221          * each thread (for better load-balancing, given each thread
          222          * would get his own kernel-queue), but this increases latency
          223          * (as a thread might get stuck on a larger request, making all
          224          * other request wait in line behind it).
          225          *
          226          * socket contention with a single listening socket is a
          227          * non-issue and thread-load-balancing is better fixed in the
          228          * kernel by changing epoll-sheduling from a FIFO- to a
          229          * LIFO-model, especially as it doesn't affect performance
          230          */
          231         insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) :
          232                            sock_get_ips(srv.host, srv.port);
          233         if (sock_set_nonblocking(insock)) {
          234                 return 1;
          235         }
          236 
          237         /*
          238          * before dropping privileges, we fork, as we need to remove
          239          * the UNIX-domain socket when we shut down, which we need
          240          * privileges for
          241          */
          242         switch (fork()) {
          243         case -1:
          244                 warn("fork:");
          245                 break;
          246         case 0:
          247                 /* restore default handlers */
          248                 handlesignals(SIG_DFL);
          249 
          250                 /* reap children automatically */
          251                 if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
          252                         die("signal: Failed to set SIG_IGN on SIGCHLD");
          253                 }
          254                 if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
          255                         die("signal: Failed to set SIG_IGN on SIGPIPE");
          256                 }
          257 
          258                 /*
          259                  * try increasing the thread-limit by the number
          260                  * of threads we need (which is the only reliable
          261                  * workaround I know given the thread-limit is per user
          262                  * rather than per process), but ignore EPERM errors,
          263                  * because this most probably means the user has already
          264                  * set the value to the kernel's limit, and there's not
          265                  * much we can do in any other case.
          266                  * There's also no danger of overflow as the value
          267                  * returned by getrlimit() is way below the limits of the
          268                  * rlim_t datatype.
          269                  */
          270                 if (getrlimit(RLIMIT_NPROC, &rlim) < 0) {
          271                         die("getrlimit:");
          272                 }
          273                 if (rlim.rlim_max == RLIM_INFINITY) {
          274                         if (rlim.rlim_cur != RLIM_INFINITY) {
          275                                 /* try increasing current limit by nthreads */
          276                                 rlim.rlim_cur += nthreads;
          277                         }
          278                 } else {
          279                         /* try increasing current and hard limit by nthreads */
          280                         rlim.rlim_cur = rlim.rlim_max += nthreads;
          281                 }
          282                 if (setrlimit(RLIMIT_NPROC, &rlim) < 0 && errno != EPERM) {
          283                         die("setrlimit()");
          284                 }
          285 
          286                 /* limit ourselves to reading the servedir and block further unveils */
          287                 eunveil(servedir, "r");
          288                 eunveil(NULL, NULL);
          289 
          290                 /* chroot */
          291                 if (chdir(servedir) < 0) {
          292                         die("chdir '%s':", servedir);
          293                 }
          294                 if (chroot(".") < 0) {
          295                         if (errno == EPERM) {
          296                                 die("You need to run as root or have "
          297                                     "CAP_SYS_CHROOT set");
          298                         } else {
          299                                 die("chroot:");
          300                         }
          301                 }
          302 
          303                 /* drop root */
          304                 if (pwd->pw_uid == 0 || grp->gr_gid == 0) {
          305                         die("Won't run under root %s for hopefully obvious reasons",
          306                             (pwd->pw_uid == 0) ? (grp->gr_gid == 0) ?
          307                             "user and group" : "user" : "group");
          308                 }
          309 
          310                 if (setgroups(1, &(grp->gr_gid)) < 0) {
          311                         if (errno == EPERM) {
          312                                 die("You need to run as root or have "
          313                                     "CAP_SETGID set");
          314                         } else {
          315                                 die("setgroups:");
          316                         }
          317                 }
          318                 if (setgid(grp->gr_gid) < 0) {
          319                         if (errno == EPERM) {
          320                                 die("You need to run as root or have "
          321                                     "CAP_SETGID set");
          322                         } else {
          323                                 die("setgid:");
          324                         }
          325 
          326                 }
          327                 if (setuid(pwd->pw_uid) < 0) {
          328                         if (errno == EPERM) {
          329                                 die("You need to run as root or have "
          330                                     "CAP_SETUID set");
          331                         } else {
          332                                 die("setuid:");
          333                         }
          334                 }
          335 
          336                 if (udsname) {
          337                         epledge("stdio rpath proc unix", NULL);
          338                 } else {
          339                         epledge("stdio rpath proc inet", NULL);
          340                 }
          341 
          342                 /* accept incoming connections */
          343                 server_init_thread_pool(insock, nthreads, nslots, &srv);
          344 
          345                 exit(0);
          346         default:
          347                 /* limit ourselves even further while we are waiting */
          348                 if (udsname) {
          349                         eunveil(udsname, "c");
          350                         eunveil(NULL, NULL);
          351                         epledge("stdio cpath", NULL);
          352                 } else {
          353                         eunveil("/", "");
          354                         eunveil(NULL, NULL);
          355                         epledge("stdio", NULL);
          356                 }
          357 
          358                 while (wait(&status) > 0)
          359                         ;
          360         }
          361 
          362         cleanup();
          363         return status;
          364 }