| main.c - 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[… | |
| 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[… | |
| 142 !tok[2]) { | |
| 143 usage(); | |
| 144 } | |
| 145 if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_le… | |
| 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 mor… | |
| 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_gi… | |
| 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 nthre… | |
| 276 rlim.rlim_cur += nthreads; | |
| 277 } | |
| 278 } else { | |
| 279 /* try increasing current and hard limit by nthr… | |
| 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 fur… | |
| 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 obvio… | |
| 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 } |