quark

quark web server
git clone git://git.suckless.org/quark
Log | Files | Refs | 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 }