about summary refs log tree commit diff
path: root/bounce.c
diff options
context:
space:
mode:
Diffstat (limited to 'bounce.c')
-rw-r--r--bounce.c592
1 files changed, 305 insertions, 287 deletions
diff --git a/bounce.c b/bounce.c
index 1aae7a3..9ab0f1d 100644
--- a/bounce.c
+++ b/bounce.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June McEnroe <june@causal.agency>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -12,9 +12,19 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with OpenSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of OpenSSL used as well as that of the
+ * covered work.
  */
 
-#include <assert.h>
 #include <err.h>
 #include <errno.h>
 #include <fcntl.h>
@@ -31,84 +41,54 @@
 #include <sys/file.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
+#include <sys/time.h>
 #include <sysexits.h>
+#include <time.h>
 #include <tls.h>
 #include <unistd.h>
 
-#ifdef __FreeBSD__
-#include <sys/capsicum.h>
+#ifndef SIGINFO
+#define SIGINFO SIGUSR2
 #endif
 
 #include "bounce.h"
 
 bool verbose;
 
-static void hashPass(void) {
-	char *pass = getpass("Password: ");
-	byte rand[12];
-	arc4random_buf(rand, sizeof(rand));
-	char salt[3 + BASE64_SIZE(sizeof(rand))] = "$6$";
-	base64(&salt[3], rand, sizeof(rand));
-	printf("%s\n", crypt(pass, salt));
-}
-
-static void genKey(const char *path) {
-	const char *name = strrchr(path, '/');
-	name = (name ? &name[1] : path);
-	char subj[256];
-	snprintf(subj, sizeof(subj), "/CN=%.*s", (int)strcspn(name, "."), name);
-	execlp(
-		OPENSSL_BIN, "openssl", "req",
-		"-x509", "-new", "-newkey", "rsa:4096", "-sha256", "-days", "3650",
-		"-nodes", "-subj", subj, "-keyout", path,
-		NULL
-	);
-	err(EX_UNAVAILABLE, "openssl");
+static volatile sig_atomic_t signals[NSIG];
+static void signalHandler(int signal) {
+	signals[signal] = 1;
 }
-
-static void redir(int dst, int src) {
-	int fd = dup2(src, dst);
-	if (fd < 0) err(EX_OSERR, "dup2");
-	close(src);
+static void justExit(int signal) {
+	exit(128 + signal);
 }
 
-static void genCert(const char *path, const char *ca) {
-	int out = open(path, O_WRONLY | O_APPEND | O_CREAT, 0600);
-	if (out < 0) err(EX_CANTCREAT, "%s", path);
-
-	redir(STDOUT_FILENO, out);
-	if (!ca) {
-		genKey(path);
-		return;
-	}
-
-	int rw[2];
-	int error = pipe(rw);
-	if (error) err(EX_OSERR, "pipe");
+static struct {
+	struct pollfd *fds;
+	struct Client **clients;
+	size_t cap, len;
+} event;
 
-	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
-	if (!pid) {
-		close(rw[0]);
-		redir(STDOUT_FILENO, rw[1]);
-		genKey(path);
+static void eventAdd(int fd, struct Client *client) {
+	if (event.len == event.cap) {
+		event.cap = (event.cap ? event.cap * 2 : 8);
+		event.fds = realloc(event.fds, sizeof(*event.fds) * event.cap);
+		if (!event.fds) err(EX_OSERR, "realloc");
+		event.clients = realloc(
+			event.clients, sizeof(*event.clients) * event.cap
+		);
+		if (!event.clients) err(EX_OSERR, "realloc");
 	}
-
-	close(rw[1]);
-	redir(STDIN_FILENO, rw[0]);
-	execlp(
-		OPENSSL_BIN, "openssl", "x509",
-		"-CA", ca, "-CAcreateserial", "-days", "3650",
-		NULL
-	);
-	err(EX_UNAVAILABLE, "openssl");
+	event.fds[event.len] = (struct pollfd) { .fd = fd, .events = POLLIN };
+	event.clients[event.len] = client;
+	event.len++;
 }
 
-static size_t parseSize(const char *str) {
-	char *rest;
-	size_t size = strtoull(str, &rest, 0);
-	if (*rest) errx(EX_USAGE, "invalid size: %s", str);
-	return size;
+static void eventRemove(size_t i) {
+	close(event.fds[i].fd);
+	event.len--;
+	event.fds[i] = event.fds[event.len];
+	event.clients[i] = event.clients[event.len];
 }
 
 static FILE *saveFile;
@@ -122,8 +102,8 @@ static void saveSave(void) {
 
 static void saveLoad(const char *path) {
 	umask(0066);
-	saveFile = fopen(path, "a+");
-	if (!saveFile) err(EX_CANTCREAT, "%s", path);
+	saveFile = dataOpen(path, "a+");
+	if (!saveFile) exit(EX_CANTCREAT);
 
 	int error = flock(fileno(saveFile), LOCK_EX | LOCK_NB);
 	if (error && errno != EWOULDBLOCK) err(EX_OSERR, "flock");
@@ -137,113 +117,29 @@ static void saveLoad(const char *path) {
 	atexit(saveSave);
 }
 
-struct SplitPath {
-	int dir;
-	char *file;
-	int targetDir;
-};
-
-static bool linkTarget(char *target, size_t cap, int dir, const char *file) {
-	ssize_t len = readlinkat(dir, file, target, cap - 1);
-	if (len < 0 && errno == EINVAL) return false;
-	if (len < 0) err(EX_NOINPUT, "%s", file);
-	target[len] = '\0';
-	return true;
-}
-
-static struct SplitPath splitPath(char *path) {
-	struct SplitPath split = { .targetDir = -1 };
-	split.file = strrchr(path, '/');
-	if (split.file) {
-		*split.file++ = '\0';
-		split.dir = open(path, O_DIRECTORY);
-	} else {
-		split.file = path;
-		split.dir = open(".", O_DIRECTORY);
-	}
-	if (split.dir < 0) err(EX_NOINPUT, "%s", path);
-
-	// Capsicum workaround for certbot "live" symlinks to "../../archive".
-	char target[PATH_MAX];
-	if (!linkTarget(target, sizeof(target), split.dir, split.file)) {
-		return split;
-	}
-	char *file = strrchr(target, '/');
-	if (file) {
-		*file = '\0';
-		split.targetDir = openat(split.dir, target, O_DIRECTORY);
-		if (split.targetDir < 0) err(EX_NOINPUT, "%s", target);
-	}
-
-	return split;
-}
-
-static FILE *splitOpen(struct SplitPath split) {
-	if (split.targetDir >= 0) {
-		char target[PATH_MAX];
-		if (!linkTarget(target, sizeof(target), split.dir, split.file)) {
-			errx(EX_CONFIG, "file is no longer a symlink");
-		}
-		split.dir = split.targetDir;
-		split.file = strrchr(target, '/');
-		if (!split.file) {
-			errx(EX_CONFIG, "symlink no longer targets directory");
-		}
-		split.file++;
-	}
-
-	int fd = openat(split.dir, split.file, O_RDONLY);
-	if (fd < 0) err(EX_NOINPUT, "%s", split.file);
-	FILE *file = fdopen(fd, "r");
-	if (!file) err(EX_IOERR, "fdopen");
-	return file;
-}
-
-#ifdef __FreeBSD__
-static void capLimit(int fd, const cap_rights_t *rights) {
-	int error = cap_rights_limit(fd, rights);
-	if (error) err(EX_OSERR, "cap_rights_limit");
-}
-static void capLimitSplit(struct SplitPath split, const cap_rights_t *rights) {
-	capLimit(split.dir, rights);
-	if (split.targetDir >= 0) capLimit(split.targetDir, rights);
-}
-#endif
-
-static volatile sig_atomic_t signals[NSIG];
-static void signalHandler(int signal) {
-	signals[signal] = 1;
+static size_t parseSize(const char *str) {
+	char *rest;
+	size_t size = strtoull(str, &rest, 0);
+	if (*rest) errx(EX_USAGE, "invalid size: %s", str);
+	return size;
 }
 
-static struct {
-	struct pollfd *fds;
-	struct Client **clients;
-	size_t cap, len;
-} event;
-
-static void eventAdd(int fd, struct Client *client) {
-	if (event.len == event.cap) {
-		event.cap = (event.cap ? event.cap * 2 : 8);
-		event.fds = realloc(event.fds, sizeof(*event.fds) * event.cap);
-		if (!event.fds) err(EX_OSERR, "realloc");
-		event.clients = realloc(
-			event.clients, sizeof(*event.clients) * event.cap
-		);
-		if (!event.clients) err(EX_OSERR, "realloc");
-	}
-	event.fds[event.len] = (struct pollfd) { .fd = fd, .events = POLLIN };
-	event.clients[event.len] = client;
-	event.len++;
+static struct timeval parseInterval(const char *str) {
+	char *rest;
+	long ms = strtol(str, &rest, 0);
+	if (*rest) errx(EX_USAGE, "invalid interval: %s", str);
+	return (struct timeval) {
+		.tv_sec = ms / 1000,
+		.tv_usec = 1000 * (ms % 1000),
+	};
 }
 
-static void eventRemove(size_t i) {
-	close(event.fds[i].fd);
-	event.len--;
-	event.fds[i] = event.fds[event.len];
-	event.clients[i] = event.clients[event.len];
-}
+static void hashPass(void);
+static void genCert(const char *path, const char *ca);
 
 int main(int argc, char *argv[]) {
+	int error;
+
 	size_t ringSize = 4096;
 	const char *savePath = NULL;
 
@@ -256,6 +152,8 @@ int main(int argc, char *argv[]) {
 	const char *genPath = NULL;
 
 	bool insecure = false;
+	bool printCert = false;
+	const char *trust = NULL;
 	const char *clientCert = NULL;
 	const char *clientPriv = NULL;
 	const char *serverBindHost = NULL;
@@ -263,12 +161,13 @@ int main(int argc, char *argv[]) {
 	const char *host = NULL;
 	const char *port = "6697";
 	char *pass = NULL;
-	bool sasl = false;
 	char *plain = NULL;
+	enum Cap blindReq = 0;
 	const char *nick = NULL;
 	const char *user = NULL;
 	const char *real = NULL;
 
+	const char *mode = NULL;
 	const char *join = NULL;
 	const char *quit = "connection reset by purr";
 
@@ -278,8 +177,11 @@ int main(int argc, char *argv[]) {
 		{ .val = 'C', .name = "local-cert", required_argument },
 		{ .val = 'H', .name = "local-host", required_argument },
 		{ .val = 'K', .name = "local-priv", required_argument },
+		{ .val = 'L', .name = "palaver", no_argument },
 		{ .val = 'N', .name = "no-names", no_argument },
 		{ .val = 'P', .name = "local-port", required_argument },
+		{ .val = 'Q', .name = "queue-interval", required_argument },
+		{ .val = 'R', .name = "blind-req", required_argument },
 		{ .val = 'S', .name = "bind", required_argument },
 		{ .val = 'T', .name = "no-sts", no_argument },
 		{ .val = 'U', .name = "local-path", required_argument },
@@ -292,26 +194,19 @@ int main(int argc, char *argv[]) {
 		{ .val = 'h', .name = "host", required_argument },
 		{ .val = 'j', .name = "join", required_argument },
 		{ .val = 'k', .name = "client-priv", required_argument },
+		{ .val = 'm', .name = "mode", required_argument },
 		{ .val = 'n', .name = "nick", required_argument },
+		{ .val = 'o', .name = "print-cert", no_argument },
 		{ .val = 'p', .name = "port", required_argument },
 		{ .val = 'q', .name = "quit", required_argument },
 		{ .val = 'r', .name = "real", required_argument },
 		{ .val = 's', .name = "size", required_argument },
+		{ .val = 't', .name = "trust", required_argument },
 		{ .val = 'u', .name = "user", required_argument },
 		{ .val = 'v', .name = "verbose", no_argument },
 		{ .val = 'w', .name = "pass", required_argument },
 		{ .val = 'x', .name = "hash", no_argument },
 		{ .val = 'y', .name = "away", required_argument },
-
-		// Deprecated names:
-		{ .val = 'A', .name = "client-ca", required_argument },
-		{ .val = 'C', .name = "cert", required_argument },
-		{ .val = 'H', .name = "bind-host", required_argument },
-		{ .val = 'K', .name = "priv", required_argument },
-		{ .val = 'P', .name = "bind-port", required_argument },
-		{ .val = 'U', .name = "bind-path", required_argument },
-		{ .val = 'W', .name = "client-pass", required_argument },
-
 		{0},
 	};
 	char opts[2 * ARRAY_LEN(options)];
@@ -323,59 +218,61 @@ int main(int argc, char *argv[]) {
 	for (int opt; 0 < (opt = getopt_config(argc, argv, opts, options, NULL));) {
 		switch (opt) {
 			break; case '!': insecure = true;
-			break; case 'A': clientCA = true; caPath = optarg;
-			break; case 'C': strlcpy(certPath, optarg, sizeof(certPath));
+			break; case 'A': caPath = optarg; clientCaps |= CapSASL;
+			break; case 'C': snprintf(certPath, sizeof(certPath), "%s", optarg);
 			break; case 'H': bindHost = optarg;
-			break; case 'K': strlcpy(privPath, optarg, sizeof(privPath));
+			break; case 'K': snprintf(privPath, sizeof(privPath), "%s", optarg);
 			break; case 'N': stateNoNames = true;
+			break; case 'L': clientCaps |= CapPalaverApp;
 			break; case 'P': bindPort = optarg;
+			break; case 'Q': serverQueueInterval = parseInterval(optarg);
+			break; case 'R': blindReq |= capParse(optarg, NULL);
 			break; case 'S': serverBindHost = optarg;
-			break; case 'T': clientSTS = false;
-			break; case 'U': strlcpy(bindPath, optarg, sizeof(bindPath));
+			break; case 'T': clientCaps &= ~CapSTS;
+			break; case 'U': snprintf(bindPath, sizeof(bindPath), "%s", optarg);
 			break; case 'W': clientPass = optarg;
-			break; case 'a': sasl = true; plain = optarg;
+			break; case 'a': blindReq |= CapSASL; plain = optarg;
 			break; case 'c': clientCert = optarg;
-			break; case 'e': sasl = true;
+			break; case 'e': blindReq |= CapSASL;
 			break; case 'f': savePath = optarg;
 			break; case 'g': genPath = optarg;
 			break; case 'h': host = optarg;
 			break; case 'j': join = optarg;
 			break; case 'k': clientPriv = optarg;
+			break; case 'm': mode = optarg;
 			break; case 'n': nick = optarg;
+			break; case 'o': printCert = true;
 			break; case 'p': port = optarg;
 			break; case 'q': quit = optarg;
 			break; case 'r': real = optarg;
 			break; case 's': ringSize = parseSize(optarg);
+			break; case 't': trust = optarg;
 			break; case 'u': user = optarg;
-			break; case 'v': verbose = true;
+			break; case 'v': verbose = true; setlinebuf(stdout);
 			break; case 'w': pass = optarg;
 			break; case 'x': hashPass(); return EX_OK;
 			break; case 'y': clientAway = optarg;
 			break; default:  return EX_USAGE;
 		}
 	}
+	if (blindReq & CapUnsupported) errx(EX_USAGE, "unsupported capability");
 	if (genPath) genCert(genPath, caPath);
 
 	if (bindPath[0]) {
 		struct stat st;
 		int error = stat(bindPath, &st);
-		if (error && errno != ENOENT) err(EX_CANTCREAT, "%s", bindPath);
-		if (S_ISDIR(st.st_mode)) {
-			strlcat(bindPath, "/", sizeof(bindPath));
-			strlcat(bindPath, bindHost, sizeof(bindPath));
+		if (error) {
+			if (errno != ENOENT) err(EX_CANTCREAT, "%s", bindPath);
+		} else if (S_ISDIR(st.st_mode)) {
+			size_t len = strlen(bindPath);
+			snprintf(&bindPath[len], sizeof(bindPath) - len, "/%s", bindHost);
 		}
 	}
 	if (!certPath[0]) {
-		snprintf(
-			certPath, sizeof(certPath), CERTBOT_PATH "/live/%s/fullchain.pem",
-			bindHost
-		);
+		snprintf(certPath, sizeof(certPath), "%s.pem", bindHost);
 	}
 	if (!privPath[0]) {
-		snprintf(
-			privPath, sizeof(privPath), CERTBOT_PATH "/live/%s/privkey.pem",
-			bindHost
-		);
+		snprintf(privPath, sizeof(privPath), "%s.key", bindHost);
 	}
 
 	if (!host) errx(EX_USAGE, "host required");
@@ -387,149 +284,270 @@ int main(int argc, char *argv[]) {
 	if (!real) real = nick;
 	if (!clientAway) clientAway = "pounced :3";
 	if (clientPass && clientPass[0] != '$') {
-		errx(EX_CONFIG, "password must be hashed with -x");
+		errx(EX_USAGE, "password must be hashed with -x");
+	}
+	if (strchr(bindHost, '.')) {
+		clientOrigin = strdup(bindHost);
+		if (!clientOrigin) err(EX_OSERR, "strdup");
+	} else {
+		int n = asprintf(&clientOrigin, "%s.", bindHost);
+		if (n < 0) err(EX_OSERR, "asprintf");
 	}
 
+	if (printCert) {
+#ifdef __OpenBSD__
+		error = pledge("stdio inet dns", NULL);
+		if (error) err(EX_OSERR, "pledge");
+#endif
+		serverConfig(true, NULL, NULL, NULL);
+		serverConnect(serverBindHost, host, port);
+		serverPrintCert();
+		serverClose();
+		return EX_OK;
+	}
+
+	// Either exit with cleanup or ignore signals until entering the main loop.
+	signal(SIGINT, justExit);
+	signal(SIGTERM, justExit);
+	signal(SIGINFO, SIG_IGN);
+	signal(SIGUSR1, SIG_IGN);
+
 	ringAlloc(ringSize);
 	if (savePath) saveLoad(savePath);
+	serverConfig(insecure, trust, clientCert, clientPriv);
+
+#ifdef __OpenBSD__
+	char buf[PATH_MAX];
+	const char *paths[] = { certPath, privPath, caPath };
+	for (size_t i = 0; i < ARRAY_LEN(paths); ++i) {
+		if (!paths[i]) continue;
+		for (int j = 0; configPath(buf, sizeof(buf), paths[i], j); ++j) {
+			error = unveil(buf, "r");
+			if (error && errno != ENOENT) err(EX_NOINPUT, "%s", buf);
+		}
+	}
+	error = unveil(tls_default_ca_cert_file(), "r");
+	if (error) err(EX_OSFILE, "%s", tls_default_ca_cert_file());
 
-	FILE *localCA = NULL;
-	if (caPath) {
-		localCA = fopen(caPath, "r");
-		if (!localCA) err(EX_NOINPUT, "%s", caPath);
+	if (bindPath[0]) {
+		error = unveil(bindPath, "c");
+		if (error) err(EX_NOINPUT, "%s", bindPath);
+		error = pledge("stdio rpath inet dns cpath unix recvfd", NULL);
+	} else {
+		error = pledge("stdio rpath inet dns", NULL);
 	}
+	if (error) err(EX_OSERR, "pledge");
+#endif
 
-	struct SplitPath certSplit = splitPath(certPath);
-	struct SplitPath privSplit = splitPath(privPath);
-	FILE *cert = splitOpen(certSplit);
-	FILE *priv = splitOpen(privSplit);
-	localConfig(cert, priv, localCA, !clientPass);
-	fclose(cert);
-	fclose(priv);
+	error = localConfig(certPath, privPath, caPath, !clientPass);
+	if (error) return EX_NOINPUT;
 
 	int bind[8];
 	size_t binds = bindPath[0]
 		? localUnix(bind, ARRAY_LEN(bind), bindPath)
 		: localBind(bind, ARRAY_LEN(bind), bindHost, bindPort);
-
-	serverConfig(insecure, clientCert, clientPriv);
 	int server = serverConnect(serverBindHost, host, port);
 
-#ifdef __FreeBSD__
-	int error = cap_enter();
-	if (error) err(EX_OSERR, "cap_enter");
-
-	cap_rights_t saveRights, fileRights, sockRights, bindRights;
-	cap_rights_init(&saveRights, CAP_WRITE);
-	cap_rights_init(&fileRights, CAP_FCNTL, CAP_FSTAT, CAP_LOOKUP, CAP_PREAD);
-	cap_rights_init(&sockRights, CAP_EVENT, CAP_RECV, CAP_SEND, CAP_SETSOCKOPT);
-	cap_rights_init(&bindRights, CAP_LISTEN, CAP_ACCEPT);
-	cap_rights_merge(&bindRights, &sockRights);
-
-	if (saveFile) capLimit(fileno(saveFile), &saveRights);
-	if (localCA) capLimit(fileno(localCA), &fileRights);
-	capLimitSplit(certSplit, &fileRights);
-	capLimitSplit(privSplit, &fileRights);
-	for (size_t i = 0; i < binds; ++i) {
-		capLimit(bind[i], &bindRights);
+#ifdef __OpenBSD__
+	if (bindPath[0]) {
+		error = pledge("stdio rpath cpath unix recvfd", NULL);
+	} else {
+		error = pledge("stdio rpath inet", NULL);
 	}
-	capLimit(server, &sockRights);
+	if (error) err(EX_OSERR, "pledge");
 #endif
 
-	stateLogin(pass, sasl, plain, nick, user, real);
+	stateLogin(pass, blindReq, plain, nick, user, real);
 	if (pass) explicit_bzero(pass, strlen(pass));
 	if (plain) explicit_bzero(plain, strlen(plain));
 
 	while (!stateReady()) serverRecv();
 	serverFormat("AWAY :%s\r\n", clientAway);
-	if (join) serverFormat("JOIN :%s\r\n", join);
+	if (mode) serverFormat("MODE %s %s\r\n", stateNick(), mode);
+	if (join) serverFormat("JOIN %s\r\n", join);
 
 	signal(SIGINT, signalHandler);
 	signal(SIGTERM, signalHandler);
 	signal(SIGPIPE, SIG_IGN);
+	signal(SIGALRM, signalHandler);
 	signal(SIGINFO, signalHandler);
 	signal(SIGUSR1, signalHandler);
 
 	for (size_t i = 0; i < binds; ++i) {
-		int error = listen(bind[i], 1);
+		error = listen(bind[i], -1);
 		if (error) err(EX_IOERR, "listen");
 		eventAdd(bind[i], NULL);
 	}
 	eventAdd(server, NULL);
+	size_t clientIndex = event.len;
 
+	enum {
+		NeedTime = 10,
+		IdleTime = 15 * 60,
+	};
 	for (;;) {
-		int nfds = poll(event.fds, event.len, -1);
-		if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll");
-		if (signals[SIGINT] || signals[SIGTERM]) break;
-
-		if (signals[SIGINFO]) {
-			ringInfo();
-			signals[SIGINFO] = 0;
+		enum Need needs = 0;
+		time_t now = time(NULL);
+		for (size_t i = clientIndex; i < event.len; ++i) {
+			struct Client *client = event.clients[i];
+			event.fds[i].events = POLLIN;
+			needs |= client->need;
+			if (client->need) continue;
+			if (ringDiff(client->consumer) || now - client->idle >= IdleTime) {
+				event.fds[i].events |= POLLOUT;
+			}
 		}
 
-		if (signals[SIGUSR1]) {
-			cert = splitOpen(certSplit);
-			priv = splitOpen(privSplit);
-			localConfig(cert, priv, localCA, !clientPass);
-			fclose(cert);
-			fclose(priv);
-			signals[SIGUSR1] = 0;
+		int ready = poll(event.fds, event.len, (needs ? NeedTime * 1000 : -1));
+		if (ready < 0 && errno != EINTR) err(EX_IOERR, "poll");
+
+		if (needs) {
+			time_t now = time(NULL);
+			for (size_t i = event.len - 1; i >= clientIndex; --i) {
+				struct Client *client = event.clients[i];
+				if (!client->need) continue;
+				if (now - client->time < NeedTime) continue;
+				clientFree(client);
+				eventRemove(i);
+			}
 		}
 
-		if (nfds < 0) continue;
-		for (size_t i = event.len - 1; i < event.len; --i) {
+		for (size_t i = event.len - 1; ready > 0 && i < event.len; --i) {
 			short revents = event.fds[i].revents;
 			if (!revents) continue;
 
-			if (event.fds[i].fd == server) {
+			struct Client *client = event.clients[i];
+			if (client) {
+				if (revents & POLLOUT) {
+					clientConsume(client);
+					if (now - client->idle >= IdleTime) {
+						clientFormat(client, "PING :%s\r\n", clientOrigin);
+					}
+				}
+				if (revents & POLLIN) clientRecv(client);
+				if (client->remove || revents & (POLLHUP | POLLERR)) {
+					clientFree(client);
+					eventRemove(i);
+				}
+			} else if (event.fds[i].fd == server) {
 				serverRecv();
-				continue;
-			}
-
-			if (!event.clients[i]) {
-				int fd;
-				struct tls *tls = localAccept(&fd, event.fds[i].fd);
-				if (!tls) {
+			} else {
+				struct tls *tls = NULL;
+				int sock = localAccept(&tls, event.fds[i].fd);
+				if (sock < 0) {
 					warn("accept");
 					continue;
 				}
-
-				int error = tls_handshake(tls);
-				if (error) {
-					warnx("tls_handshake: %s", tls_error(tls));
-					tls_free(tls);
-					close(fd);
-				} else {
-					eventAdd(fd, clientAlloc(tls));
-				}
-				continue;
-			}
-
-			struct Client *client = event.clients[i];
-			if (revents & POLLOUT) clientConsume(client);
-			if (revents & POLLIN) clientRecv(client);
-			if (clientError(client) || revents & (POLLHUP | POLLERR)) {
-				clientFree(client);
-				eventRemove(i);
+				eventAdd(sock, clientAlloc(sock, tls));
 			}
 		}
 
-		for (size_t i = binds + 1; i < event.len; ++i) {
-			assert(event.clients[i]);
-			if (clientDiff(event.clients[i])) {
-				event.fds[i].events |= POLLOUT;
-			} else {
-				event.fds[i].events &= ~POLLOUT;
-			}
+		if (clientQuit || signals[SIGINT] || signals[SIGTERM]) {
+			break;
+		}
+		if (signals[SIGALRM]) {
+			signals[SIGALRM] = 0;
+			serverDequeue();
+		}
+		if (signals[SIGINFO]) {
+			signals[SIGINFO] = 0;
+			ringInfo();
+		}
+		if (signals[SIGUSR1]) {
+			signals[SIGUSR1] = 0;
+			localConfig(certPath, privPath, caPath, !clientPass);
 		}
 	}
 
+	if (clientQuit && clientQuit[0]) quit = clientQuit;
 	serverFormat("QUIT :%s\r\n", quit);
-	for (size_t i = binds + 1; i < event.len; ++i) {
-		assert(event.clients[i]);
-		clientFormat(event.clients[i], ":%s QUIT :%s\r\n", stateEcho(), quit);
-		clientFormat(event.clients[i], "ERROR :Disconnecting\r\n");
-		clientFree(event.clients[i]);
-		close(event.fds[i].fd);
+	serverClose();
+	for (size_t i = clientIndex; i < event.len; ++i) {
+		struct Client *client = event.clients[i];
+		if (!client->need) {
+			clientFormat(client, ":%s QUIT :%s\r\n", stateEcho(), quit);
+			clientFormat(client, "ERROR :Disconnecting\r\n");
+		}
+		clientFree(client);
 	}
+	if (bindPath[0]) unlink(bindPath);
+}
+
+#ifdef __OpenBSD__
+static void hashPass(void) {
+	int error = pledge("stdio tty", NULL);
+	if (error) err(EX_OSERR, "pledge");
+	char hash[_PASSWORD_LEN];
+	char *pass = getpass("Password: ");
+	error = crypt_newhash(pass, "bcrypt,a", hash, sizeof(hash));
+	if (error) err(EX_OSERR, "crypt_newhash");
+	printf("%s\n", hash);
+}
+#else
+static void hashPass(void) {
+	byte rand[12];
+	FILE *file = fopen("/dev/urandom", "r");
+	if (!file) err(EX_OSFILE, "/dev/urandom");
+	size_t n = fread(rand, sizeof(rand), 1, file);
+	if (!n) err(EX_IOERR, "/dev/urandom");
+	fclose(file);
+	char salt[3 + BASE64_SIZE(sizeof(rand))] = "$6$";
+	base64(&salt[3], rand, sizeof(rand));
+	char *pass = getpass("Password: ");
+	printf("%s\n", crypt(pass, salt));
+}
+#endif
+
+static void genReq(const char *path) {
+	const char *name = strrchr(path, '/');
+	name = (name ? &name[1] : path);
+	char subj[256];
+	snprintf(subj, sizeof(subj), "/CN=%.*s", (int)strcspn(name, "."), name);
+	execlp(
+		OPENSSL_BIN, "openssl", "req",
+		"-new", "-newkey", "rsa:4096", "-sha256", "-nodes",
+		"-subj", subj, "-keyout", path,
+		NULL
+	);
+	err(EX_UNAVAILABLE, "openssl");
+}
+
+static void redir(int dst, int src) {
+	int fd = dup2(src, dst);
+	if (fd < 0) err(EX_OSERR, "dup2");
+	close(src);
+}
+
+static void genCert(const char *path, const char *ca) {
+	int out = open(path, O_WRONLY | O_APPEND | O_CREAT, 0600);
+	if (out < 0) err(EX_CANTCREAT, "%s", path);
+
+	int error;
+#ifdef __OpenBSD__
+	error = pledge("stdio proc exec", NULL);
+	if (error) err(EX_OSERR, "pledge");
+#endif
+
+	int rw[2];
+	error = pipe(rw);
+	if (error) err(EX_OSERR, "pipe");
+
+	pid_t pid = fork();
+	if (pid < 0) err(EX_OSERR, "fork");
+	if (!pid) {
+		close(rw[0]);
+		redir(STDOUT_FILENO, rw[1]);
+		genReq(path);
+	}
+
+	close(rw[1]);
+	redir(STDIN_FILENO, rw[0]);
+	redir(STDOUT_FILENO, out);
+	execlp(
+		OPENSSL_BIN, "openssl", "x509",
+		"-req", "-days", "3650", "-CAcreateserial",
+		(ca ? "-CA" : "-signkey"), (ca ? ca : path),
+		NULL
+	);
+	err(EX_UNAVAILABLE, "openssl");
 }