diff options
-rw-r--r-- | .gitignore | 10 | ||||
-rw-r--r-- | Makefile | 89 | ||||
-rw-r--r-- | QUIRKS.7 | 66 | ||||
-rw-r--r-- | README.7 | 156 | ||||
-rw-r--r-- | bounce.c | 592 | ||||
-rw-r--r-- | bounce.h | 123 | ||||
-rw-r--r-- | calico.1 | 97 | ||||
-rw-r--r-- | client.c | 531 | ||||
-rw-r--r-- | compat.h | 37 | ||||
-rw-r--r-- | config.c | 36 | ||||
-rwxr-xr-x | configure | 85 | ||||
-rw-r--r-- | dispatch.c | 207 | ||||
-rw-r--r-- | local.c | 160 | ||||
-rw-r--r-- | notify.c | 353 | ||||
-rw-r--r-- | palaver.c | 796 | ||||
-rw-r--r-- | pounce-notify.1 | 115 | ||||
-rw-r--r-- | pounce-palaver.1 | 112 | ||||
-rw-r--r-- | pounce.1 | 785 | ||||
-rw-r--r-- | rc.d/calico | 30 | ||||
-rw-r--r-- | rc.d/pounce | 63 | ||||
-rw-r--r-- | ring.c | 39 | ||||
-rw-r--r-- | server.c | 124 | ||||
-rw-r--r-- | state.c | 149 | ||||
-rw-r--r-- | xdg.c | 131 |
24 files changed, 3537 insertions, 1349 deletions
diff --git a/.gitignore b/.gitignore index c7ce8fe..5249f84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.conf *.o -/calico -/pounce +calico config.mk -localhost.crt -localhost.key +localhost.pem +pounce +pounce-edit +pounce-notify +pounce-palaver tags diff --git a/Makefile b/Makefile index 597534e..c7c1ef7 100644 --- a/Makefile +++ b/Makefile @@ -1,61 +1,70 @@ -PREFIX = /usr/local -MANDIR = ${PREFIX}/share/man -ETCDIR = ${PREFIX}/etc +PREFIX ?= /usr/local +BINDIR ?= ${PREFIX}/bin +MANDIR ?= ${PREFIX}/man CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -LDLIBS = -lcrypt -lcrypto -ltls +LDADD.crypt = -lcrypt +LDADD.libcurl = -lcurl +LDADD.libtls = -ltls +LDADD.sqlite3 = -lsqlite3 BINS = calico pounce MANS = ${BINS:=.1} -RCS = ${BINS:%=rc.d/%} -DIRS = ${ETCDIR}/pounce /var/run/calico -include config.mk -OBJS += bounce.o -OBJS += client.o -OBJS += config.o -OBJS += local.o -OBJS += ring.o -OBJS += server.o -OBJS += state.o +LDLIBS.calico = +LDLIBS.pounce = ${LDADD.crypt} ${LDADD.libtls} +LDLIBS.pounce-notify = ${LDADD.libtls} +LDLIBS.pounce-palaver = ${LDADD.libcurl} ${LDADD.libtls} ${LDADD.sqlite3} + +OBJS.calico += dispatch.o + +OBJS.pounce += bounce.o +OBJS.pounce += client.o +OBJS.pounce += config.o +OBJS.pounce += local.o +OBJS.pounce += ring.o +OBJS.pounce += server.o +OBJS.pounce += state.o +OBJS.pounce += xdg.o + +OBJS.pounce-notify = notify.o +OBJS.pounce-palaver = palaver.o xdg.o + +OBJS += ${OBJS.calico} +OBJS += ${OBJS.pounce} +OBJS += ${OBJS.pounce-notify} +OBJS += ${OBJS.pounce-palaver} dev: tags all all: ${BINS} -calico: dispatch.o - ${CC} ${LDFLAGS} dispatch.o ${LDLIBS_calico} -o $@ - -pounce: ${OBJS} - ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ +calico: ${OBJS.calico} +pounce: ${OBJS.pounce} +pounce-notify: ${OBJS.pounce-notify} +pounce-palaver: ${OBJS.pounce-palaver} -${OBJS}: bounce.h compat.h +${BINS}: + ${CC} ${LDFLAGS} ${OBJS.$@} ${LDLIBS.$@} -o $@ -dispatch.o: compat.h +${OBJS.pounce}: bounce.h -tags: *.c *.h - ctags -w *.c *.h +tags: *.[ch] + ctags -w *.[ch] clean: - rm -f tags ${BINS} ${OBJS} dispatch.o + rm -f ${BINS} ${OBJS} tags -install: ${BINS} ${MANS} ${RCS} - install -d ${PREFIX}/bin ${MANDIR}/man1 - install ${BINS} ${PREFIX}/bin - install -m 644 ${MANS} ${MANDIR}/man1 - if [ -n '${RCS}' ]; then install -d ${ETCDIR}/rc.d; fi - if [ -n '${RCS}' ]; then install ${RCS} ${ETCDIR}/rc.d; fi - if [ -n '${DIRS}' ]; then install -d ${DIRS}; fi +install: ${BINS} ${MANS} + install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 + install ${BINS} ${DESTDIR}${BINDIR} + install -m 644 ${MANS} ${DESTDIR}${MANDIR}/man1 uninstall: - rm -f ${BINS:%=${PREFIX}/bin/%} - rm -f ${MANS:%=${MANDIR}/man1/%} - if [ -n '${RCS}' ]; then rm -f ${RCS:%=${ETCDIR}/%}; fi - if [ -n '${DIRS}' ]; then rmdir ${DIRS}; fi - -localhost.crt: - printf "[dn]\nCN=localhost\n[req]\ndistinguished_name=dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth" \ - | openssl req -x509 -out localhost.crt -keyout localhost.key \ - -newkey rsa:2048 -nodes -sha256 \ - -subj '/CN=localhost' -extensions EXT -config /dev/fd/0 + rm -f ${BINS:%=${DESTDIR}${BINDIR}/%} + rm -f ${MANS:%=${DESTDIR}${MANDIR}/man1/%} + +localhost.pem: pounce + ./pounce -g $@ diff --git a/QUIRKS.7 b/QUIRKS.7 new file mode 100644 index 0000000..f6e3aca --- /dev/null +++ b/QUIRKS.7 @@ -0,0 +1,66 @@ +.Dd October 15, 2021 +.Dt QUIRKS 7 +.Os "Causal Agency" +. +.Sh NAME +.Nm Quirks +.Nd IRC networks and clients +. +.Sh DESCRIPTION +This file documents the quirks +of connecting +.Xr pounce 1 +with particular IRC networks and clients. +. +.Ss Networks +.Bl -tag -width Ds +.It EFnet +EFnet uses port 9999 for TLS +and uses self-signed certificates. +. +.It EsperNet +EsperNet supports SASL, +but not SASL EXTERNAL. +Either the +.Cm sasl-plain +option can be used, +or the +.Cm client-cert +option without the +.Cm sasl-external +option. +. +.It Libera.Chat +Libera.Chat requires SASL authentication +for some IP address ranges. +They recommend +.Lk https://libera.chat/guides/sasl#sasl-access-only-ip-ranges "using their webchat to register" . +It is possible to enable +.Sy userhost-in-names +with +.Cm blind-req . +. +.It OFTC +OFTC does not support SASL, +but does support CertFP. +The +.Cm client-cert +option can be used without the +.Cm sasl-external +option. +.El +. +.Ss Clients +.Bl -tag -width Ds +.It Palaver (iOS) +Palaver sets its username +to the same as its nickname. +The default nick of +.Dq Palaver +therefore works well. +. +.It Revolution (Android) +Revolution won't connect properly +if the nick it is configured with +is not the one pounce is currently using. +.El diff --git a/README.7 b/README.7 index 225a7a7..b15a8e2 100644 --- a/README.7 +++ b/README.7 @@ -1,4 +1,4 @@ -.Dd March 1, 2020 +.Dd October 21, 2023 .Dt README 7 .Os "Causal Agency" . @@ -9,11 +9,38 @@ .Sh DESCRIPTION .Xr pounce 1 is a multi-client, TLS-only IRC bouncer. -It takes a simple approach, -using a multiple-consumer ring buffer -and the IRCv3.2 +It maintains a persistent connection +to an IRC server, +acting as a proxy and buffer +for a number of clients. +When a client connects, +any messages received +since it last disconnected +will be relayed to it. +Unlike some other bouncers, +.Nm +uses a single buffer +for all IRC messages, +which acts as a queue +from which each client +reads messages independently. +. +.Pp +.Nm +speaks regular modern IRC +to both servers and clients, +using the .Sy server-time -extension to communicate with clients. +extension to indicate +when messages originally occurred. +Clients identify themselves to +.Nm +by their IRC usernames. +See +.Xr QUIRKS 7 +for notes on connecting +.Nm +with particular networks and clients. . .Sh RATIONALE As a former @@ -42,35 +69,95 @@ rather than being limited to messages. . .Sh INSTALLING .Nm -requires LibreSSL -.Pq Fl ltls -and primarily targets -.Fx , -as well as macOS and Linux. -On -.Fx , -processes are sandboxed with -.Xr capsicum 4 . -On other systems, -who knows what might happen? +requires +.Sy libtls , +provided by either +.Lk https://git.causal.agency/libretls/about LibreTLS +(for OpenSSL) +or by LibreSSL. +.Nm +and +.Sy libtls +may be packaged for your system. +Check the Repology pages for +.Lk https://repology.org/project/pounce/versions pounce +and +.Lk https://repology.org/project/libretls/versions libretls . . +.Pp +.Nm +primarily targets +.Ox , +where it is sandboxed with +.Xr pledge 2 +and +.Xr unveil 2 . +.Fx , +Linux and macOS +are also supported. .Bd -literal -offset indent \&./configure make all -sudo make install PREFIX=/usr/local +sudo make install .Ed . .Pp -If your system installs LibreSSL -in a non-standard path, set +If installing +.Sy libtls +manually to +.Pa /usr/local , +for example, +make sure +.Pa /usr/local/lib +appears in +.Pa /etc/ld.so.conf +or +.Pa /etc/ld.so.conf.d/* +and be sure to run +.Xr ldconfig 8 +once the library is installed. +Set .Ev PKG_CONFIG_PATH for -.Nm ./configure . -For example, +.Nm ./configure +to find it. .Bd -literal -offset indent -PKG_CONFIG_PATH=/opt/libressl/lib/pkgconfig ./configure +PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure .Ed . +.Pp +On +.Ox +the recommended way to run +.Nm +is with the process supervisor +.Lk https://git.causal.agency/kitd kitd . +. +.Ss Additional Components +Additional functionality can be provided +by special-purpose clients connected to +.Nm . +.Bl -inset +.It Lk https://git.causal.agency/litterbox litterbox +provides logging and search. +.It Xr pounce-notify 1 +provides notifications +by running an external command. +Configure with +.Fl \-enable-notify +to build. +.It Xr pounce-palaver 1 +provides push notifications +for the Palaver IRC app. +Configure with +.Fl \-enable-palaver +to build. +Requires +.Sy libcurl +and +.Sy libsqlite3 . +.El +. .Sh FILES .Bl -tag -width "dispatch.c" -compact .It Pa bounce.h @@ -90,23 +177,25 @@ buffer between server and clients .It Pa config.c .Xr getopt_long 3 Ns -integrated configuration parsing +.It Pa xdg.c +XDG base directories .It Pa dispatch.c SNI socket dispatch -.It Pa compat.h -compatibility with lesser operating systems -.It Pa rc.d/ -.Fx -.Xr rc 8 -scripts .El . .Sh CONTRIBUTING The upstream URL of this project is .Aq Lk https://git.causal.agency/pounce . -I'm happy to receive contributions in any form at -.Aq Mt june@causal.agency . +Contributions in any form can be sent to +.Aq Mt list+pounce@causal.agency . For sending patches by email, see .Aq Lk https://git-send-email.io . +Mailing list archives are available at +.Aq Lk https://causal.agency/list/pounce.html . +. +.Pp +Monetary contributions can be +.Lk https://liberapay.com/june/donate "donated via Liberapay" . . .Sh SEE ALSO .Xr calico 1 , @@ -115,3 +204,10 @@ For sending patches by email, see .Pp Central logging with full-text search: .Lk https://git.causal.agency/litterbox "litterbox" +. +.Rs +.%A June McEnroe +.%T IRC Suite +.%U https://text.causal.agency/010-irc-suite.txt +.%D June 19, 2020 +.Re 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"); } diff --git a/bounce.h b/bounce.h index a5dc836..a7bad16 100644 --- a/bounce.h +++ b/bounce.h @@ -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,33 +12,59 @@ * * 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 <err.h> +#include <limits.h> +#include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/time.h> +#include <sysexits.h> #include <tls.h> -#include "compat.h" - -#ifndef CERTBOT_PATH -#define CERTBOT_PATH "/usr/local/etc/letsencrypt" -#endif - #ifndef OPENSSL_BIN #define OPENSSL_BIN "openssl" #endif #define SOURCE_URL "https://git.causal.agency/pounce" -#define ORIGIN "irc.invalid" #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) typedef unsigned char byte; +static inline char *seprintf(char *ptr, char *end, const char *fmt, ...) + __attribute__((format(printf, 3, 4))); +static inline char *seprintf(char *ptr, char *end, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(ptr, end - ptr, fmt, ap); + va_end(ap); + if (n < 0) return NULL; + if (n > end - ptr) return end; + return ptr + n; +} + +static inline void set(char **field, const char *value) { + if (*field) free(*field); + *field = strdup(value); + if (!*field) err(EX_OSERR, "strdup"); +} + enum { MessageCap = 8191 + 512 }; enum { ParamCap = 15 }; @@ -51,9 +77,10 @@ struct Message { static inline struct Message parse(char *line) { struct Message msg = {0}; - if (line[0] == '@') msg.tags = 1 + strsep(&line, " "); - if (line[0] == ':') msg.origin = 1 + strsep(&line, " "); + if (line && line[0] == '@') msg.tags = 1 + strsep(&line, " "); + if (line && line[0] == ':') msg.origin = 1 + strsep(&line, " "); msg.cmd = strsep(&line, " "); + if (msg.cmd && !msg.cmd[0]) msg.cmd = NULL; for (size_t i = 0; line && i < ParamCap; ++i) { if (line[0] == ':') { msg.params[i] = &line[1]; @@ -73,16 +100,21 @@ static inline struct Message parse(char *line) { X("causal.agency/consumer", CapConsumer) \ X("causal.agency/passive", CapPassive) \ X("chghost", CapChghost) \ + X("draft/read-marker", CapReadMarker) \ + X("echo-message", CapEchoMessage) \ X("extended-join", CapExtendedJoin) \ + X("extended-monitor", CapExtendedMonitor) \ X("invite-notify", CapInviteNotify) \ X("labeled-response", CapLabeledResponse) \ X("message-tags", CapMessageTags) \ X("multi-prefix", CapMultiPrefix) \ + X("palaverapp.com", CapPalaverApp) \ X("sasl", CapSASL) \ X("server-time", CapServerTime) \ X("setname", CapSetname) \ X("sts", CapSTS) \ X("userhost-in-names", CapUserhostInNames) \ + X("znc.in/self-message", CapSelfMessage) \ X("", CapUnsupported) enum Cap { @@ -127,13 +159,14 @@ static inline enum Cap capParse(const char *list, const char *values[CapBits]) { static inline const char *capList(enum Cap caps, const char *values[CapBits]) { static char buf[1024]; buf[0] = '\0'; + char *ptr = buf, *end = &buf[sizeof(buf)]; for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { if (caps & (1 << i)) { - if (buf[0]) strlcat(buf, " ", sizeof(buf)); - strlcat(buf, CapNames[i], sizeof(buf)); + ptr = seprintf( + ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i] + ); if (values && values[i]) { - strlcat(buf, "=", sizeof(buf)); - strlcat(buf, values[i], sizeof(buf)); + ptr = seprintf(ptr, end, "=%s", values[i]); } } } @@ -141,6 +174,13 @@ static inline const char *capList(enum Cap caps, const char *values[CapBits]) { } extern bool verbose; +static inline void +verboseLog(const char *prefix, const char *line, size_t len) { + if (!verbose) return; + if (len && line[len - 1] == '\n') len--; + if (len && line[len - 1] == '\r') len--; + printf("%s %.*s\n", prefix, (int)len, line); +} void ringAlloc(size_t len); void ringProduce(const char *line); @@ -154,36 +194,68 @@ void ringInfo(void); int ringSave(FILE *file); void ringLoad(FILE *file); -void localConfig(FILE *cert, FILE *priv, FILE *ca, bool require); +int localConfig( + const char *cert, const char *priv, const char *ca, bool require +); size_t localBind(int fds[], size_t cap, const char *host, const char *port); size_t localUnix(int fds[], size_t cap, const char *path); -struct tls *localAccept(int *fd, int bind); +int localAccept(struct tls **tls, int bind); -void serverConfig(bool insecure, const char *cert, const char *priv); +extern struct timeval serverQueueInterval; +void serverConfig( + bool insecure, const char *trust, const char *cert, const char *priv +); int serverConnect(const char *bindHost, const char *host, const char *port); +void serverPrintCert(void); void serverRecv(void); void serverSend(const char *ptr, size_t len); void serverFormat(const char *format, ...) __attribute__((format(printf, 1, 2))); +void serverEnqueue(const char *format, ...) + __attribute__((format(printf, 1, 2))); +void serverDequeue(void); +void serverClose(void); -extern bool clientCA; -extern bool clientSTS; +enum Need { + BIT(NeedHandshake), + BIT(NeedNick), + BIT(NeedUser), + BIT(NeedPass), + BIT(NeedCapEnd), +}; +struct Client { + bool remove; + int sock; + struct tls *tls; + time_t time; + time_t idle; + enum Need need; + enum Cap caps; + size_t consumer; + size_t setPos; + char buf[MessageCap]; + size_t len; +}; +extern enum Cap clientCaps; +extern char *clientOrigin; extern char *clientPass; extern char *clientAway; -struct Client *clientAlloc(struct tls *tls); +extern char *clientQuit; +struct Client *clientAlloc(int sock, struct tls *tls); void clientFree(struct Client *client); -bool clientError(const struct Client *client); void clientRecv(struct Client *client); void clientSend(struct Client *client, const char *ptr, size_t len); void clientFormat(struct Client *client, const char *format, ...) __attribute__((format(printf, 2, 3))); -size_t clientDiff(const struct Client *client); void clientConsume(struct Client *client); +void clientGetMarker(struct Client *client, const char *target); extern bool stateNoNames; extern enum Cap stateCaps; +extern char *stateAccount; +extern bool stateAway; void stateLogin( - const char *pass, bool sasl, const char *plain, + const char *pass, enum Cap blind, const char *plain, const char *nick, const char *user, const char *real ); bool stateReady(void); @@ -192,6 +264,11 @@ void stateSync(struct Client *client); const char *stateNick(void); const char *stateEcho(void); +char *configPath(char *buf, size_t cap, const char *path, int i); +char *dataPath(char *buf, size_t cap, const char *path, int i); +FILE *configOpen(const char *path, const char *mode); +FILE *dataOpen(const char *path, const char *mode); + struct option; int getopt_config( int argc, char *const *argv, diff --git a/calico.1 b/calico.1 index 4d52a10..37e61a9 100644 --- a/calico.1 +++ b/calico.1 @@ -1,4 +1,4 @@ -.Dd November 6, 2019 +.Dd August 27, 2020 .Dt CALICO 1 .Os . @@ -16,7 +16,7 @@ .Sh DESCRIPTION The .Nm -daemon +program dispatches incoming TLS connections to instances of .Xr pounce 1 @@ -26,11 +26,25 @@ Instances of should be configured with .Fl U to bind to UNIX-domain sockets -named by the host they wish to accept connections for in the directory passed to .Nm . . .Pp +Note that +.Nm +is not a proxy. +Incoming connections are passed directly +to instances of +.Xr pounce 1 , +which handle TLS negotiation. +Instances of +.Xr pounce 1 +and +.Nm +can be restarted +independently of each other. +. +.Pp The arguments are as follows: .Bl -tag -width Ds .It Fl H Ar host @@ -52,84 +66,53 @@ The path to the directory containing UNIX-domain sockets. .El . -.Ss Service Configuration -Add the following to -.Pa /etc/rc.conf -to enable the -.Nm -daemon: -.Bd -literal -offset indent -calico_enable="YES" -.Ed -. -.Pp -The default socket directory is -.Pa /var/run/calico . -It can be changed by setting -.Va calico_path . -The -.Xr pounce 1 -service can be configured -to listen in this directory -with the following: +.Sh EXAMPLES +Start and dispatch to two instances of +.Xr pounce 1 : .Bd -literal -offset indent -pounce_flags="-U /var/run/calico" +$ pounce -U /var/run/calico -H oftc.example.org oftc.conf +$ pounce -U /var/run/calico -H libera.example.org libera.conf +$ calico -H example.org /var/run/calico .Ed -. .Pp -The -.Nm +The two instances can be connected to via +.Li oftc.example.org:6697 and -.Xr pounce 1 -services can be started and stopped -completely independently of each other. -. -.Sh EXAMPLES -.Bd -literal -offset indent -pounce -U sockets/foo.example.org foo.conf -pounce -U sockets/bar.example.org bar.conf -calico -H example.org sockets/ -.Ed +.Li libera.example.org:6697 , +respectively. . .Sh SEE ALSO .Xr pounce 1 . .Sh STANDARDS -The -.Nm -daemon implements the following: -. .Bl -item .It .Rs -.%A E. Rescorla -.%Q Mozilla -.%T The Transport Layer Security (TLS) Protocol Version 1.3 +.%A D. Eastlake 3rd +.%T Transport Layer Security (TLS) Extensions: Extension Definitions .%I IETF -.%N RFC 8446 -.%D August 2018 -.%U https://tools.ietf.org/html/rfc8446 +.%R RFC 6066 +.%U https://tools.ietf.org/html/rfc6066 +.%D January 2011 .Re -. .It .Rs -.%A D. Eastlake 3rd -.%Q Huawei -.%T Transport Layer Security (TLS) Extensions: Extension Definitions +.%A E. Rescorla +.%T The Transport Layer Security (TLS) Protocol Version 1.3 .%I IETF -.%N RFC 6066 -.%D January 2011 -.%U https://tools.ietf.org/html/rfc6066 +.%R RFC 8446 +.%U https://tools.ietf.org/html/rfc8446 +.%D August 2018 .Re .El . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency +.An June McEnroe Aq Mt june@causal.agency . .Sh BUGS Send mail to -.Aq Mt june@causal.agency +.Aq Mt list+pounce@causal.agency or join .Li #ascii.town on -.Li chat.freenode.net . +.Li irc.tilde.chat . diff --git a/client.c b/client.c index 25707a8..23cde36 100644 --- a/client.c +++ b/client.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,10 +12,22 @@ * * 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 <fcntl.h> #include <regex.h> #include <stdarg.h> #include <stdbool.h> @@ -30,70 +42,87 @@ #include "bounce.h" -bool clientCA; -bool clientSTS = true; +enum Cap clientCaps = 0 + | CapConsumer + | CapPassive + | CapReadMarker + | CapSTS + | CapSelfMessage + | CapServerTime; + +char *clientOrigin; char *clientPass; char *clientAway; +char *clientQuit; static size_t active; -enum Need { - BIT(NeedNick), - BIT(NeedUser), - BIT(NeedPass), - BIT(NeedCapEnd), -}; +static void activeIncr(const struct Client *client) { + if (client->need) return; + if (client->caps & CapPassive) return; + if (!active++) { + serverEnqueue("AWAY\r\n"); + } +} -struct Client { - struct tls *tls; - enum Need need; - size_t consumer; - size_t setPos; - enum Cap caps; - char buf[MessageCap]; - size_t len; - bool error; -}; +static void activeDecr(const struct Client *client) { + if (client->need) return; + if (client->caps & CapPassive) return; + if (!--active && !stateAway) { + serverEnqueue("AWAY :%s\r\n", clientAway); + } +} -struct Client *clientAlloc(struct tls *tls) { +struct Client *clientAlloc(int sock, struct tls *tls) { struct Client *client = calloc(1, sizeof(*client)); if (!client) err(EX_OSERR, "calloc"); + fcntl(sock, F_SETFL, O_NONBLOCK); + client->sock = sock; client->tls = tls; - client->need = NeedNick | NeedUser | (clientPass ? NeedPass : 0); - if (clientCA && tls_peer_cert_provided(tls)) { + client->time = time(NULL); + client->idle = client->time; + client->need = NeedHandshake | NeedNick | NeedUser; + if (clientPass) client->need |= NeedPass; + return client; +} + +static void clientHandshake(struct Client *client) { + int error = tls_handshake(client->tls); + if (error == TLS_WANT_POLLIN || error == TLS_WANT_POLLOUT) return; + if (error) { + warnx("client tls_handshake: %s", tls_error(client->tls)); + client->remove = true; + return; + } + client->need &= ~NeedHandshake; + if ((clientCaps & CapSASL) && tls_peer_cert_provided(client->tls)) { client->need &= ~NeedPass; } - return client; } void clientFree(struct Client *client) { - if (!client->need) { - if (!(client->caps & CapPassive) && !--active) { - serverFormat("AWAY :%s\r\n", clientAway); - } - } + activeDecr(client); tls_close(client->tls); tls_free(client->tls); free(client); } -bool clientError(const struct Client *client) { - return client->error; -} - void clientSend(struct Client *client, const char *ptr, size_t len) { - if (verbose) fprintf(stderr, "\x1B[34m%.*s\x1B[m", (int)len, ptr); + verboseLog("<-", ptr, len); + fcntl(client->sock, F_SETFL, 0); while (len) { ssize_t ret = tls_write(client->tls, ptr, len); if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; if (ret < 0) { warnx("client tls_write: %s", tls_error(client->tls)); - client->error = true; - return; + client->remove = true; + break; } ptr += ret; len -= ret; } + fcntl(client->sock, F_SETFL, O_NONBLOCK); + client->idle = time(NULL); } void clientFormat(struct Client *client, const char *format, ...) { @@ -111,9 +140,9 @@ static void passRequired(struct Client *client) { client, ":%s 464 * :Password incorrect\r\n" "ERROR :Password incorrect\r\n", - ORIGIN + clientOrigin ); - client->error = true; + client->remove = true; } static void maybeSync(struct Client *client) { @@ -121,9 +150,7 @@ static void maybeSync(struct Client *client) { if (!client->need) { stateSync(client); if (client->setPos) ringSet(client->consumer, client->setPos); - if (!(client->caps & CapPassive) && !active++) { - serverFormat("AWAY\r\n"); - } + activeIncr(client); } } @@ -137,7 +164,7 @@ static void handleNick(struct Client *client, struct Message *msg) { static void handleUser(struct Client *client, struct Message *msg) { if (!msg->params[0]) { - client->error = true; + client->remove = true; return; } if (client->need & NeedPass) { @@ -153,25 +180,27 @@ static void handleUser(struct Client *client, struct Message *msg) { static void handlePass(struct Client *client, struct Message *msg) { if (!clientPass) return; if (!msg->params[0]) { - client->error = true; + client->remove = true; return; } - if (!strcmp(crypt(msg->params[0], clientPass), clientPass)) { +#ifdef __OpenBSD__ + int error = crypt_checkpass(msg->params[0], clientPass); +#else + int error = strcmp(crypt(msg->params[0], clientPass), clientPass); +#endif + explicit_bzero(msg->params[0], strlen(msg->params[0])); + if (error) { + passRequired(client); + } else { client->need &= ~NeedPass; maybeSync(client); - } else { - passRequired(client); } - explicit_bzero(msg->params[0], strlen(msg->params[0])); } static void handleCap(struct Client *client, struct Message *msg) { if (!msg->params[0]) msg->params[0] = ""; - enum Cap avail = (stateCaps & ~CapSASL) - | CapServerTime | CapConsumer | CapPassive - | (clientCA ? CapSASL : 0) - | (clientSTS ? CapSTS : 0); + enum Cap avail = clientCaps | (stateCaps & ~CapSASL); const char *values[CapBits] = { [CapSASLBit] = "EXTERNAL", [CapSTSBit] = "duration=2147483647", @@ -190,12 +219,12 @@ static void handleCap(struct Client *client, struct Message *msg) { if (avail & CapCapNotify) client->caps |= CapCapNotify; clientFormat( client, ":%s CAP * LS :%s\r\n", - ORIGIN, capList(avail, values) + clientOrigin, capList(avail, values) ); } else { clientFormat( client, ":%s CAP * LS :%s\r\n", - ORIGIN, capList(avail, NULL) + clientOrigin, capList(avail, NULL) ); } @@ -203,106 +232,277 @@ static void handleCap(struct Client *client, struct Message *msg) { if (client->need) client->need |= NeedCapEnd; enum Cap caps = capParse(msg->params[1], values); if (caps == (avail & caps)) { - client->caps |= caps; if (caps & CapConsumer && values[CapConsumerBit]) { client->setPos = strtoull(values[CapConsumerBit], NULL, 10); } - clientFormat(client, ":%s CAP * ACK :%s\r\n", ORIGIN, msg->params[1]); + if (caps & CapPassive && !(client->caps & CapPassive)) { + activeDecr(client); + } + client->caps |= caps; + clientFormat( + client, ":%s CAP * ACK :%s\r\n", + clientOrigin, msg->params[1] + ); } else { - clientFormat(client, ":%s CAP * NAK :%s\r\n", ORIGIN, msg->params[1]); + clientFormat( + client, ":%s CAP * NAK :%s\r\n", + clientOrigin, msg->params[1] + ); } } else if (!strcmp(msg->params[0], "LIST")) { clientFormat( client, ":%s CAP * LIST :%s\r\n", - ORIGIN, capList(client->caps, NULL) + clientOrigin, capList(client->caps, NULL) ); } else { - clientFormat(client, ":%s 410 * :Invalid CAP subcommand\r\n", ORIGIN); + clientFormat( + client, ":%s 410 * :Invalid CAP subcommand\r\n", clientOrigin + ); } } static void handleAuthenticate(struct Client *client, struct Message *msg) { if (!msg->params[0]) msg->params[0] = ""; - if (!strcmp(msg->params[0], "EXTERNAL")) { + bool cert = (clientCaps & CapSASL) && tls_peer_cert_provided(client->tls); + if (cert && !strcmp(msg->params[0], "EXTERNAL")) { clientFormat(client, "AUTHENTICATE +\r\n"); - } else if (!strcmp(msg->params[0], "+")) { + } else if (cert && !strcmp(msg->params[0], "+")) { + const char *account = (stateAccount ? stateAccount : "*"); clientFormat( - client, ":%s 900 * %s * :You are now logged in as *\r\n", - ORIGIN, stateEcho() + client, ":%s 900 * %s %s :You are now logged in as %s\r\n", + clientOrigin, stateEcho(), account, account ); clientFormat( client, ":%s 903 * :SASL authentication successful\r\n", - ORIGIN + clientOrigin ); } else { clientFormat( client, ":%s 904 * :SASL authentication failed\r\n", - ORIGIN + clientOrigin ); } } -static void handleQuit(struct Client *client, struct Message *msg) { +static void handleJoin(struct Client *client, struct Message *msg) { + (void)client; (void)msg; - clientFormat(client, "ERROR :Detaching\r\n"); - client->error = true; + // irssi intentionally sends an invalid JOIN command, at + // an invalid time (during client registration), on every + // connection. Utterly mind-boggling. Ignore it so the + // connection doesn't just get dropped like it deserves to be. } -static void handlePrivmsg(struct Client *client, struct Message *msg) { - if (!msg->params[0] || !msg->params[1]) return; +static void handleQuit(struct Client *client, struct Message *msg) { + const char *mesg = msg->params[0]; + if (mesg && !strncmp(mesg, "$pounce", 7) && (!mesg[7] || mesg[7] == ' ')) { + mesg += 7; + mesg += strspn(mesg, " "); + clientQuit = strdup(mesg); + if (!clientQuit) err(EX_OSERR, "strdup"); + } else { + clientFormat(client, "ERROR :Detaching\r\n"); + client->remove = true; + } +} - int origin; - char line[MessageCap]; - snprintf( - line, sizeof(line), "@%s %n:%s %s %s :%s", - (msg->tags ? msg->tags : ""), &origin, - stateEcho(), msg->cmd, msg->params[0], msg->params[1] - ); - size_t diff = ringDiff(client->consumer); - ringProduce((msg->tags ? line : &line[origin])); - if (!diff) ringConsume(NULL, client->consumer); - if (!strcmp(msg->params[0], stateNick())) return; +static bool hasTag(const char *tags, const char *tag) { + if (!tags) return false; + size_t len = strlen(tag); + bool val = strchr(tag, '='); + while (*tags && *tags != ' ') { + if ( + !strncmp(tags, tag, len) && + (!tags[len] || strchr((val ? "; " : "=; "), tags[len])) + ) return true; + tags += strcspn(tags, "; "); + tags += (*tags == ';'); + } + return false; +} + +static const char *synthLabel(struct Client *client) { + enum { LabelCap = 64 }; + static char buf[sizeof("label=") + LabelCap]; + snprintf(buf, sizeof(buf), "label=pounce~%zu", client->consumer); + return buf; +} +static void reserialize( + char *buf, size_t cap, const char *origin, const struct Message *msg +) { + char *ptr = buf, *end = &buf[cap]; if (msg->tags) { + ptr = seprintf(ptr, end, "@%s ", msg->tags); + } + if (origin || msg->origin) { + ptr = seprintf(ptr, end, ":%s ", (origin ? origin : msg->origin)); + } + ptr = seprintf(ptr, end, "%s", msg->cmd); + for (size_t i = 0; i < ParamCap && msg->params[i]; ++i) { + if (i + 1 == ParamCap || !msg->params[i + 1]) { + ptr = seprintf(ptr, end, " :%s", msg->params[i]); + } else { + ptr = seprintf(ptr, end, " %s", msg->params[i]); + } + } +} + +static void clientProduce(struct Client *client, const char *line) { + size_t diff = ringDiff(client->consumer); + ringProduce(line); + if (!diff && !(client->caps & CapEchoMessage)) { + ringConsume(NULL, client->consumer); + } +} + +static void handlePrivmsg(struct Client *client, struct Message *msg) { + if (!msg->params[0]) return; + char buf[MessageCap]; + bool self = !strcmp(msg->params[0], stateNick()); + if (!(stateCaps & CapEchoMessage) || self) { + reserialize(buf, sizeof(buf), stateEcho(), msg); + clientProduce(client, buf); + } + if (self) return; + reserialize(buf, sizeof(buf), NULL, msg); + if (stateCaps & CapEchoMessage && !hasTag(msg->tags, "label")) { serverFormat( - "@%s %s %s :%s\r\n", - msg->tags, msg->cmd, msg->params[0], msg->params[1] + "@%s%c%s\r\n", + synthLabel(client), + (buf[0] == '@' ? ';' : ' '), + (buf[0] == '@' ? &buf[1] : buf) ); } else { - serverFormat("%s %s :%s\r\n", msg->cmd, msg->params[0], msg->params[1]); + serverFormat("%s\r\n", buf); } } -static void handleTagmsg(struct Client *client, struct Message *msg) { - if (!msg->tags || !msg->params[0]) return; - char line[MessageCap]; +static void handlePalaver(struct Client *client, struct Message *msg) { + if (client->need & NeedPass) return; + char buf[MessageCap]; + reserialize(buf, sizeof(buf), NULL, msg); + clientProduce(client, buf); +} + +struct Marker { + char *target; + char *timestamp; +}; + +static struct { + struct Marker *ptr; + size_t cap, len; +} markers; + +void clientGetMarker(struct Client *client, const char *target) { + for (size_t i = 0; i < markers.len; ++i) { + struct Marker marker = markers.ptr[i]; + if (strcasecmp(marker.target, target)) continue; + clientFormat( + client, ":%s MARKREAD %s timestamp=%s\r\n", + clientOrigin, target, marker.timestamp + ); + return; + } + clientFormat(client, ":%s MARKREAD %s *\r\n", clientOrigin, target); +} + +static void clientSetMarker( + struct Client *client, const char *target, const char *timestamp +) { + struct Marker *marker = NULL; + for (size_t i = 0; i < markers.len; ++i) { + marker = &markers.ptr[i]; + if (strcasecmp(marker->target, target)) continue; + if (strcmp(timestamp, marker->timestamp) < 0) { + clientFormat( + client, ":%s MARKREAD %s timestamp=%s\r\n", + clientOrigin, target, marker->timestamp + ); + return; + } + set(&marker->timestamp, timestamp); + goto notify; + } + if (markers.len == markers.cap) { + markers.cap = (markers.cap ? markers.cap * 2 : 8); + markers.ptr = realloc(markers.ptr, sizeof(*markers.ptr) * markers.cap); + if (!markers.ptr) err(EX_OSERR, "realloc"); + } + marker = &markers.ptr[markers.len++]; + *marker = (struct Marker) {0}; + set(&marker->target, target); + set(&marker->timestamp, timestamp); +notify:; + char buf[512]; snprintf( - line, sizeof(line), "@%s :%s TAGMSG %s", - msg->tags, stateEcho(), msg->params[0] + buf, sizeof(buf), ":%s MARKREAD %s timestamp=%s", + clientOrigin, marker->target, marker->timestamp ); - size_t diff = ringDiff(client->consumer); - ringProduce(line); - if (!diff) ringConsume(NULL, client->consumer); - if (!strcmp(msg->params[0], stateNick())) return; - serverFormat("@%s TAGMSG %s\r\n", msg->tags, msg->params[0]); + ringProduce(buf); +} + +static regex_t *TimestampRegex(void) { + static const char *Pattern = { +#define R2D "[0-9]{2}" + "^timestamp=[0-9]{4,}-" R2D "-" R2D + "T" R2D ":" R2D ":" R2D "[.][0-9]{3}Z$" +#undef R2D + }; + static bool compiled; + static regex_t regex; + if (!compiled) { + int error = regcomp(®ex, Pattern, REG_EXTENDED | REG_NOSUB); + assert(!error); + } + compiled = true; + return ®ex; +} + +static void handleMarkRead(struct Client *client, struct Message *msg) { + if (!msg->params[0]) { + clientFormat( + client, "FAIL MARKREAD NEED_MORE_PARAMS :Missing parameters\r\n" + ); + } else if (!msg->params[1]) { + clientGetMarker(client, msg->params[0]); + } else if (regexec(TimestampRegex(), msg->params[1], 0, NULL, 0)) { + clientFormat( + client, "FAIL MARKREAD INVALID_PARAMS %s :Invalid parameters\r\n", + msg->params[1] + ); + } else { + clientSetMarker(client, msg->params[0], &msg->params[1][10]); + } +} + +static void handlePong(struct Client *client, struct Message *msg) { + (void)client; + (void)msg; } static const struct { + bool intercept; + bool need; const char *cmd; Handler *fn; - bool need; } Handlers[] = { - { "AUTHENTICATE", handleAuthenticate, false }, - { "CAP", handleCap, false }, - { "NICK", handleNick, false }, - { "NOTICE", handlePrivmsg, true }, - { "PASS", handlePass, false }, - { "PRIVMSG", handlePrivmsg, true }, - { "QUIT", handleQuit, true }, - { "TAGMSG", handleTagmsg, true }, - { "USER", handleUser, false }, + { false, false, "AUTHENTICATE", handleAuthenticate }, + { false, false, "JOIN", handleJoin }, + { false, false, "NICK", handleNick }, + { false, false, "PASS", handlePass }, + { false, false, "USER", handleUser }, + { true, false, "CAP", handleCap }, + { true, false, "PALAVER", handlePalaver }, + { true, false, "PONG", handlePong }, + { true, true, "MARKREAD", handleMarkRead }, + { true, true, "NOTICE", handlePrivmsg }, + { true, true, "PRIVMSG", handlePrivmsg }, + { true, true, "QUIT", handleQuit }, + { true, true, "TAGMSG", handlePrivmsg }, }; static void clientParse(struct Client *client, char *line) { @@ -314,7 +514,7 @@ static void clientParse(struct Client *client, char *line) { Handlers[i].fn(client, &msg); return; } - client->error = true; + client->remove = true; } static bool intercept(const char *line, size_t len) { @@ -325,16 +525,22 @@ static bool intercept(const char *line, size_t len) { len -= sp - line; line = sp; } - if (len >= 4 && !memcmp(line, "CAP ", 4)) return true; - if (len == 4 && !memcmp(line, "QUIT", 4)) return true; - if (len >= 5 && !memcmp(line, "QUIT ", 5)) return true; - if (len >= 7 && !memcmp(line, "NOTICE ", 7)) return true; - if (len >= 7 && !memcmp(line, "TAGMSG ", 7)) return true; - if (len >= 8 && !memcmp(line, "PRIVMSG ", 8)) return true; + for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) { + if (!Handlers[i].intercept) continue; + size_t n = strlen(Handlers[i].cmd); + if (len < n) continue; + if (memcmp(line, Handlers[i].cmd, n)) continue; + if (len == n || line[n] == ' ' || line[n] == '\r') return true; + } return false; } void clientRecv(struct Client *client) { + if (client->need & NeedHandshake) { + clientHandshake(client); + return; + } + ssize_t read = tls_read( client->tls, &client->buf[client->len], sizeof(client->buf) - client->len @@ -342,34 +548,29 @@ void clientRecv(struct Client *client) { if (read == TLS_WANT_POLLIN || read == TLS_WANT_POLLOUT) return; if (read <= 0) { if (read < 0) warnx("client tls_read: %s", tls_error(client->tls)); - client->error = true; + client->remove = true; return; } client->len += read; - char *crlf; + char *lf; char *line = client->buf; for (;;) { - crlf = memmem(line, &client->buf[client->len] - line, "\r\n", 2); - if (!crlf) break; - if (verbose) { - fprintf(stderr, "\x1B[33m%.*s\x1B[m\n", (int)(crlf - line), line); - } - if (client->need || intercept(line, crlf - line)) { - crlf[0] = '\0'; + lf = memchr(line, '\n', &client->buf[client->len] - line); + if (!lf) break; + verboseLog("->", line, lf - line); + if (client->need || intercept(line, lf - line)) { + lf[0] = '\0'; + if (lf - line && lf[-1] == '\r') lf[-1] = '\0'; clientParse(client, line); } else { - serverSend(line, crlf + 2 - line); + serverSend(line, lf + 1 - line); } - line = crlf + 2; + line = lf + 1; } client->len -= line - client->buf; memmove(client->buf, line, client->len); -} - -size_t clientDiff(const struct Client *client) { - if (client->need) return 0; - return ringDiff(client->consumer); + client->idle = time(NULL); } static int wordcmp(const char *line, size_t i, const char *word) { @@ -391,34 +592,22 @@ static int wordcmp(const char *line, size_t i, const char *word) { : (int)len - (int)strlen(word); } -static size_t strlcpyn(char *dst, const char *src, size_t cap, size_t len) { - if (len < cap) { - memcpy(dst, src, len); - dst[len] = '\0'; - } else { - memcpy(dst, src, cap - 1); - dst[cap - 1] = '\0'; - } - return len; -} - // s/..(..)../\1/g -static char *snip(char *dst, size_t cap, const char *src, const regex_t *regex) { - size_t len = 0; +static char * +snip(char *dst, size_t cap, const char *src, const regex_t *regex) { + char *ptr = dst, *end = &dst[cap]; regmatch_t match[2]; assert(regex->re_nsub); for (; *src; src += match[0].rm_eo) { if (regexec(regex, src, 2, match, 0)) break; - len += strlcpyn(&dst[len], src, cap - len, match[0].rm_so); - if (len >= cap) return NULL; - len += strlcpyn( - &dst[len], &src[match[1].rm_so], - cap - len, match[1].rm_eo - match[1].rm_so + ptr = seprintf( + ptr, end, "%.*s%.*s", + (int)match[0].rm_so, src, + (int)(match[1].rm_eo - match[1].rm_so), &src[match[1].rm_so] ); - if (len >= cap) return NULL; } - len += strlcpy(&dst[len], src, cap - len); - return (len < cap ? dst : NULL); + ptr = seprintf(ptr, end, "%s", src); + return (ptr == end ? NULL : dst); } static regex_t *compile(regex_t *regex, const char *pattern) { @@ -496,6 +685,14 @@ static const char *filterMultiPrefix(const char *line) { } } +static const char *filterReadMarker(const char *line) { + return (wordcmp(line, 0, "MARKREAD") ? line : NULL); +} + +static const char *filterPalaverApp(const char *line) { + return (wordcmp(line, 0, "PALAVER") ? line : NULL); +} + static const char *filterSetname(const char *line) { return (wordcmp(line, 0, "SETNAME") ? line : NULL); } @@ -510,13 +707,7 @@ static const char *filterUserhostInNames(const char *line) { ); } -static const char *filterTags(const char *line) { - if (line[0] != '@') return line; - const char *sp = strchr(line, ' '); - return (sp ? sp + 1 : NULL); -} - -static Filter *Filters[] = { +static Filter *Filters[CapBits] = { [CapAccountNotifyBit] = filterAccountNotify, [CapAwayNotifyBit] = filterAwayNotify, [CapBatchBit] = filterBatch, @@ -527,18 +718,22 @@ static Filter *Filters[] = { [CapLabeledResponseBit] = filterLabeledResponse, [CapMessageTagsBit] = filterMessageTags, [CapMultiPrefixBit] = filterMultiPrefix, + [CapPalaverAppBit] = filterPalaverApp, + [CapReadMarkerBit] = filterReadMarker, [CapSetnameBit] = filterSetname, [CapUserhostInNamesBit] = filterUserhostInNames, }; -static bool hasTime(const char *line) { - if (!strncmp(line, "@time=", 6)) return true; - while (*line && *line != ' ') { - line += strcspn(line, "; "); - if (!strncmp(line, ";time=", 6)) return true; - if (*line == ';') line++; - } - return false; +static const char *filterEchoMessage(struct Client *client, const char *line) { + if (line[0] != '@') return line; + if (!hasTag(&line[1], synthLabel(client))) return line; + return NULL; +} + +static const char *filterTags(const char *line) { + if (line[0] != '@') return line; + const char *sp = strchr(line, ' '); + return (sp ? sp + 1 : NULL); } void clientConsume(struct Client *client) { @@ -546,10 +741,13 @@ void clientConsume(struct Client *client) { const char *line = ringPeek(&time, client->consumer); if (!line) return; - if (stateCaps & TagCaps && !(client->caps & TagCaps)) { + enum Cap diff = client->caps ^ (clientCaps | stateCaps); + if (diff & CapEchoMessage) { + line = filterEchoMessage(client, line); + } + if (line && stateCaps & TagCaps && !(client->caps & TagCaps)) { line = filterTags(line); } - enum Cap diff = client->caps ^ stateCaps; for (size_t i = 0; line && i < ARRAY_LEN(Filters); ++i) { if (!Filters[i]) continue; if (diff & (1 << i)) line = Filters[i](line); @@ -559,7 +757,10 @@ void clientConsume(struct Client *client) { return; } - if (client->caps & CapServerTime && !hasTime(line)) { + if ( + client->caps & CapServerTime && + (line[0] != '@' || !hasTag(&line[1], "time")) + ) { char ts[sizeof("YYYY-MM-DDThh:mm:ss")]; struct tm *tm = gmtime(&time.tv_sec); strftime(ts, sizeof(ts), "%FT%T", tm); @@ -580,5 +781,5 @@ void clientConsume(struct Client *client) { } else { clientFormat(client, "%s\r\n", line); } - if (!client->error) ringConsume(NULL, client->consumer); + if (!client->remove) ringConsume(NULL, client->consumer); } diff --git a/compat.h b/compat.h deleted file mode 100644 index d471a8f..0000000 --- a/compat.h +++ /dev/null @@ -1,37 +0,0 @@ -/* Copyright (C) 2019 C. 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 - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -#include <stdint.h> -#include <stdlib.h> - -// libcrypto defines these functions if libc doesn't. -void explicit_bzero(void *b, size_t len); -#ifndef strlcpy -size_t strlcpy(char *restrict dst, const char *restrict src, size_t dstsize); -size_t strlcat(char *restrict dst, const char *restrict src, size_t dstsize); -#endif -uint32_t arc4random(void); -void arc4random_buf(void *buf, size_t nbytes); -uint32_t arc4random_uniform(uint32_t upper_bound); - -// The default value of SO_RCVLOWAT is 1 anyway... -#ifndef SO_NOSIGPIPE -#define SO_NOSIGPIPE SO_RCVLOWAT -#endif - -#ifndef SIGINFO -#define SIGINFO SIGUSR2 -#endif diff --git a/config.c b/config.c index 653ae16..30d7c6c 100644 --- a/config.c +++ b/config.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,6 +12,17 @@ * * 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 <err.h> @@ -43,24 +54,17 @@ int getopt_config( const char *optstring, const struct option *longopts, int *longindex ) { static int opt; - if (opt >= 0) { - opt = getopt_long(argc, argv, optstring, longopts, longindex); - } - if (opt >= 0) return opt; - for (;;) { if (!file) { - if (optind < argc) { - num = 0; - path = argv[optind++]; - file = fopen(path, "r"); - if (!file) { - warn("%s", path); - return clean('?'); - } - } else { - return clean(-1); + if (optind == argc) return clean(-1); + if (opt >= 0 && argv[optind][0] == '-') { + opt = getopt_long(argc, argv, optstring, longopts, longindex); + if (opt >= 0 || optind == argc) return clean(opt); } + num = 0; + path = argv[optind++]; + file = configOpen(path, "r"); + if (!file) return clean('?'); } for (;;) { diff --git a/configure b/configure index b75a2d3..29587a2 100755 --- a/configure +++ b/configure @@ -1,45 +1,64 @@ #!/bin/sh set -eu -base='-lcrypt' -libs='libcrypto libtls' +cflags() { + echo "CFLAGS += $*" +} +defstr() { + cflags "-D'$1=\"$2\"'" +} +defvar() { + defstr "$1" "$(pkg-config --variable=$3 $2)${4:-}" +} +ldadd() { + lib=$1; shift + echo "LDADD.${lib} = $*" +} +config() { + pkg-config --print-errors "$@" + cflags $(pkg-config --cflags "$@") + for lib; do ldadd $lib $(pkg-config --libs $lib); done +} exec >config.mk +for opt; do + case "${opt}" in + (--prefix=*) echo "PREFIX = ${opt#*=}" ;; + (--bindir=*) echo "BINDIR = ${opt#*=}" ;; + (--mandir=*) echo "MANDIR = ${opt#*=}" ;; + (--enable-notify) echo 'BINS += pounce-notify' ;; + (--enable-palaver) + echo 'BINS += pounce-palaver' + config libcurl sqlite3 + ;; + (*) echo "warning: unsupported option ${opt}" >&2 ;; + esac +done + case "$(uname)" in - (Darwin) - base='' - ;; (FreeBSD) - if ! pkg info -e libressl; then - echo 'LibreSSL required' >&2 - exit 1 - fi - prefix=$(pkg query '%p' libressl) - cat <<-EOF - MANDIR = \${PREFIX}/man - CFLAGS += -I${prefix}/include - CFLAGS += -D'OPENSSL_BIN="${prefix}/bin/openssl"' - LDFLAGS += -L${prefix}/lib - EOF - exit + config libtls + defstr OPENSSL_BIN /usr/bin/openssl + ;; + (OpenBSD) + ldadd crypt '' + defstr OPENSSL_BIN /usr/bin/openssl ;; (Linux) - cat <<-EOF - CFLAGS += -D_GNU_SOURCE - CFLAGS += -D'CERTBOT_PATH="/etc/letsencrypt"' - LDLIBS_calico = \${LDLIBS} - RCS = - DIRS = - EOF + cflags -D_GNU_SOURCE + config libtls + defvar OPENSSL_BIN openssl exec_prefix /bin/openssl + ;; + (Darwin) + cflags -D__STDC_WANT_LIB_EXT1__=1 + cflags "-D'explicit_bzero(b,l)=memset_s((b),(l),0,(l))'" + ldadd crypt '' + config libtls + defvar OPENSSL_BIN openssl exec_prefix /bin/openssl + ;; + (*) + config libtls + defvar OPENSSL_BIN openssl exec_prefix /bin/openssl ;; esac - -pkg-config --print-errors $libs - -cat <<EOF -CFLAGS += $(pkg-config --cflags $libs) -CFLAGS += -D'OPENSSL_BIN="$(pkg-config --variable=prefix openssl)/bin/openssl"' -LDFLAGS += $(pkg-config --libs-only-L $libs) -LDLIBS = $base $(pkg-config --libs-only-l $libs) -EOF diff --git a/dispatch.c b/dispatch.c index c558729..6353686 100644 --- a/dispatch.c +++ b/dispatch.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,8 +12,20 @@ * * 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 <fcntl.h> #include <netdb.h> @@ -29,55 +41,6 @@ #include <sysexits.h> #include <unistd.h> -#ifdef __FreeBSD__ -#include <sys/capsicum.h> -#endif - -#include "compat.h" - -static struct { - struct pollfd *ptr; - size_t len, cap; -} event; - -static void eventAdd(int fd) { - if (event.len == event.cap) { - event.cap = (event.cap ? event.cap * 2 : 8); - event.ptr = realloc(event.ptr, sizeof(*event.ptr) * event.cap); - if (!event.ptr) err(EX_OSERR, "malloc"); - } - event.ptr[event.len++] = (struct pollfd) { - .fd = fd, - .events = POLLIN, - }; -} - -static void eventRemove(size_t i) { - close(event.ptr[i].fd); - event.ptr[i] = event.ptr[--event.len]; -} - -static ssize_t sendfd(int sock, int fd) { - char buf[CMSG_SPACE(sizeof(int))]; - - char x = 0; - struct iovec iov = { .iov_base = &x, .iov_len = 1 }; - struct msghdr msg = { - .msg_iov = &iov, - .msg_iovlen = 1, - .msg_control = buf, - .msg_controllen = sizeof(buf), - }; - - struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); - cmsg->cmsg_len = CMSG_LEN(sizeof(int)); - cmsg->cmsg_level = SOL_SOCKET; - cmsg->cmsg_type = SCM_RIGHTS; - *(int *)CMSG_DATA(cmsg) = fd; - - return sendmsg(sock, &msg, 0); -} - static struct { uint8_t buf[4096]; uint8_t *ptr; @@ -112,7 +75,9 @@ static char *serverName(void) { skip(uint8()); skip(uint16()); skip(uint8()); - peek.len = uint16(); + uint16_t len = uint16(); + if (len > peek.len) return NULL; + peek.len = len; while (peek.len) { // Extension uint16_t type = uint16(); @@ -145,14 +110,36 @@ static void alert(int sock) { if (len < 0) warn("send"); } +static ssize_t sendfd(int sock, int fd) { + char buf[CMSG_SPACE(sizeof(int))]; + + char x = 0; + struct iovec iov = { .iov_base = &x, .iov_len = 1 }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = buf, + .msg_controllen = sizeof(buf), + }; + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(cmsg) = fd; + + return sendmsg(sock, &msg, 0); +} + int main(int argc, char *argv[]) { + int error; + const char *host = "localhost"; const char *port = "6697"; const char *path = NULL; int timeout = 1000; - int opt; - while (0 < (opt = getopt(argc, argv, "H:P:t:"))) { + for (int opt; 0 < (opt = getopt(argc, argv, "H:P:t:"));) { switch (opt) { break; case 'H': host = optarg; break; case 'P': port = optarg; @@ -170,12 +157,21 @@ int main(int argc, char *argv[]) { errx(EX_USAGE, "directory required"); } - int dir = open(path, O_DIRECTORY); - if (dir < 0) err(EX_NOINPUT, "%s", path); +#ifdef __OpenBSD__ + error = unveil(path, "rw"); + if (error) err(EX_OSERR, "unveil"); - int error = fchdir(dir); + error = pledge("stdio rpath inet unix dns sendfd", NULL); + if (error) err(EX_OSERR, "pledge"); +#endif + + error = chdir(path); if (error) err(EX_NOINPUT, "%s", path); + enum { Cap = 1024 }; + struct pollfd fds[Cap]; + size_t nfds = 0; + struct addrinfo *head; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -186,7 +182,7 @@ int main(int argc, char *argv[]) { if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error)); size_t binds = 0; - for (struct addrinfo *ai = head; ai; ai = ai->ai_next) { + for (struct addrinfo *ai = head; ai && binds < Cap - 1; ai = ai->ai_next) { int sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (sock < 0) err(EX_OSERR, "socket"); @@ -203,122 +199,87 @@ int main(int argc, char *argv[]) { continue; } - eventAdd(sock); + fds[nfds++] = (struct pollfd) { .fd = sock, .events = POLLIN }; binds++; } if (!binds) errx(EX_UNAVAILABLE, "could not bind any sockets"); freeaddrinfo(head); -#ifdef __FreeBSD__ - error = cap_enter(); - if (error) err(EX_OSERR, "cap_enter"); - - cap_rights_t dirRights, sockRights, unixRights, bindRights; - cap_rights_init(&dirRights, CAP_CONNECTAT); - cap_rights_init(&sockRights, CAP_EVENT, CAP_RECV, CAP_SEND, CAP_SETSOCKOPT); - cap_rights_init(&unixRights, CAP_CONNECT, CAP_SEND); - cap_rights_init(&bindRights, CAP_LISTEN, CAP_ACCEPT); - cap_rights_merge(&bindRights, &sockRights); - - error = cap_rights_limit(dir, &dirRights); - if (error) err(EX_OSERR, "cap_rights_limit"); for (size_t i = 0; i < binds; ++i) { - error = cap_rights_limit(event.ptr[i].fd, &bindRights); - if (error) err(EX_OSERR, "cap_rights_limit"); - } -#endif - - for (size_t i = 0; i < binds; ++i) { - error = listen(event.ptr[i].fd, 1); + error = listen(fds[i].fd, -1); if (error) err(EX_IOERR, "listen"); } signal(SIGPIPE, SIG_IGN); for (;;) { - int nfds = poll( - event.ptr, event.len, (event.len > binds ? timeout : -1) - ); - if (nfds < 0) err(EX_IOERR, "poll"); + for (size_t i = 0; i < binds; ++i) { + fds[i].events = (nfds < Cap ? POLLIN : 0); + } + + int ready = poll(fds, nfds, (nfds > binds ? timeout : -1)); + if (ready < 0) err(EX_IOERR, "poll"); - if (!nfds) { - for (size_t i = event.len - 1; i >= binds; --i) { - eventRemove(i); + if (!ready) { + for (size_t i = binds; i < nfds; ++i) { + close(fds[i].fd); } + nfds = binds; continue; } - for (size_t i = event.len - 1; i < event.len; --i) { - if (!event.ptr[i].revents) continue; + for (size_t i = nfds - 1; i < nfds; --i) { + if (!fds[i].revents) continue; if (i < binds) { - int sock = accept(event.ptr[i].fd, NULL, NULL); + int sock = accept(fds[i].fd, NULL, NULL); if (sock < 0) { warn("accept"); continue; } - - int yes = 1; - error = setsockopt( - sock, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes) - ); - if (error) err(EX_OSERR, "setsockopt"); - - eventAdd(sock); + assert(nfds < Cap); + fds[nfds++] = (struct pollfd) { .fd = sock, .events = POLLIN }; continue; } - if (event.ptr[i].revents & (POLLHUP | POLLERR)) { - eventRemove(i); - continue; - } + if (fds[i].revents & (POLLHUP | POLLERR)) goto remove; ssize_t len = recv( - event.ptr[i].fd, peek.buf, sizeof(peek.buf) - 1, MSG_PEEK + fds[i].fd, peek.buf, sizeof(peek.buf) - 1, MSG_PEEK ); if (len < 0) { warn("recv"); - eventRemove(i); - continue; + goto remove; } peek.len = len; char *name = serverName(); - if (!name || name[0] == '.' || name[0] == '/') { - alert(event.ptr[i].fd); - eventRemove(i); - continue; + if (!name || name[0] == '.' || strchr(name, '/')) { + alert(fds[i].fd); + goto remove; } struct sockaddr_un addr = { .sun_family = AF_UNIX }; - strlcpy(addr.sun_path, name, sizeof(addr.sun_path)); + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", name); int sock = socket(PF_UNIX, SOCK_STREAM, 0); if (sock < 0) err(EX_OSERR, "socket"); -#ifdef __FreeBSD__ - error = cap_rights_limit(sock, &unixRights); - if (error) err(EX_OSERR, "cap_rights_limit"); - - error = connectat( - dir, sock, (struct sockaddr *)&addr, SUN_LEN(&addr) - ); -#else error = connect(sock, (struct sockaddr *)&addr, SUN_LEN(&addr)); -#endif - if (error) { warn("%s", name); - alert(event.ptr[i].fd); + alert(fds[i].fd); } else { - len = sendfd(sock, event.ptr[i].fd); + len = sendfd(sock, fds[i].fd); if (len < 0) { warn("%s", name); - alert(event.ptr[i].fd); + alert(fds[i].fd); } } - close(sock); - eventRemove(i); + +remove: + close(fds[i].fd); + fds[i] = fds[--nfds]; } } } diff --git a/local.c b/local.c index 0a140e6..fcd670a 100644 --- a/local.c +++ b/local.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,81 +12,66 @@ * * 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 <err.h> #include <errno.h> -#include <fcntl.h> #include <limits.h> #include <netdb.h> #include <netinet/in.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> -#include <string.h> #include <sys/socket.h> -#include <sys/stat.h> #include <sys/un.h> #include <sysexits.h> #include <tls.h> #include <unistd.h> -#ifdef __FreeBSD__ -#include <sys/capsicum.h> -#endif - #include "bounce.h" static struct tls *server; -static byte *readFile(size_t *len, FILE *file) { - struct stat stat; - int error = fstat(fileno(file), &stat); - if (error) err(EX_IOERR, "fstat"); - - byte *buf = malloc(stat.st_size); - if (!buf) err(EX_OSERR, "malloc"); - - rewind(file); - *len = fread(buf, 1, stat.st_size, file); - if (ferror(file)) err(EX_IOERR, "fread"); - - return buf; -} - -void localConfig(FILE *cert, FILE *priv, FILE *ca, bool require) { - tls_free(server); - server = tls_server(); +int localConfig( + const char *cert, const char *priv, const char *ca, bool require +) { + if (!server) server = tls_server(); if (!server) errx(EX_SOFTWARE, "tls_server"); struct tls_config *config = tls_config_new(); if (!config) errx(EX_SOFTWARE, "tls_config_new"); - size_t len; - byte *buf = readFile(&len, cert); - int error = tls_config_set_cert_mem(config, buf, len); - if (error) { - errx(EX_CONFIG, "tls_config_set_cert_mem: %s", tls_config_error(config)); + int error; + char buf[PATH_MAX]; + for (int i = 0; configPath(buf, sizeof(buf), cert, i); ++i) { + error = tls_config_set_cert_file(config, buf); + if (!error) break; } - free(buf); + if (error) goto fail; - buf = readFile(&len, priv); - error = tls_config_set_key_mem(config, buf, len); - if (error) { - errx(EX_CONFIG, "tls_config_set_key_mem: %s", tls_config_error(config)); + for (int i = 0; configPath(buf, sizeof(buf), priv, i); ++i) { + error = tls_config_set_key_file(config, buf); + if (!error) break; } - free(buf); + if (error) goto fail; if (ca) { - buf = readFile(&len, ca); - error = tls_config_set_ca_mem(config, buf, len); - if (error) { - errx( - EX_CONFIG, "tls_config_set_ca_mem: %s", - tls_config_error(config) - ); + for (int i = 0; configPath(buf, sizeof(buf), ca, i); ++i) { + error = tls_config_set_ca_file(config, buf); + if (!error) break; } - free(buf); + if (error) goto fail; if (require) { tls_config_verify_client(config); } else { @@ -97,6 +82,12 @@ void localConfig(FILE *cert, FILE *priv, FILE *ca, bool require) { error = tls_configure(server, config); if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(server)); tls_config_free(config); + return 0; + +fail: + warnx("%s", tls_config_error(config)); + tls_config_free(config); + return -1; } size_t localBind(int fds[], size_t cap, const char *host, const char *port) { @@ -134,48 +125,39 @@ size_t localBind(int fds[], size_t cap, const char *host, const char *port) { } static bool unix; -static int unixDir = -1; -static char unixFile[PATH_MAX]; - -static void unixUnlink(void) { - int error = unlinkat(unixDir, unixFile, 0); - if (error) warn("unlinkat"); -} - -size_t localUnix(int fds[], size_t cap, const char *path) { - if (!cap) return 0; - - int sock = socket(PF_UNIX, SOCK_STREAM, 0); - if (sock < 0) err(EX_OSERR, "socket"); +static int unixBind(int sock, const char *path) { struct sockaddr_un addr = { .sun_family = AF_UNIX }; - size_t len = strlcpy(addr.sun_path, path, sizeof(addr.sun_path)); - if (len >= sizeof(addr.sun_path)) { + int len = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); + if ((size_t)len >= sizeof(addr.sun_path)) { errx(EX_CONFIG, "path too long: %s", path); } int error = bind(sock, (struct sockaddr *)&addr, SUN_LEN(&addr)); - if (error) err(EX_UNAVAILABLE, "%s", path); + if (!error || errno != EADDRINUSE) return error; - char dir[PATH_MAX] = "."; - const char *base = strrchr(path, '/'); - if (base) { - snprintf(dir, sizeof(dir), "%.*s", (int)(base - path), path); - base++; - } else { - base = path; + int check = socket(PF_UNIX, SOCK_STREAM, 0); + if (check < 0) err(EX_OSERR, "socket"); + + error = connect(check, (struct sockaddr *)&addr, SUN_LEN(&addr)); + close(check); + if (!error) { + errno = EADDRINUSE; + return -1; } - snprintf(unixFile, sizeof(unixFile), "%s", base); - unixDir = open(dir, O_DIRECTORY); - if (unixDir < 0) err(EX_UNAVAILABLE, "%s", dir); - atexit(unixUnlink); + unlink(path); + return bind(sock, (struct sockaddr *)&addr, SUN_LEN(&addr)); +} + +size_t localUnix(int fds[], size_t cap, const char *path) { + if (!cap) return 0; + + int sock = socket(PF_UNIX, SOCK_STREAM, 0); + if (sock < 0) err(EX_OSERR, "socket"); -#ifdef __FreeBSD__ - cap_rights_t rights; - error = cap_rights_limit(unixDir, cap_rights_init(&rights, CAP_UNLINKAT)); - if (error) err(EX_OSERR, "cap_rights_limit"); -#endif + int error = unixBind(sock, path); + if (error) err(EX_UNAVAILABLE, "%s", path); unix = true; fds[0] = sock; @@ -203,23 +185,19 @@ static int recvfd(int sock) { return *(int *)CMSG_DATA(cmsg); } -struct tls *localAccept(int *fd, int bind) { - *fd = accept(bind, NULL, NULL); - if (*fd < 0) return NULL; +int localAccept(struct tls **client, int bind) { + int fd = accept(bind, NULL, NULL); + if (fd < 0) return fd; if (unix) { - int sent = recvfd(*fd); - if (sent < 0) err(EX_IOERR, "recvfd"); - close(*fd); - *fd = sent; + int sent = recvfd(fd); + close(fd); + if (sent < 0) return sent; + fd = sent; } - int yes = 1; - int error = setsockopt(*fd, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes)); - if (error) err(EX_OSERR, "setsockopt"); - - struct tls *client; - error = tls_accept_socket(server, &client, *fd); + int error = tls_accept_socket(server, client, fd); if (error) errx(EX_SOFTWARE, "tls_accept_socket: %s", tls_error(server)); - return client; + + return fd; } diff --git a/notify.c b/notify.c new file mode 100644 index 0000000..22295f7 --- /dev/null +++ b/notify.c @@ -0,0 +1,353 @@ +/* Copyright (C) 2021 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * 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 <ctype.h> +#include <err.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <sysexits.h> +#include <time.h> +#include <tls.h> +#include <unistd.h> + +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +static bool verbose; +static struct tls *client; + +static void clientWrite(const char *ptr, size_t len) { + while (len) { + ssize_t ret = tls_write(client, ptr, len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(client)); + ptr += ret; + len -= ret; + } +} + +static void format(const char *format, ...) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + if (verbose) fprintf(stderr, "%s", buf); + clientWrite(buf, len); +} + +enum { ParamCap = 2 }; +struct Message { + char *time; + char *nick; + char *user; + char *host; + char *cmd; + char *params[ParamCap]; +}; + +static struct Message parse(char *line) { + if (verbose) fprintf(stderr, "%s\n", line); + struct Message msg = {0}; + if (line[0] == '@') { + char *tags = 1 + strsep(&line, " "); + while (tags) { + char *tag = strsep(&tags, ";"); + char *key = strsep(&tag, "="); + if (!strcmp(key, "time")) msg.time = tag; + } + } + if (line[0] == ':') { + char *origin = 1 + strsep(&line, " "); + msg.nick = strsep(&origin, "!"); + msg.user = strsep(&origin, "@"); + msg.host = origin; + } + msg.cmd = strsep(&line, " "); + for (size_t i = 0; line && i < ParamCap; ++i) { + if (line[0] == ':') { + msg.params[i] = &line[1]; + break; + } + msg.params[i] = strsep(&line, " "); + } + return msg; +} + +static void require(const struct Message *msg, bool nick, size_t len) { + if (nick && !msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + for (size_t i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(EX_PROTOCOL, "%s missing parameter %zu", msg->cmd, 1 + i); + } +} + +typedef void Handler(struct Message *msg); + +static void handlePing(struct Message *msg) { + require(msg, false, 1); + format("PONG :%s\r\n", msg->params[0]); +} + +static void handleError(struct Message *msg) { + require(msg, false, 1); + errx(EX_UNAVAILABLE, "%s", msg->params[0]); +} + +static char *nick; +static bool away; + +static void handleReplyWelcome(struct Message *msg) { + require(msg, false, 1); + free(nick); + nick = strdup(msg->params[0]); + if (!nick) err(EX_OSERR, "strdup"); + format("USERHOST %s\r\n", nick); +} + +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (nick && !strcmp(msg->nick, nick)) { + free(nick); + nick = strdup(msg->params[0]); + if (!nick) err(EX_OSERR, "strdup"); + } +} + +static void handleReplyUserHost(struct Message *msg) { + require(msg, false, 2); + while (msg->params[1]) { + char *reply = strsep(&msg->params[1], " "); + char *replyNick = strsep(&reply, "*="); + if (strcmp(replyNick, nick)) continue; + if (reply && !reply[0]) strsep(&msg->params[1], "="); + if (!reply) errx(EX_PROTOCOL, "invalid USERHOST reply"); + away = (reply[0] == '-'); + break; + } +} + +static void handleReplyNowAway(struct Message *msg) { + (void)msg; + away = true; +} + +static void handleReplyUnaway(struct Message *msg) { + (void)msg; + away = false; +} + +static const char *command; + +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + if (!nick || !away) return; + + if (!msg->time) return; + struct tm tm = {0}; + strptime(msg->time, "%FT%T", &tm); + time_t then = timegm(&tm); + if (time(NULL) - then > 60) return; + + bool query = (msg->params[0][0] != '#'); + bool mention = false; + size_t len = strlen(nick); + for ( + char *match = msg->params[1]; + NULL != (match = strstr(match, nick)); + match = &match[len] + ) { + char a = (match > msg->params[1] ? match[-1] : ' '); + char b = (match[len] ? match[len] : ' '); + if (b == '\1') b = ' '; + if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { + mention = true; + break; + } + match = &match[len]; + } + if (!query && !mention) return; + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + setenv("NOTIFY_TIME", msg->time, 1); + setenv("NOTIFY_NICK", msg->nick, 1); + if (msg->user) setenv("NOTIFY_USER", msg->user, 1); + if (msg->host) setenv("NOTIFY_HOST", msg->host, 1); + if (!query) setenv("NOTIFY_CHANNEL", msg->params[0], 1); + setenv("NOTIFY_MESSAGE", msg->params[1], 1); + + const char *shell = getenv("SHELL"); + if (!shell) shell = "/bin/sh"; + execl(shell, "sh", "-c", command, NULL); + err(EX_OSERR, "%s", shell); +} + +static const struct { + const char *cmd; + Handler *fn; +} Handlers[] = { + { "001", handleReplyWelcome }, + { "302", handleReplyUserHost }, + { "305", handleReplyUnaway }, + { "306", handleReplyNowAway }, + { "ERROR", handleError }, + { "NICK", handleNick }, + { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, +}; + +static void handle(struct Message *msg) { + if (!msg->cmd) return; + for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) { + if (strcmp(msg->cmd, Handlers[i].cmd)) continue; + Handlers[i].fn(msg); + break; + } +} + +static void reap(int sig) { + (void)sig; + int status; + wait(&status); +} + +static void quit(int sig) { + (void)sig; + format("QUIT\r\n"); + tls_close(client); + _exit(EX_OK); +} + +int main(int argc, char *argv[]) { + bool insecure = false; + const char *cert = NULL; + const char *priv = NULL; + const char *host = NULL; + const char *port = "6697"; + const char *pass = NULL; + const char *trust = NULL; + const char *user = "pounce-notify"; + + for (int opt; 0 < (opt = getopt(argc, argv, "!c:k:p:t:u:vw:"));) { + switch (opt) { + break; case '!': insecure = true; + break; case 'c': cert = optarg; + break; case 'k': priv = optarg; + break; case 'p': port = optarg; + break; case 't': trust = optarg; + break; case 'u': user = optarg; + break; case 'v': verbose = true; + break; case 'w': pass = optarg; + break; default: return EX_USAGE; + } + } + if (argc - optind < 1) errx(EX_USAGE, "host required"); + if (argc - optind < 2) errx(EX_USAGE, "command required"); + host = argv[optind++]; + command = argv[optind]; + + setenv("POUNCE_HOST", host, 1); + setenv("POUNCE_PORT", port, 1); + + client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + struct tls_config *config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); + + if (insecure) { + tls_config_insecure_noverifycert(config); + tls_config_insecure_noverifyname(config); + } + + int error; + if (trust) { + tls_config_insecure_noverifyname(config); + error = tls_config_set_ca_file(config, trust); + if (error) errx(EX_NOINPUT, "%s: %s", trust, tls_config_error(config)); + } + if (cert) { + error = tls_config_set_keypair_file(config, cert, (priv ? priv : cert)); + if (error) { + errx( + EX_NOINPUT, "tls_config_set_keypair_file: %s", + tls_config_error(config) + ); + } + } + + error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); + + error = tls_connect(client, host, port); + if (error) errx(EX_UNAVAILABLE, "tls_connect: %s", tls_error(client)); + + if (pass) format("PASS :%s\r\n", pass); + format( + "CAP REQ :causal.agency/passive server-time\r\n" + "CAP END\r\n" + "NICK *\r\n" + "USER %s 0 * :pounce-notify\r\n", + user + ); + + signal(SIGINT, quit); + signal(SIGTERM, quit); + signal(SIGCHLD, reap); + + size_t len = 0; + char buf[8191 + 512]; + for (;;) { + ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); + if (!ret) errx(EX_PROTOCOL, "server closed connection"); + len += ret; + + char *line = buf; + for (;;) { + char *crlf = memmem(line, &buf[len] - line, "\r\n", 2); + if (!crlf) break; + *crlf = '\0'; + struct Message msg = parse(line); + handle(&msg); + line = crlf + 2; + } + len -= line - buf; + memmove(buf, line, len); + } +} diff --git a/palaver.c b/palaver.c new file mode 100644 index 0000000..1453551 --- /dev/null +++ b/palaver.c @@ -0,0 +1,796 @@ +/* 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * 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 <ctype.h> +#include <curl/curl.h> +#include <err.h> +#include <errno.h> +#include <limits.h> +#include <signal.h> +#include <sqlite3.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sysexits.h> +#include <time.h> +#include <tls.h> +#include <unistd.h> + +char *dataPath(char *buf, size_t cap, const char *path, int i); + +// Why must it return (const unsigned char *)? +#define sqlite3_column_text(...) (const char *)sqlite3_column_text(__VA_ARGS__) + +#define SQL(...) #__VA_ARGS__ +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +static bool verbose; +static char curlError[CURL_ERROR_SIZE]; + +static CURL *curl; +static sqlite3 *db; +static struct tls *client; + +static void dbOpen(const char *path, int flags) { + int error = sqlite3_open_v2(path, &db, flags, NULL); + if (error == SQLITE_CANTOPEN) { + sqlite3_close(db); + db = NULL; + return; + } + if (error) errx(EX_NOINPUT, "%s: %s", path, sqlite3_errmsg(db)); + + sqlite3_busy_timeout(db, 10000); +} + +static void dbFind(const char *path) { + if (path) { + dbOpen(path, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + if (db) return; + errx(EX_NOINPUT, "%s: database not found", path); + } + + char buf[PATH_MAX]; + for (int i = 0; dataPath(buf, sizeof(buf), "palaver.sqlite", i); ++i) { + dbOpen(buf, SQLITE_OPEN_READWRITE); + if (db) return; + } + + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), 0700); + if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", buf); + + dbOpen( + dataPath(buf, sizeof(buf), "palaver.sqlite", 0), + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE + ); + if (!db) errx(EX_CANTCREAT, "%s: cannot create database", buf); +} + +static int dbParam(sqlite3_stmt *stmt, const char *param) { + int index = sqlite3_bind_parameter_index(stmt, param); + if (index) return index; + errx(EX_SOFTWARE, "no such parameter %s: %s", param, sqlite3_sql(stmt)); +} + +static void +dbBindText(sqlite3_stmt *stmt, const char *param, const char *value) { + if (!sqlite3_bind_text(stmt, dbParam(stmt, param), value, -1, NULL)) return; + errx(EX_SOFTWARE, "sqlite3_bind_text: %s", sqlite3_errmsg(db)); +} + +static void +dbBindCopy(sqlite3_stmt *stmt, const char *param, const char *value) { + int error = sqlite3_bind_text( + stmt, dbParam(stmt, param), value, -1, SQLITE_TRANSIENT + ); + if (error) errx(EX_SOFTWARE, "sqlite3_bind_text: %s", sqlite3_errmsg(db)); +} + +static void dbVerbose(sqlite3_stmt *stmt) { + if (!verbose) return; + char *sql = sqlite3_expanded_sql(stmt); + if (sql) fprintf(stderr, "%s\n", sql); + sqlite3_free(sql); +} + +static void dbInit(void) { + const char *sql = SQL( + CREATE TABLE IF NOT EXISTS clients ( + host TEXT NOT NULL, + port INTEGER NOT NULL, + client TEXT NOT NULL, + version TEXT NOT NULL, + UNIQUE (host, port, client) + ); + CREATE TABLE IF NOT EXISTS preferences ( + client TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS preferencesIndex + ON preferences (client, key); + CREATE TABLE IF NOT EXISTS badges ( + host TEXT NOT NULL, + port TEXT NOT NULL, + count INTEGER NOT NULL, + UNIQUE (host, port) + ); + ); + int error = sqlite3_exec(db, sql, NULL, NULL, NULL); + if (error) errx(EX_SOFTWARE, "%s: %s", sqlite3_errmsg(db), sql); +} + +static void clientWrite(const char *ptr, size_t len) { + while (len) { + ssize_t ret = tls_write(client, ptr, len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(client)); + ptr += ret; + len -= ret; + } +} + +static void format(const char *format, ...) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + if (verbose) fprintf(stderr, "%s", buf); + clientWrite(buf, len); +} + +enum { ParamCap = 4 }; +struct Message { + char *time; + char *nick; + char *cmd; + char *params[ParamCap]; +}; + +static struct Message parse(char *line) { + if (verbose) fprintf(stderr, "%s\n", line); + struct Message msg = {0}; + if (line[0] == '@') { + char *tags = 1 + strsep(&line, " "); + while (tags) { + char *tag = strsep(&tags, ";"); + char *key = strsep(&tag, "="); + if (!strcmp(key, "time")) msg.time = tag; + } + } + if (line[0] == ':') { + char *origin = 1 + strsep(&line, " "); + msg.nick = strsep(&origin, "!"); + } + msg.cmd = strsep(&line, " "); + for (size_t i = 0; line && i < ParamCap; ++i) { + if (line[0] == ':') { + msg.params[i] = &line[1]; + break; + } + msg.params[i] = strsep(&line, " "); + } + return msg; +} + +static void require(const struct Message *msg, bool nick, size_t len) { + if (nick && !msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + for (size_t i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(EX_PROTOCOL, "%s missing parameter %zu", msg->cmd, 1 + i); + } +} + +typedef void Handler(struct Message *msg); + +static void handleCap(struct Message *msg) { + require(msg, false, 3); + if (!strcmp(msg->params[1], "NAK")) { + errx(EX_CONFIG, "pounce palaver option not enabled"); + } +} + +static void handlePing(struct Message *msg) { + require(msg, false, 1); + format("PONG :%s\r\n", msg->params[0]); +} + +static void handleError(struct Message *msg) { + require(msg, false, 1); + errx(EX_UNAVAILABLE, "%s", msg->params[0]); +} + +static char *nick; +static bool away; + +static void handleReplyWelcome(struct Message *msg) { + require(msg, false, 1); + free(nick); + nick = strdup(msg->params[0]); + if (!nick) err(EX_OSERR, "strdup"); + format("USERHOST %s\r\n", nick); +} + +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (nick && !strcmp(msg->nick, nick)) { + free(nick); + nick = strdup(msg->params[0]); + if (!nick) err(EX_OSERR, "strdup"); + } +} + +static void handleReplyUserHost(struct Message *msg) { + require(msg, false, 2); + while (msg->params[1]) { + char *reply = strsep(&msg->params[1], " "); + char *replyNick = strsep(&reply, "*="); + if (strcmp(replyNick, nick)) continue; + if (reply && !reply[0]) strsep(&msg->params[1], "="); + if (!reply) errx(EX_PROTOCOL, "invalid USERHOST reply"); + away = (reply[0] == '-'); + break; + } +} + +static bool sensitive; + +static void keyword(sqlite3_context *ctx, int n, sqlite3_value *args[]) { + assert(n == 2); + const char *haystack = (const char *)sqlite3_value_text(args[0]); + const char *needle = (const char *)sqlite3_value_text(args[1]); + if (!nick || !haystack || !needle) { + sqlite3_result_null(ctx); + return; + } + + char *copy = NULL; + const char *replace; + if (!strcmp(needle, "{nick}")) { + needle = nick; + } else if (NULL != (replace = strstr(needle, "{nick}"))) { + int n = asprintf( + ©, "%.*s%s%s", + (int)(replace - needle), needle, nick, &replace[6] + ); + if (n < 0) { + sqlite3_result_error_nomem(ctx); + return; + } + needle = copy; + } + + size_t len = strlen(needle); + const char *match = haystack; + sqlite3_result_int(ctx, false); + while (NULL != (match = (sensitive ? strstr : strcasestr)(match, needle))) { + char a = (match > haystack ? match[-1] : ' '); + char b = (match[len] ? match[len] : ' '); + if (b == '\1') b = ' '; + if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { + sqlite3_result_int(ctx, true); + break; + } + match = &match[len]; + } + free(copy); +} + +enum { + Identify, + Begin, + Set, + End, + Each, + Notify, + Increment, + Reset, + Badge, + QueriesLen, +}; + +static sqlite3_stmt *stmts[QueriesLen]; +static const char *Queries[QueriesLen] = { + [Identify] = SQL( + SELECT 1 FROM clients + WHERE host = :host AND port = :port + AND client = :client AND version = :version; + ), + + [Begin] = SQL( + DELETE FROM preferences WHERE client = :client; + ), + + [Set] = SQL( + INSERT INTO preferences (client, key, value) + VALUES (:client, :key, :value); + ), + + [End] = SQL( + INSERT INTO clients (host, port, client, version) + VALUES (:host, :port, :client, :version) + ON CONFLICT (host, port, client) DO + UPDATE SET version = :version + WHERE host = :host AND port = :port AND client = :client; + ), + + [Each] = SQL( + SELECT pushToken.value, pushEndpoint.value + FROM clients + JOIN preferences AS pushToken USING (client) + JOIN preferences AS pushEndpoint USING (client) + WHERE host = :host AND port = :port + AND pushToken.key = 'PUSH-TOKEN' + AND pushEndpoint.key = 'PUSH-ENDPOINT'; + ), + + [Notify] = SQL( + WITH mentions AS ( + SELECT DISTINCT client + FROM clients + JOIN preferences USING (client) + WHERE host = :host AND port = :port AND ( + (key = 'MENTION-KEYWORD' AND keyword(:message, value)) OR + (key = 'MENTION-CHANNEL' AND value = :channel) OR + (key = 'MENTION-NICK' AND value = :nick) OR + :direct + ) + ), + ignores AS ( + SELECT DISTINCT client + FROM clients + JOIN preferences USING (client) + WHERE host = :host AND port = :port AND ( + (key = 'IGNORE-KEYWORD' AND keyword(:message, value)) OR + (key = 'IGNORE-CHANNEL' AND value = :channel) OR + (key = 'IGNORE-NICK' AND value = :nick) + ) + ), + matches AS (SELECT * FROM mentions EXCEPT SELECT * FROM ignores) + SELECT + pushToken.value, + pushEndpoint.value, + coalesce(showMessagePreview.value, 'true') + FROM clients + JOIN matches USING (client) + JOIN preferences AS pushToken USING (client) + JOIN preferences AS pushEndpoint USING (client) + LEFT JOIN preferences AS showMessagePreview + ON showMessagePreview.client = clients.client + AND showMessagePreview.key = 'SHOW-MESSAGE-PREVIEW' + WHERE pushToken.key = 'PUSH-TOKEN' + AND pushEndpoint.key = 'PUSH-ENDPOINT'; + ), + + [Increment] = SQL( + INSERT INTO badges (host, port, count) + VALUES (:host, :port, 1) + ON CONFLICT (host, port) DO + UPDATE SET count = count + 1 + WHERE host = :host AND port = :port; + ), + + [Reset] = SQL( + DELETE FROM badges WHERE host = :host AND port = :port; + ), + + [Badge] = SQL( + SELECT sum(count) FROM badges; + ), +}; + +static int badgeCount(int op) { + dbVerbose(stmts[op]); + int result = sqlite3_step(stmts[op]); + if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + sqlite3_reset(stmts[op]); + + dbVerbose(stmts[Badge]); + result = sqlite3_step(stmts[Badge]); + if (result != SQLITE_ROW) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + int badge = sqlite3_column_int(stmts[Badge], 0); + sqlite3_reset(stmts[Badge]); + return badge; +} + +static void palaverIdentify(struct Message *msg) { + require(msg, false, 3); + dbBindText(stmts[Identify], ":client", msg->params[1]); + dbBindText(stmts[Identify], ":version", msg->params[2]); + dbVerbose(stmts[Identify]); + int result = sqlite3_step(stmts[Identify]); + if (result == SQLITE_DONE) { + format("PALAVER REQ\r\n"); + } else if (result != SQLITE_ROW) { + errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + } + sqlite3_reset(stmts[Identify]); +} + +static void palaverBegin(struct Message *msg) { + require(msg, false, 3); + dbBindText(stmts[Begin], ":client", msg->params[1]); + dbVerbose(stmts[Begin]); + int result = sqlite3_step(stmts[Begin]); + if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + sqlite3_reset(stmts[Begin]); + dbBindCopy(stmts[Set], ":client", msg->params[1]); + dbBindCopy(stmts[End], ":client", msg->params[1]); + dbBindCopy(stmts[End], ":version", msg->params[2]); +} + +static void palaverSet(struct Message *msg) { + require(msg, false, 3); + dbBindText(stmts[Set], ":key", msg->params[1]); + dbBindText(stmts[Set], ":value", msg->params[2]); + dbVerbose(stmts[Set]); + int result = sqlite3_step(stmts[Set]); + if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + sqlite3_reset(stmts[Set]); +} + +static void palaverEnd(struct Message *msg) { + (void)msg; + dbVerbose(stmts[End]); + int result = sqlite3_step(stmts[End]); + if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + sqlite3_reset(stmts[End]); +} + +static void handlePalaver(struct Message *msg) { + require(msg, false, 1); + if (!strcmp(msg->params[0], "IDENTIFY")) { + palaverIdentify(msg); + } else if (!strcmp(msg->params[0], "BEGIN")) { + palaverBegin(msg); + } else if (!strcmp(msg->params[0], "SET")) { + palaverSet(msg); + } else if (!strcmp(msg->params[0], "ADD")) { + palaverSet(msg); + } else if (!strcmp(msg->params[0], "END")) { + palaverEnd(msg); + } +} + +static void pushNotify(const char *endpoint, const char *token, char *body) { + CURLcode code = curl_easy_setopt(curl, CURLOPT_URL, endpoint); + if (code) { + warnx("%s: %s", endpoint, curlError); + return; + } + + char auth[256]; + struct curl_slist *list = NULL; + snprintf(auth, sizeof(auth), "Authorization: Bearer %s", token); + list = curl_slist_append(list, "Content-Type: application/json"); + list = curl_slist_append(list, auth); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); + + size_t len = strlen(body); + FILE *file = fmemopen(body, len, "r"); + if (!file) err(EX_OSERR, "fmemopen"); + + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)len); + curl_easy_setopt(curl, CURLOPT_READDATA, file); + + if (verbose) fprintf(stderr, "%s\n", body); + code = curl_easy_perform(curl); + if (code) warnx("%s: %s", endpoint, curlError); + + fclose(file); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL); + curl_slist_free_all(list); +} + +static void handleReplyNowAway(struct Message *msg) { + (void)msg; + away = true; +} + +static void handleReplyUnaway(struct Message *msg) { + (void)msg; + if (!away) return; + away = false; + + char json[32]; + snprintf(json, sizeof(json), "{\"badge\":%d}", badgeCount(Reset)); + + int result; + dbVerbose(stmts[Each]); + while (SQLITE_ROW == (result = sqlite3_step(stmts[Each]))) { + int i = 0; + const char *token = sqlite3_column_text(stmts[Each], i++); + const char *endpoint = sqlite3_column_text(stmts[Each], i++); + pushNotify(endpoint, token, json); + } + if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + sqlite3_reset(stmts[Each]); +} + +static bool noPreview; +static bool noPrivatePreview; + +static void jsonString(FILE *file, const char *str) { + fputc('"', file); + for (const char *ch = str; *ch; ++ch) { + if (iscntrl(*ch) || *ch == '"' || *ch == '\\') { + fprintf(file, "\\u%04x", (unsigned)*ch); + } else { + fputc(*ch, file); + } + } + fputc('"', file); +} + +static char *jsonBody(int badge, struct Message *msg, bool preview) { + bool private = (msg->params[0][0] != '#'); + if (private && noPrivatePreview) preview = false; + if (noPreview) preview = false; + + char *buf; + size_t len; + FILE *file = open_memstream(&buf, &len); + if (!file) err(EX_OSERR, "open_memstream"); + + fprintf(file, "{\"badge\":%d", badge); + fprintf(file, ",\"sender\":"); + jsonString(file, msg->nick); + if (!private) { + fprintf(file, ",\"channel\":"); + jsonString(file, msg->params[0]); + } + if (preview) { + if (!strncmp(msg->params[1], "\1ACTION ", 8)) { + size_t len = strlen(msg->params[1]); + if (msg->params[1][len - 1] == '\1') msg->params[1][len - 1] = '\0'; + fprintf(file, ",\"intent\":\"ACTION\",\"message\":"); + jsonString(file, &msg->params[1][8]); + } else { + fprintf(file, ",\"message\":"); + jsonString(file, msg->params[1]); + } + } else { + fprintf(file, ",\"private\":true"); + } + fprintf(file, "}"); + + int error = fclose(file); + if (error) err(EX_IOERR, "fclose"); + + return buf; +} + +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + if (!away) return; + if (!msg->time) return; + struct tm tm = {0}; + strptime(msg->time, "%FT%T", &tm); + time_t then = timegm(&tm); + if (time(NULL) - then > 60) return; + + dbBindText(stmts[Notify], ":nick", msg->nick); + dbBindText(stmts[Notify], ":channel", msg->params[0]); + dbBindText(stmts[Notify], ":message", msg->params[1]); + dbBindText( + stmts[Notify], ":direct", (!strcmp(msg->params[0], nick) ? "1" : NULL) + ); + dbVerbose(stmts[Notify]); + int result; + int badge = 0; + while (SQLITE_ROW == (result = sqlite3_step(stmts[Notify]))) { + int i = 0; + const char *token = sqlite3_column_text(stmts[Notify], i++); + const char *endpoint = sqlite3_column_text(stmts[Notify], i++); + const char *preview = sqlite3_column_text(stmts[Notify], i++); + + if (!badge) badge = badgeCount(Increment); + char *body = jsonBody(badge, msg, !strcmp(preview, "true")); + pushNotify(endpoint, token, body); + free(body); + } + if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); + sqlite3_reset(stmts[Notify]); +} + +static const struct { + const char *cmd; + Handler *fn; +} Handlers[] = { + { "001", handleReplyWelcome }, + { "302", handleReplyUserHost }, + { "305", handleReplyUnaway }, + { "306", handleReplyNowAway }, + { "CAP", handleCap }, + { "ERROR", handleError }, + { "NICK", handleNick }, + { "NOTICE", handlePrivmsg }, + { "PALAVER", handlePalaver }, + { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, +}; + +static void handle(struct Message *msg) { + if (!msg->cmd) return; + for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) { + if (strcmp(msg->cmd, Handlers[i].cmd)) continue; + Handlers[i].fn(msg); + break; + } +} + +static void atExit(void) { + if (client) tls_close(client); + curl_easy_cleanup(curl); + for (size_t i = 0; i < QueriesLen; ++i) { + sqlite3_finalize(stmts[i]); + } + sqlite3_close(db); +} + +static void quit(int sig) { + (void)sig; + format("QUIT\r\n"); + atExit(); + _exit(EX_OK); +} + +int main(int argc, char *argv[]) { + bool insecure = false; + char *path = NULL; + const char *cert = NULL; + const char *priv = NULL; + const char *host = NULL; + const char *port = "6697"; + const char *pass = NULL; + const char *trust = NULL; + const char *user = "pounce-palaver"; + + for (int opt; 0 < (opt = getopt(argc, argv, "!NPc:d:k:p:st:u:vw:"));) { + switch (opt) { + break; case '!': insecure = true; + break; case 'N': noPreview = true; + break; case 'P': noPrivatePreview = true; + break; case 'c': cert = optarg; + break; case 'd': path = optarg; + break; case 'k': priv = optarg; + break; case 'p': port = optarg; + break; case 's': sensitive = true; + break; case 't': trust = optarg; + break; case 'u': user = optarg; + break; case 'v': verbose = true; + break; case 'w': pass = optarg; + break; default: return EX_USAGE; + } + } + if (optind == argc) errx(EX_USAGE, "host required"); + host = argv[optind]; + + CURLcode code = curl_global_init(CURL_GLOBAL_ALL); + if (code) errx(EX_OSERR, "curl_global_init: %s", curl_easy_strerror(code)); + + curl = curl_easy_init(); + if (!curl) errx(EX_SOFTWARE, "curl_easy_init"); + + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curlError); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); + curl_easy_setopt(curl, CURLOPT_VERBOSE, (verbose ? 1L : 0L)); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + + dbFind(path); + atexit(atExit); + + dbInit(); + sqlite3_create_function( + db, "keyword", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, + keyword, NULL, NULL + ); + for (size_t i = 0; i < QueriesLen; ++i) { + int error = sqlite3_prepare_v3( + db, Queries[i], -1, SQLITE_PREPARE_PERSISTENT, &stmts[i], NULL + ); + if (error) errx(EX_SOFTWARE, "%s: %s", sqlite3_errmsg(db), Queries[i]); + if (sqlite3_bind_parameter_index(stmts[i], ":host")) { + dbBindText(stmts[i], ":host", host); + dbBindText(stmts[i], ":port", port); + } + } + + client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + struct tls_config *config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); + + if (insecure) { + tls_config_insecure_noverifycert(config); + tls_config_insecure_noverifyname(config); + } + + int error; + if (trust) { + tls_config_insecure_noverifyname(config); + error = tls_config_set_ca_file(config, trust); + if (error) errx(EX_NOINPUT, "%s: %s", trust, tls_config_error(config)); + } + if (cert) { + error = tls_config_set_keypair_file(config, cert, (priv ? priv : cert)); + if (error) { + errx( + EX_SOFTWARE, "tls_config_set_keypair_file: %s", + tls_config_error(config) + ); + } + } + + error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); + + error = tls_connect(client, host, port); + if (error) errx(EX_UNAVAILABLE, "tls_connect: %s", tls_error(client)); + + if (pass) format("PASS :%s\r\n", pass); + format( + "CAP REQ :server-time palaverapp.com causal.agency/passive\r\n" + "CAP END\r\n" + "NICK *\r\n" + "USER %s 0 * :pounce-palaver\r\n", + user + ); + + signal(SIGINT, quit); + signal(SIGTERM, quit); + + char buf[8191 + 512]; + size_t len = 0; + for (;;) { + ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); + if (!ret) errx(EX_PROTOCOL, "server closed connection"); + len += ret; + + char *line = buf; + for (;;) { + char *crlf = memmem(line, &buf[len] - line, "\r\n", 2); + if (!crlf) break; + crlf[0] = '\0'; + struct Message msg = parse(line); + handle(&msg); + line = crlf + 2; + } + len -= line - buf; + memmove(buf, line, len); + } +} diff --git a/pounce-notify.1 b/pounce-notify.1 new file mode 100644 index 0000000..2df59a0 --- /dev/null +++ b/pounce-notify.1 @@ -0,0 +1,115 @@ +.Dd November 28, 2021 +.Dt POUNCE-NOTIFY 1 +.Os +. +.Sh NAME +.Nm pounce-notify +.Nd notifications for pounce +. +.Sh SYNOPSIS +.Nm +.Op Fl v +.Op Fl c Ar cert +.Op Fl k Ar priv +.Op Fl p Ar port +.Op Fl t Ar trust +.Op Fl u Ar user +.Op Fl w Ar pass +.Ar host +.Ar command +. +.Sh DESCRIPTION +The +.Nm +program connects to an instance of +.Xr pounce 1 +at +.Ar host +and runs +.Ar command +for each private message +or message containing the user's nickname +while the user is marked as away. +The nickname is matched case-sensitively +and must be surrounded by whitespace or punctuation. +Information about the message +is passed to the command +through the environment. +. +.Pp +The arguments are as follows: +.Bl -tag -width Ds +.It Fl c Ar cert +Load the TLS client certificate from +.Ar cert . +If the private key is in a separate file, +it is loaded with +.Fl k . +.It Fl k Ar priv +Load the TLS client private key from +.Ar priv . +.It Fl p Ar port +Connect to +.Ar port . +The default port is 6697. +.It Fl t Ar path +Trust the self-signed certificate loaded +from +.Ar path +and disable server name verification. +.It Fl u Ar user +Set the username to +.Ar user . +The default username is +.Dq pounce-notify . +.It Fl w Ar pass +Log in with the server password +.Ar pass . +.El +. +.Sh ENVIRONMENT +The following variables +are set in the environment of +.Ar command : +.Pp +.Bl -tag -width "NOTIFY_MESSAGE" -compact +.It Ev NOTIFY_CHANNEL +The name of the channel +in which the message was sent. +Unset if the message was private. +.It Ev NOTIFY_HOST +The hostname of the user who sent the message. +.It Ev NOTIFY_MESSAGE +The content of the message. +.It Ev NOTIFY_NICK +The nickname of the user who sent the message. +.It Ev NOTIFY_TIME +The time the message was sent, +in +.Sy server-time +format. +.It Ev NOTIFY_USER +The username of the user who sent the message. +.It Ev POUNCE_HOST +The host +.Nm +is connected to. +.It Ev POUNCE_PORT +The port +.Nm +is connected to. +.El +. +.Sh SEE ALSO +.Xr pounce 1 +. +.Sh AUTHORS +.An June McEnroe Aq Mt june@causal.agency +. +.Sh BUGS +Send mail to +.Aq Mt list+pounce@causal.agency +or join +.Li #ascii.town +on +.Li irc.tilde.chat . diff --git a/pounce-palaver.1 b/pounce-palaver.1 new file mode 100644 index 0000000..2d5aa1d --- /dev/null +++ b/pounce-palaver.1 @@ -0,0 +1,112 @@ +.Dd November 28, 2021 +.Dt POUNCE-PALAVER 1 +.Os +. +.Sh NAME +.Nm pounce-palaver +.Nd Palaver push notifications for pounce +. +.Sh SYNOPSIS +.Nm +.Op Fl PNsv +.Op Fl c Ar cert +.Op Fl d Ar path +.Op Fl k Ar priv +.Op Fl p Ar port +.Op Fl t Ar trust +.Op Fl u Ar user +.Op Fl w Ar pass +.Ar host +. +.Sh DESCRIPTION +The +.Nm +daemon provides push notifications +for the Palaver IRC app via the +.Xr pounce 1 +IRC bouncer. +The +.Cm palaver +option must be enabled in +.Xr pounce 1 . +. +.Pp +The arguments are as follows: +.Bl -tag -width Ds +.It Fl N +Never send message previews, +regardless of the app preferences. +.It Fl P +Never send message previews +for private messages. +.It Fl c Ar cert +Load the TLS client certificate from +.Ar path . +If the private key is in a separate file, +it is loaded with +.Fl k . +.It Fl d Ar path +Set the path to the database file +used to store notification preferences. +The default path is documented in +.Sx FILES . +.It Fl k Ar priv +Load the TLS client private key from +.Ar path . +.It Fl p Ar port +Connect to +.Ar port . +The default port is 6697. +.It Fl s +Match nick and keywords case-sensitively, +despite the specification. +.It Fl t Ar path +Trust the self-signed certificate loaded from +.Ar path +and disable server name verification. +.It Fl u Ar user +Set the username to +.Ar user . +The default username is +.Dq pounce-palaver . +.It Fl v +Log IRC protocol, SQL and HTTP to standard error. +.It Fl w Ar pass +Log in with the server password +.Ar pass . +.It Ar host +Connect to +.Ar host . +.El +. +.Sh FILES +.Bl -tag -width Ds +.It Pa $XDG_DATA_DIRS/pounce/palaver.sqlite +The database file is searched for first in +.Ev $XDG_DATA_HOME , +usually +.Pa ~/.local/share , +followed by the colon-separated list of paths +.Ev $XDG_DATA_DIRS , +usually +.Pa /usr/local/share:/usr/share . +.It Pa ~/.local/share/pounce/palaver.sqlite +The most likely default path to the database file. +.El +. +.Sh SEE ALSO +.Xr pounce 1 +. +.Sh STANDARDS +.Lk https://github.com/cocodelabs/palaver-irc-capability "Palaver IRC Capability" +. +.Sh AUTHORS +.An June McEnroe Aq Mt june@causal.agency +. +.Sh BUGS +Send mail to +.Aq Mt list+pounce@causal.agency +or join +.Li #ascii.town +on +.Li irc.tilde.chat . diff --git a/pounce.1 b/pounce.1 index b61527a..e4919d2 100644 --- a/pounce.1 +++ b/pounce.1 @@ -1,4 +1,4 @@ -.Dd February 27, 2020 +.Dd July 16, 2023 .Dt POUNCE 1 .Os . @@ -8,25 +8,29 @@ . .Sh SYNOPSIS .Nm -.Op Fl NTev -.Op Fl A Ar cert -.Op Fl C Ar cert -.Op Fl H Ar host -.Op Fl K Ar priv -.Op Fl P Ar port +.Op Fl LNTev +.Op Fl A Ar local-ca +.Op Fl C Ar local-cert +.Op Fl H Ar local-host +.Op Fl K Ar local-priv +.Op Fl P Ar local-port +.Op Fl Q Ar queue-interval +.Op Fl R Ar blind-req .Op Fl S Ar bind -.Op Fl U Ar unix -.Op Fl W Ar pass -.Op Fl a Ar auth -.Op Fl c Ar cert +.Op Fl U Ar local-path +.Op Fl W Ar local-pass +.Op Fl a Ar sasl-plain +.Op Fl c Ar client-cert .Op Fl f Ar save .Op Fl h Ar host .Op Fl j Ar join -.Op Fl k Ar priv +.Op Fl k Ar client-priv +.Op Fl m Ar mode .Op Fl n Ar nick .Op Fl p Ar port .Op Fl q Ar quit .Op Fl r Ar real +.Op Fl t Ar trust .Op Fl s Ar size .Op Fl u Ar user .Op Fl w Ar pass @@ -34,6 +38,13 @@ .Op Ar config ... . .Nm +.Fl o +.Op Fl S Ar bind +.Op Fl h Ar host +.Op Fl p Ar port +.Op Ar config ... +. +.Nm .Op Fl A Ar ca .Fl g Ar cert . @@ -43,23 +54,79 @@ .Sh DESCRIPTION The .Nm -daemon +program is a multi-client, TLS-only IRC bouncer. It maintains a persistent connection to an IRC server while allowing clients to connect and disconnect, receiving messages that were missed upon reconnection. -Clients should use the IRCv3.2 +Clients must uniquely identify themselves to +.Nm +by their IRC username +(not nickname). +The IRCv3 .Sy server-time -extension -to know when missed messages were received -and uniquely identify themselves by username. +extension is used to indicate +when messages were originally received. See .Sx Client Configuration for details. . .Pp +The local server portion of +.Nm +requires a TLS certificate, +which can be obtained for example +by an ACME client such as +.Xr acme-client 8 . +The private key +must be made readable by +the user running +.Nm . +. +.Pp +One instance of +.Nm +must be configured for each IRC network. +Instances of +.Nm +must either use different local ports with +.Cm local-port +or different local host names with +.Cm local-host +and +.Cm local-path +to be dispatched from the same port by +.Xr calico 1 . +. +.Pp +Client connections are not accepted +until successful login to the server. +If the server connection is lost, +the +.Nm +process exits. +. +.Pp Options can be loaded from files listed on the command line. +Files are searched for in +.Pa $XDG_CONFIG_DIRS/pounce +.Po +usually +.Pa ~/.config/pounce +.Pc +unless the path starts with +.Ql / , +.Ql \&./ +or +.Ql \&../ . +Certificate and private key paths +are searched for in the same manner. +Files and flags +listed later on the command line +take precedence over those listed earlier. +. +.Pp Each option is placed on a line, and lines beginning with .Ql # @@ -67,75 +134,80 @@ are ignored. The options are listed below following their corresponding flags. . -.Pp -The arguments are as follows: -. +.Ss Local Server Options .Bl -tag -width Ds -.It Fl A Ar path , Cm local-ca = Ar path +.It Fl A Ar path | Cm local-ca No = Ar path Require clients to authenticate using a TLS client certificate -signed by the certificate authority loaded from +either contained in +or signed by a certificate in +the file loaded from .Ar path . +The file is reloaded when the +.Dv SIGUSR1 +signal is received. See .Sx Generating Client Certificates . If -.Fl W +.Cm local-pass is also set, -clients may instead connect +clients may instead authenticate with a server password. . -.It Fl C Ar path , Cm local-cert = Ar path +.It Fl C Ar path | Cm local-cert No = Ar path Load TLS certificate from .Ar path . -The default path is the -.Xr certbot 8 -path for the +The file is reloaded when the +.Dv SIGUSR1 +signal is received. +The default path is +.Ar host Ns .pem , +where .Ar host -set by -.Fl H . +is set by +.Cm local-host . . -.It Fl H Ar host , Cm local-host = Ar host +.It Fl H Ar host | Cm local-host No = Ar host Bind to .Ar host . The default host is localhost. . -.It Fl K Ar path , Cm local-priv = Ar path +.It Fl K Ar path | Cm local-priv No = Ar path Load TLS private key from .Ar path . -The default path is the -.Xr certbot 8 -path for the +The file is reloaded when the +.Dv SIGUSR1 +signal is received. +The default path is +.Ar host Ns .key , +where .Ar host -set by -.Fl H . -. -.It Fl N , Cm no-names -Do not request -.Ql NAMES -for each channel when a client connects. -This avoids already connected clients -receiving unsolicited responses -but prevents new clients from populating user lists. -. -.It Fl P Ar port , Cm local-port = Ar port +is set by +.Cm local-host . +. +.It Fl L | Cm palaver +Advertise the +.Sy palaverapp.com +IRCv3 vendor-specific capability to clients. +This option only enables the capability; +push notifications must be provided by the +.Xr pounce-palaver 1 +special-purpose client. +. +.It Fl P Ar port | Cm local-port No = Ar port Bind to .Ar port . The default port is 6697. . -.It Fl S Ar host , Cm bind = Ar host -Bind to source address -.Ar host -when connecting to the server. -. -.It Fl T +.It Fl T | Cm no-sts Do not advertise a strict transport security (STS) policy to clients. . -.It Fl U Ar path , Cm local-path = Ar path +.It Fl U Ar path | Cm local-path No = Ar path Bind to a UNIX-domain socket at .Ar path . -Clients are accepted as sent by +Clients are only accepted as dispatched by .Xr calico 1 . If .Ar path @@ -143,14 +215,14 @@ is a directory, the .Ar host set by -.Fl H +.Cm local-host is appended to it. This option takes precedence over -.Fl H +.Cm local-host and -.Fl P . +.Cm local-port . . -.It Fl W Ar pass , Cm local-pass = Ar pass +.It Fl W Ar pass | Cm local-pass No = Ar pass Require the server password .Ar pass for clients to connect. @@ -159,12 +231,80 @@ The string must be hashed using .Fl x . If -.Fl A +.Cm local-ca is also set, -clients may instead connect +clients may instead authenticate using a TLS client certificate. . -.It Fl a Ar user : Ns Ar pass , Cm sasl-plain = Ar user : Ns Ar pass +.It Fl f Ar path | Cm save No = Ar path +Save and load the contents of the buffer from +.Ar path +in +.Pa $XDG_DATA_DIRS/pounce , +usually +.Pa ~/.local/share/pounce , +or an absolute or relative path if +.Ar path +starts with +.Ql / , +.Ql \&./ +or +.Ql \&../ . +The file is truncated after loading. +. +.It Fl s Ar size | Cm size No = Ar size +Set the number of messages contained in the buffer to +.Ar size . +This sets the maximum number +of recent messages +which can be relayed +to a reconnecting client. +The size must be a power of two. +The default size is 4096. +.El +. +.Ss Remote Server Options +.Bl -tag -width Ds +.It Fl N | Cm no-names +Do not request +.Ql NAMES +for each channel when a client connects. +This avoids already connected clients +receiving unsolicited responses +but prevents new clients from populating user lists. +. +.It Fl Q Ar ms | Cm queue-interval No = Ar ms +Set the server send queue interval in milliseconds. +The queue is used +to send automated messages from +.Nm +to the server. +Messages from clients +are sent to the server directly. +The default interval is 200 milliseconds. +. +.It Fl R Ar caps | Cm blind-req No = Ar caps +Blindly request the IRCv3 capabilities +.Ar caps , +which must be supported by +.Nm . +This can be used to enable hidden capabilities, +such as +.Sy userhost-in-names +on some networks. +. +.It Fl S Ar host | Cm bind No = Ar host +Bind to source address +.Ar host +when connecting to the server. +To connect from any address +over IPv4 only, +use 0.0.0.0. +To connect from any address +over IPv6 only, +use ::. +. +.It Fl a Ar user : Ns Ar pass | Cm sasl-plain No = Ar user : Ns Ar pass Authenticate as .Ar user with @@ -172,147 +312,148 @@ with using SASL PLAIN. Since this method requires the account password in plaintext, -it is recommended to use SASL EXTERNAL instead with -.Fl e . +it is recommended to use CertFP instead with +.Cm sasl-external . . -.It Fl c Ar path , Cm client-cert = Ar path +.It Fl c Ar path | Cm client-cert No = Ar path Load the TLS client certificate from .Ar path . If the private key is in a separate file, it is loaded with -.Fl k . +.Cm client-priv . With -.Fl e , +.Cm sasl-external , authenticate using SASL EXTERNAL. Certificates can be generated with .Fl g . . -.It Fl e , Cm sasl-external +.It Fl e | Cm sasl-external Authenticate using SASL EXTERNAL, also known as CertFP. The TLS client certificate is loaded with -.Fl c . -For more information, see +.Cm client-cert . +See .Sx Configuring CertFP . . -.It Fl f Ar path , Cm save = Ar path -Load the contents of the buffer from -.Ar path , -if it exists, -and truncate it. -On shutdown, -save the contents of the buffer to -.Ar path . -. -.It Fl g Ar path -Generate a TLS client certificate using -.Xr openssl 1 -and write it to -.Ar path . -The certificate is signed -by the certificate authority if -.Fl A -is set, -otherwise it is self-signed. -. -.It Fl h Ar host , Cm host = Ar host +.It Fl h Ar host | Cm host No = Ar host Connect to .Ar host . . -.It Fl j Ar chan , Cm join = Ar chan +.It Fl j Ar channels Oo Ar keys Oc | Cm join No = Ar channels Op Ar keys Join the comma-separated list of -.Ar chan . +.Ar channels +with the optional comma-separated list of channel +.Ar keys . . -.It Fl k Ar path , Cm client-priv = Ar path +.It Fl k Ar path | Cm client-priv No = Ar path Load the TLS client private key from .Ar path . . -.It Fl n Ar nick , Cm nick = Ar nick +.It Fl m Ar mode | Cm mode No = Ar mode +Set the user +.Ar mode . +. +.It Fl n Ar nick | Cm nick No = Ar nick Set nickname to .Ar nick . The default nickname is the user's name. . -.It Fl p Ar port , Cm port = Ar port +.It Fl p Ar port | Cm port No = Ar port Connect to .Ar port . The default port is 6697. . -.It Fl q Ar mesg , Cm quit = Ar mesg +.It Fl q Ar mesg | Cm quit No = Ar mesg Quit with message .Ar mesg when shutting down. . -.It Fl r Ar real , Cm real = Ar real +.It Fl r Ar real | Cm real No = Ar real Set realname to .Ar real . The default realname is the same as the nickname. . -.It Fl s Ar size , Cm size = Ar size -Set the number of messages contained in the buffer to -.Ar size . -The size must be a power of two. -The default size is 4096. +.It Fl t Ar path | Cm trust No = Ar path +Trust the certificate loaded from +.Ar path . +Server name verification is disabled. +See +.Sx Connecting to Servers with Self-signed Certificates . . -.It Fl u Ar user , Cm user = Ar user +.It Fl u Ar user | Cm user No = Ar user Set username to .Ar user . The default username is the same as the nickname. . -.It Fl v , Cm verbose -Write IRC messages to standard error -in red to the server, -green from the server, -yellow from clients -and blue to clients. -. -.It Fl w Ar pass , Cm pass = Ar pass +.It Fl w Ar pass | Cm pass No = Ar pass Log in with the server password .Ar pass . . -.It Fl x -Prompt for a password -and output a hash -for use with -.Fl W . -. -.It Fl y Ar mesg , Cm away = Ar mesg +.It Fl y Ar mesg | Cm away No = Ar mesg Set away status to .Ar mesg -when no clients are connected. +when no clients are connected +and no other away status has been set. .El . +.Ss Other Options +.Bl -tag -width Ds +.It Fl g Ar path +Generate a TLS client certificate using +.Xr openssl 1 +and write it to +.Ar path . +The certificate is signed +by the certificate authority if +.Fl A +is set, +otherwise it is self-signed. +. +.It Fl o +Print the server certificate chain +to standard output in PEM format +and exit. +. +.It Fl v | Cm verbose +Log IRC messages to standard output: .Pp -Client connections are not accepted -until successful login to the server. -If the server connection is lost, -the +.Bl -tag -width "<<" -compact +.It << +from +.Nm +to the server +.It >> +from the server to +.Nm +.It -> +from clients to +.Nm +.It <- +from .Nm -daemon exits. +to clients +.El . -.Pp -Upon receiving the -.Dv SIGUSR1 -signal, -the certificate and private key -will be reloaded from the paths -specified by -.Fl C -and -.Fl K . +.It Fl x +Prompt for a password +and output a hash +for use with +.Cm local-pass . +.El . .Ss Client Configuration Clients should be configured to connect to the host and port set by -.Fl H +.Cm local-host and -.Fl P , +.Cm local-port , with TLS or SSL enabled. If -.Fl W +.Cm local-pass is used, clients must send a server password. If -.Fl A +.Cm local-ca is used, clients must connect with a client certificate and may request SASL EXTERNAL. @@ -320,7 +461,7 @@ If both are used, clients may authenticate with either method. . .Pp -Clients should register with unique usernames, +Clients must register with unique usernames (not nicknames), for example the name of the client software or location from which it is connecting. New clients with the same username @@ -331,13 +472,33 @@ The nickname and real name sent by clients are ignored. . .Pp +Normally a client sending +.Ic QUIT +will simply be disconnected from +.Nm . +If, however, +the quit message +starts with the keyword +.Sy $pounce , +.Nm +itself will quit. +The remainder of the message +following the keyword +will be used as +.Nm Ap s +quit message, +or the default set by +.Cm quit +if there isn't any. +. +.Pp Clients which request the .Sy causal.agency/passive capability or with usernames beginning with hyphen .Ql - are considered passive -and do not affect away status. +and do not affect automatic away status. . .Pp Pass-through of the following IRCv3 capabilities @@ -348,7 +509,9 @@ is supported: .Sy batch , .Sy cap-notify , .Sy chghost , +.Sy echo-message , .Sy extended-join , +.Sy extended-monitor , .Sy invite-notify , .Sy labeled-response , .Sy message-tags , @@ -366,23 +529,54 @@ not to the server. .Ss Generating Client Certificates .Bl -enum .It -Generate a self-signed certificate authority (CA): +Generate self-signed client certificates and private keys: +.Bd -literal -offset indent +$ pounce -g client1.pem +$ pounce -g client2.pem +.Ed +.It +Concatenate the certificate public keys into a CA file: +.Bd -literal -offset indent +$ openssl x509 -subject -in client1.pem \e + >> ~/.config/pounce/auth.pem +$ openssl x509 -subject -in client2.pem \e + >> ~/.config/pounce/auth.pem +.Ed +.It +Configure +.Nm +to verify client certificates +against the CA file: +.Bd -literal -offset indent +local-ca = auth.pem +# or: $ pounce -A auth.pem +.Ed +.El +. +.Pp +Alternatively, +client certificates can be signed +by a generated certificate authority: +. +.Bl -enum +.It +Generate a self-signed certificate authority: .Bd -literal -offset indent -pounce -g auth.pem +$ pounce -g auth.pem .Ed .It Generate and sign client certificates using the CA: .Bd -literal -offset indent -pounce -A auth.pem -g client1.pem -pounce -A auth.pem -g client2.pem +$ pounce -A auth.pem -g client1.pem +$ pounce -A auth.pem -g client2.pem .Ed .It Since only the public key is needed for certificate verification, extract it from the CA: .Bd -literal -offset indent -openssl x509 -in auth.pem -out auth.crt +$ openssl x509 -in auth.pem -out ~/.config/pounce/auth.crt .Ed .It Configure @@ -391,7 +585,7 @@ to verify client certificates against the CA: .Bd -literal -offset indent local-ca = auth.crt -# or: pounce -A auth.crt +# or: $ pounce -A auth.crt .Ed .El . @@ -400,13 +594,13 @@ local-ca = auth.crt .It Generate a new TLS client certificate: .Bd -literal -offset indent -pounce -g example.pem +$ pounce -g ~/.config/pounce/example.pem .Ed .It Connect to the server using the certificate: .Bd -literal -offset indent client-cert = example.pem -# or: pounce -c example.pem +# or: $ pounce -c example.pem .Ed .It Identify with services or use @@ -421,68 +615,27 @@ to require successful authentication when connecting: .Bd -literal -offset indent client-cert = example.pem sasl-external -# or: pounce -e -c example.pem +# or: $ pounce -e -c example.pem .Ed .El . -.Ss Service Configuration -Add the following to -.Pa /etc/rc.conf -to enable the -.Nm -daemon: -.Bd -literal -offset indent -pounce_enable="YES" -.Ed -. -.Pp -By default, -the -.Nm -daemon is started in the -.Pa /usr/local/etc/pounce -directory. -Configuration files in that location -can be loaded by setting -.Va pounce_flags : +.Ss Connecting to Servers with Self-signed Certificates +.Bl -enum +.It +Connect to the server +and write its certificate to a file: .Bd -literal -offset indent -pounce_flags="example.conf" +$ pounce -o -h irc.example.org > ~/.config/pounce/example.pem .Ed -. -.Pp -The +.It +Configure .Nm -service supports profiles -for running multiple instances. -Set -.Va pounce_profiles -to a space-separated list of names. -Flags for each profile will be set from -.Va pounce_${profile}_flags . -For example: +to trust the certificate: .Bd -literal -offset indent -pounce_profiles="example1 example2" -pounce_example1_flags="example1.conf" -pounce_example2_flags="example2.conf" +trust = example.pem +# or: $ pounce -t example.pem .Ed -. -.Pp -The commands -.Cm start , stop , -etc.\& -will operate on the profile given as an additional argument, -or on all profiles without an additional argument. -. -.Pp -The -.Cm reload -command will cause the -.Nm -daemon to reload certificate files. -To reload other configuration, -use the -.Cm restart -command. +.El . .Sh ENVIRONMENT .Bl -tag -width Ds @@ -490,94 +643,203 @@ command. The default nickname. .El . +.Sh FILES +.Bl -tag -width Ds +.It Pa $XDG_CONFIG_DIRS/pounce +Configuration files, certificates and private keys +are searched for first in +.Ev $XDG_CONFIG_HOME , +usually +.Pa ~/.config , +followed by the colon-separated list of paths +.Ev $XDG_CONFIG_DIRS , +usually +.Pa /etc/xdg . +.It Pa ~/.config/pounce +The most likely location of configuration files. +. +.It Pa $XDG_DATA_DIRS/pounce +Save files are searched for first in +.Ev $XDG_DATA_HOME , +usually +.Pa ~/.local/share , +followed by the colon-separated list of paths +.Ev $XDG_DATA_DIRS , +usually +.Pa /usr/local/share:/usr/share . +New save files are created in +.Ev $XDG_DATA_HOME . +.It Pa ~/.local/share/pounce +The most likely location of save files. +.El +. .Sh EXAMPLES -Configuration on the command line: +Start +.Nm : .Bd -literal -offset indent -pounce -H pounce.example.org -h chat.freenode.net -j '#ascii.town' +$ pounce -H irc.example.org -h irc.tilde.chat -j '#ascii.town' .Ed -. .Pp -Configuration in a file: +Write an equivalent configuration file to +.Pa ~/.config/pounce/tilde.conf : .Bd -literal -offset indent -local-host = pounce.example.org -host = chat.freenode.net +local-host = irc.example.org +host = irc.tilde.chat join = #ascii.town .Ed +.Pp +Load the configuration file: +.Bd -literal -offset indent +$ pounce tilde.conf +.Ed +. +.Pp +Add a certificate to +.Xr acme-client.conf 5 : +.Bd -literal -offset indent +domain irc.example.org { + domain key "/home/user/.config/pounce/irc.example.org.key" + domain full chain certificate \e + "/home/user/.config/pounce/irc.example.org.pem" + sign with letsencrypt +} +.Ed +.Pp +Obtain the certificate +and make the private key readable by +.Nm : +.Bd -literal -offset indent +# acme-client irc.example.org +# chown user /home/user/.config/pounce/irc.example.org.key +.Ed +.Pp +Renew and reload the certificate with a +.Xr cron 8 +job: +.Bd -literal -offset indent +~ * * * * acme-client irc.example.org && pkill -USR1 pounce +.Ed +. +.Sh DIAGNOSTICS +Upon receiving the +.Dv SIGINFO +signal, +.Nm +prints the current producer position +and the positions of each consumer +identified by username. +Following each consumer position +is the number by which it trails the producer. +On systems lacking +.Dv SIGINFO , +.Dv SIGUSR2 +is used. +. +.Pp +If a client reconnects +after having missed more messages +than the size of the buffer, +.Nm +will print a warning: +.Bd -ragged -offset indent +consumer +.Em name +dropped +.Em n +messages +.Ed +.Pp +The size of the buffer +can be adjusted with +.Fl s . . .Sh SEE ALSO .Xr calico 1 . .Sh STANDARDS -The -.Nm -daemon implements the following: -. .Bl -item .It .Rs -.%A Attila Molnar +.%A Waldo Bastian +.%A Ryan Lortie +.%A Lennart Poettering +.%T XDG Base Directory Specification +.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +.%D November 24, 2010 +.Re +.It +.Rs +.%A Kyle Fuller +.%A St\('ephan Kochen +.%A Alexey Sokolov .%A James Wheare -.%T IRCv3 Strict Transport Security +.%T server-time Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/sts +.%U https://ircv3.net/specs/extensions/server-time .Re .It .Rs +.%A Lee Hardy +.%A Perry Lorier +.%A Kevin L. Mitchell .%A Attila Molnar +.%A Daniel Oakley .%A William Pitcock -.%T IRCv3.2 SASL Authentication +.%A James Wheare +.%T IRCv3 Client Capability Negotiation .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/sasl-3.2 +.%U https://ircv3.net/specs/core/capability-negotiation .Re .It .Rs -.%A C. Kalt -.%T Internet Relay Chat: Client Protocol +.%A S. Josefsson +.%T The Base16, Base32, and Base64 Data Encodings .%I IETF -.%N RFC 2812 -.%D April 2000 -.%U https://tools.ietf.org/html/rfc2812 +.%R RFC 4648 +.%U https://tools.ietf.org/html/rfc4648 +.%D October 2006 .Re .It .Rs -.%A K. Zeilenga, Ed. -.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism +.%A C. Kalt +.%T Internet Relay Chat: Client Protocol .%I IETF -.%N RFC 4616 -.%D August 2006 -.%U https://tools.ietf.org/html/rfc4616 +.%R RFC 2812 +.%U https://tools.ietf.org/html/rfc2812 +.%D April 2000 .Re .It .Rs -.%A Kevin L. Mitchell -.%A Perry Lorier -.%A Lee Hardy -.%A William Pitcock .%A Attila Molnar -.%A Daniel Oakley .%A James Wheare -.%T IRCv3 Client Capability Negotiation +.%T IRCv3 Strict Transport Security .%I IRCv3 Working Group -.%U https://ircv3.net/specs/core/capability-negotiation +.%U https://ircv3.net/specs/extensions/sts .Re .It .Rs -.%A S. Josefsson -.%T The Base16, Base32, and Base64 Data Encodings -.%I IETF -.%N RFC 4648 -.%D October 2006 -.%U https://tools.ietf.org/html/rfc4648 +.%A Attila Molnar +.%A William Pitcock +.%T IRCv3.2 SASL Authentication +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/sasl-3.2 .Re .It .Rs -.%A St\('ephan Kochen -.%A Alexey Sokolov -.%A Kyle Fuller -.%A James Wheare -.%T IRCv3.2 server-time Extension +.%A Simon Ser +.%A delthas +.%T Read marker .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/server-time-3.2 +.%U https://ircv3.net/specs/extensions/read-marker +.Re +.It +.Rs +.%A K. Zeilenga, Ed. +.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism +.%I IETF +.%R RFC 4616 +.%U https://tools.ietf.org/html/rfc4616 +.%D August 2006 .Re .El . @@ -618,7 +880,7 @@ indicate if capabilities MUST NOT have values. The .Nm -daemon parses +implementation parses .Ql CAP REQ values in the same way as .Ql CAP LS @@ -632,36 +894,15 @@ indicates that a client should not affect the automatic away status. . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency -. -.Sh CAVEATS -One instance of -.Nm , -and therefore one local port, -is required for each server connection. -Alternatively, -the -.Xr calico 1 -daemon can be used to dispatch from one local port -to many instances of -.Nm -using Server Name Indication. -. -.Pp -The -.Nm -daemon makes no distinction between channels. -Elevated activity in one channel -may push messages from a quieter channel -out of the buffer. +.An June McEnroe Aq Mt june@causal.agency . .Sh BUGS Send mail to -.Aq Mt june@causal.agency +.Aq Mt list+pounce@causal.agency or join .Li #ascii.town on -.Li chat.freenode.net . +.Li irc.tilde.chat . . .Pp A client will sometimes receive its own message, diff --git a/rc.d/calico b/rc.d/calico deleted file mode 100644 index 32c21d8..0000000 --- a/rc.d/calico +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# PROVIDE: calico -# REQUIRE: LOGIN -# KEYWORD: shutdown - -. /etc/rc.subr - -name='calico' -rcvar='calico_enable' - -load_rc_config "${name}" - -: ${calico_enable:='NO'} -: ${calico_path:="/var/run/${name}"} -calico_flags="${calico_flags} ${calico_path}" - -command='/usr/sbin/daemon' -procname='/usr/local/bin/calico' -pidfile="/var/run/${name}.pid" -required_dirs=$calico_path - -child_flags=$calico_flags -child_user=$calico_user -unset calico_flags calico_user -command_args="\ - -p ${pidfile} -T ${name} ${child_user:+-u ${child_user}} \ - -- ${procname} ${child_flags}" - -run_rc_command "$1" diff --git a/rc.d/pounce b/rc.d/pounce deleted file mode 100644 index 9777fda..0000000 --- a/rc.d/pounce +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/sh - -# PROVIDE: pounce -# REQUIRE: LOGIN -# KEYWORD: shutdown - -. /etc/rc.subr - -name='pounce' -rcvar='pounce_enable' -extra_commands='reload' -sig_reload='USR1' - -load_rc_config "${name}" - -: ${pounce_enable:='NO'} -: ${pounce_chdir:="/usr/local/etc/${name}"} - -command='/usr/sbin/daemon' -pidprefix="/var/run/${name}" -pidfile="${pidprefix}.pid" -required_dirs=$pounce_chdir - -child_command='/usr/local/bin/pounce' -child_pidfile="${pidprefix}.child.pid" - -if [ -n "$2" ]; then - profile=$2 - if [ -n "${pounce_profiles}" ]; then - pidfile="${pidprefix}.${profile}.pid" - child_pidfile="${pidprefix}.${profile}.child.pid" - eval pounce_enable="\${pounce_${profile}_enable:-${pounce_enable}}" - eval pounce_flags="\${pounce_${profile}_flags:-${pounce_flags}}" - eval pounce_chdir="\${pounce_${profile}_chdir:-${pounce_chdir}}" - eval pounce_user="\${pounce_${profile}_user:-${pounce_user}}" - else - echo "$0: extra argument ignored" - fi -else - if [ -n "${pounce_profiles}" -a -n "$1" ]; then - for profile in ${pounce_profiles}; do - echo "===> ${name} profile: ${profile}" - /usr/local/etc/rc.d/${name} "$1" "${profile}" || exit "$?" - done - exit - fi -fi - -child_flags=$pounce_flags -child_user=$pounce_user -unset pounce_flags pounce_user -command_args="\ - -r -P ${pidfile} -p ${child_pidfile} -T ${name}${profile:+/${profile}} \ - ${child_user:+-u ${child_user}} \ - -- ${child_command} ${child_flags}" - -pounce_reload() { - rc_pid=$(check_pidfile "$child_pidfile" "$child_command") - kill "-$sig_reload" "$rc_pid" -} -reload_cmd='pounce_reload' - -run_rc_command "$1" diff --git a/ring.c b/ring.c index 254c285..07b66d8 100644 --- a/ring.c +++ b/ring.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,10 +12,23 @@ * * 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 <inttypes.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <sys/time.h> @@ -31,7 +44,7 @@ static struct { void ringAlloc(size_t len) { if (len & (len - 1)) { - errx(EX_CONFIG, "ring length must be power of two: %zu", len); + errx(EX_USAGE, "ring length must be power of two: %zu", len); } ring.lines = calloc(len, sizeof(*ring.lines)); if (!ring.lines) err(EX_OSERR, "calloc"); @@ -40,7 +53,7 @@ void ringAlloc(size_t len) { ring.len = len; } -size_t producer; +static size_t producer; void ringProduce(const char *line) { size_t i = producer++ & (ring.len - 1); @@ -99,10 +112,12 @@ size_t ringDiff(size_t consumer) { const char *ringPeek(struct timeval *time, size_t consumer) { if (!ringDiff(consumer)) return NULL; if (ringDiff(consumer) > ring.len) { - warnx( - "consumer %s dropped %zu messages", - consumers.ptr[consumer].name, ringDiff(consumer) - ring.len - ); + if (consumers.ptr[consumer].pos) { + warnx( + "consumer %s dropped %zu messages", + consumers.ptr[consumer].name, ringDiff(consumer) - ring.len + ); + } consumers.ptr[consumer].pos = producer - ring.len; } size_t i = consumers.ptr[consumer].pos & (ring.len - 1); @@ -128,17 +143,17 @@ void ringInfo(void) { } } -static const size_t Signatures[] = { +static const uint64_t Signatures[] = { 0x0165636E756F70, // no ring size 0x0265636E756F70, // time_t only 0x0365636E756F70, }; -static size_t signatureVersion(size_t signature) { +static size_t signatureVersion(uint64_t signature) { for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) { if (signature == Signatures[i]) return i; } - errx(EX_DATAERR, "unknown file signature %zX", signature); + errx(EX_DATAERR, "unknown file signature %" PRIX64, signature); } static int writeSize(FILE *file, size_t value) { @@ -152,7 +167,7 @@ static int writeString(FILE *file, const char *str) { } int ringSave(FILE *file) { - if (writeSize(file, Signatures[2])) return -1; + if (!fwrite(&Signatures[2], sizeof(*Signatures), 1, file)) return -1; if (writeSize(file, ring.len)) return -1; if (writeSize(file, producer)) return -1; if (writeSize(file, consumers.len)) return -1; @@ -191,7 +206,7 @@ static void readString(FILE *file, char **buf, size_t *cap) { } void ringLoad(FILE *file) { - size_t signature; + uint64_t signature; fread(&signature, sizeof(signature), 1, file); if (ferror(file)) err(EX_IOERR, "fread"); if (feof(file)) return; diff --git a/server.c b/server.c index 20d94e3..9d7be14 100644 --- a/server.c +++ b/server.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,10 +12,22 @@ * * 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 <limits.h> #include <netdb.h> #include <netinet/in.h> #include <stdarg.h> @@ -30,29 +42,47 @@ #include "bounce.h" static struct tls *client; +static struct tls_config *config; -void serverConfig(bool insecure, const char *cert, const char *priv) { - struct tls_config *config = tls_config_new(); +void serverConfig( + bool insecure, const char *trust, const char *cert, const char *priv +) { + int error = 0; + char buf[PATH_MAX]; + config = tls_config_new(); if (!config) errx(EX_SOFTWARE, "tls_config_new"); - int error = tls_config_set_ciphers(config, "compat"); - if (error) { - errx(EX_SOFTWARE, "tls_config_set_ciphers: %s", tls_config_error(config)); - } - if (insecure) { tls_config_insecure_noverifycert(config); tls_config_insecure_noverifyname(config); } + if (trust) { + tls_config_insecure_noverifyname(config); + for (int i = 0; configPath(buf, sizeof(buf), trust, i); ++i) { + error = tls_config_set_ca_file(config, buf); + if (!error) break; + } + if (error) errx(EX_NOINPUT, "%s: %s", trust, tls_config_error(config)); + } + if (cert) { - error = tls_config_set_keypair_file(config, cert, (priv ? priv : cert)); - if (error) { - errx( - EX_SOFTWARE, "tls_config_set_keypair_file: %s", - tls_config_error(config) - ); + for (int i = 0; configPath(buf, sizeof(buf), cert, i); ++i) { + if (priv) { + error = tls_config_set_cert_file(config, buf); + } else { + error = tls_config_set_keypair_file(config, buf, buf); + } + if (!error) break; + } + if (error) errx(EX_NOINPUT, "%s: %s", cert, tls_config_error(config)); + } + if (priv) { + for (int i = 0; configPath(buf, sizeof(buf), priv, i); ++i) { + error = tls_config_set_key_file(config, buf); + if (!error) break; } + if (error) errx(EX_NOINPUT, "%s: %s", priv, tls_config_error(config)); } client = tls_client(); @@ -60,7 +90,6 @@ void serverConfig(bool insecure, const char *cert, const char *priv) { error = tls_configure(client, config); if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); - tls_config_free(config); } int serverConnect(const char *bindHost, const char *host, const char *port) { @@ -114,21 +143,32 @@ int serverConnect(const char *bindHost, const char *host, const char *port) { if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port); freeaddrinfo(head); - int yes = 1; - error = setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes)); - if (error) err(EX_OSERR, "setsockopt"); - error = tls_connect_socket(client, sock, host); if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client)); - error = tls_handshake(client); + do { + error = tls_handshake(client); + } while (error == TLS_WANT_POLLIN || error == TLS_WANT_POLLOUT); if (error) errx(EX_PROTOCOL, "tls_handshake: %s", tls_error(client)); + tls_config_clear_keys(config); return sock; } +void serverClose(void) { + tls_close(client); + tls_free(client); +} + +void serverPrintCert(void) { + size_t len; + const byte *pem = tls_peer_cert_chain_pem(client, &len); + printf("subject= %s\n", tls_peer_cert_subject(client)); + fwrite(pem, len, 1, stdout); +} + void serverSend(const char *ptr, size_t len) { - if (verbose) fprintf(stderr, "\x1B[31m%.*s\x1B[m", (int)len, ptr); + verboseLog("<<", ptr, len); while (len) { ssize_t ret = tls_write(client, ptr, len); if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; @@ -148,6 +188,46 @@ void serverFormat(const char *format, ...) { serverSend(buf, len); } +enum { QueueCap = 256 }; +static struct { + size_t enq; + size_t deq; + char *msgs[QueueCap]; +} queue; + +void serverDequeue(void) { + if (queue.enq - queue.deq) { + char *msg = queue.msgs[queue.deq++ % QueueCap]; + serverSend(msg, strlen(msg)); + free(msg); + } else { + struct itimerval timer = { .it_value = {0} }; + int error = setitimer(ITIMER_REAL, &timer, NULL); + if (error) err(EX_OSERR, "setitimer"); + } +} + +struct timeval serverQueueInterval = { .tv_usec = 1000 * 200 }; + +void serverEnqueue(const char *format, ...) { + if (queue.enq - queue.deq == QueueCap) { + warnx("server send queue full"); + serverDequeue(); + } else if (queue.enq == queue.deq) { + struct itimerval timer = { + .it_interval = serverQueueInterval, + .it_value = { .tv_usec = 1 }, + }; + int error = setitimer(ITIMER_REAL, &timer, NULL); + if (error) err(EX_OSERR, "setitimer"); + } + va_list ap; + va_start(ap, format); + int len = vasprintf(&queue.msgs[queue.enq++ % QueueCap], format, ap); + va_end(ap); + if (len < 0) err(EX_OSERR, "vasprintf"); +} + void serverRecv(void) { static char buf[MessageCap]; static size_t len; @@ -164,7 +244,7 @@ void serverRecv(void) { crlf = memmem(line, &buf[len] - line, "\r\n", 2); if (!crlf) break; crlf[0] = '\0'; - if (verbose) fprintf(stderr, "\x1B[32m%s\x1B[m\n", line); + verboseLog(">>", line, crlf - line); const char *ping = line; if (ping[0] == '@') { ping += strcspn(ping, " "); diff --git a/state.c b/state.c index 5da5bcc..a28b3ba 100644 --- a/state.c +++ b/state.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,6 +12,17 @@ * * 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> @@ -27,6 +38,8 @@ bool stateNoNames; enum Cap stateCaps; +char *stateAccount; +bool stateAway; typedef void Handler(struct Message *msg); @@ -45,31 +58,37 @@ enum { AuthLen = 299 }; static char plainBase64[BASE64_SIZE(AuthLen)]; void stateLogin( - const char *pass, bool sasl, const char *plain, + const char *pass, enum Cap blind, const char *plain, const char *nick, const char *user, const char *real ) { + if (plain) { + byte buf[AuthLen] = {0}; + size_t len = 1 + strlen(plain); + if (len > sizeof(buf)) errx(EX_USAGE, "SASL PLAIN too long"); + memcpy(&buf[1], plain, len - 1); + byte *sep = memchr(buf, ':', len); + if (!sep) errx(EX_USAGE, "SASL PLAIN missing colon"); + *sep = 0; + base64(plainBase64, buf, len); + explicit_bzero(buf, len); + } + serverFormat("CAP LS 302\r\n"); if (pass) serverFormat("PASS :%s\r\n", pass); - if (sasl) { - serverFormat("CAP REQ :%s\r\n", capList(CapSASL, NULL)); - if (plain) { - byte buf[AuthLen]; - size_t len = 1 + strlen(plain); - if (sizeof(buf) < len) { - errx(EX_SOFTWARE, "SASL PLAIN is too long"); - } - buf[0] = 0; - for (size_t i = 0; plain[i]; ++i) { - buf[1 + i] = (plain[i] == ':' ? 0 : plain[i]); - } - base64(plainBase64, buf, len); - explicit_bzero(buf, sizeof(buf)); - } - } + if (blind) serverFormat("CAP REQ :%s\r\n", capList(blind, NULL)); serverFormat("NICK %s\r\n", nick); serverFormat("USER %s 0 * :%s\r\n", user, real); } +static const enum Cap DontReq = 0 + | CapConsumer + | CapPalaverApp + | CapPassive + | CapReadMarker + | CapSASL + | CapSTS + | CapUnsupported; + static void handleCap(struct Message *msg) { require(msg, false, 3); enum Cap caps; @@ -80,8 +99,15 @@ static void handleCap(struct Message *msg) { } if (!strcmp(msg->params[1], "LS") || !strcmp(msg->params[1], "NEW")) { - caps &= ~(CapSASL | CapSTS | CapUnsupported); - if (caps) serverFormat("CAP REQ :%s\r\n", capList(caps, NULL)); + caps &= ~DontReq; + if (caps & CapEchoMessage && !(caps & CapLabeledResponse)) { + caps &= ~CapEchoMessage; + } + if (caps) { + serverFormat("CAP REQ :%s\r\n", capList(caps, NULL)); + } else { + if (!(stateCaps & CapSASL)) serverFormat("CAP END\r\n"); + } } else if (!strcmp(msg->params[1], "ACK")) { stateCaps |= caps; @@ -111,7 +137,8 @@ static void handleAuthenticate(struct Message *msg) { } static void handleReplyLoggedIn(struct Message *msg) { - (void)msg; + require(msg, false, 3); + set(&stateAccount, msg->params[2]); serverFormat("CAP END\r\n"); } @@ -150,12 +177,6 @@ bool stateReady(void) { && intro.myInfo[0]; } -static void set(char **field, const char *value) { - if (*field) free(*field); - *field = strdup(value); - if (!*field) err(EX_OSERR, "strdup"); -} - static void handleErrorNicknameInUse(struct Message *msg) { if (self.nick) return; require(msg, false, 2); @@ -189,6 +210,7 @@ static void handleReplyMyInfo(struct Message *msg) { } static struct { + bool done; char **tokens; size_t cap, len; } support; @@ -206,12 +228,18 @@ static void supportAdd(const char *token) { static void handleReplyISupport(struct Message *msg) { require(msg, false, 1); + if (support.done) return; for (size_t i = 1; i < ParamCap; ++i) { if (!msg->params[i] || strchr(msg->params[i], ' ')) break; supportAdd(msg->params[i]); } } +static void handleReplyMOTDStart(struct Message *msg) { + (void)msg; + support.done = true; +} + struct Channel { char *name; char *topic; @@ -258,9 +286,9 @@ static bool originSelf(const char *origin) { size_t len = strlen(self.nick); if (strlen(origin) < len) return false; if (strncmp(origin, self.nick, len)) return false; - if (origin[len] != '!') return false; + if (origin[len] && origin[len] != '!') return false; - if (!self.origin || strcmp(self.origin, origin)) { + if (origin[len] && (!self.origin || strcmp(self.origin, origin))) { set(&self.origin, origin); } return true; @@ -271,6 +299,7 @@ static void handleNick(struct Message *msg) { if (!originSelf(msg->origin)) return; set(&self.nick, msg->params[0]); + if (!self.origin) return; char *rest = strchr(self.origin, '!'); assert(rest); size_t size = strlen(self.nick) + strlen(rest) + 1; @@ -308,6 +337,16 @@ static void handleReplyTopic(struct Message *msg) { chanTopic(msg->params[1], msg->params[2]); } +static void handleReplyUnaway(struct Message *msg) { + (void)msg; + stateAway = false; +} + +static void handleReplyNowAway(struct Message *msg) { + (void)msg; + stateAway = true; +} + static void handleError(struct Message *msg) { require(msg, false, 1); errx(EX_UNAVAILABLE, "%s", msg->params[0]); @@ -322,8 +361,13 @@ static const struct { { "003", handleReplyCreated }, { "004", handleReplyMyInfo }, { "005", handleReplyISupport }, + { "305", handleReplyUnaway }, + { "306", handleReplyNowAway }, { "332", handleReplyTopic }, + { "375", handleReplyMOTDStart }, + { "422", handleReplyMOTDStart }, { "433", handleErrorNicknameInUse }, + { "437", handleErrorNicknameInUse }, { "900", handleReplyLoggedIn }, { "904", handleErrorSASLFail }, { "905", handleErrorSASLFail }, @@ -355,18 +399,30 @@ void stateSync(struct Client *client) { client, ":%s NOTICE %s :" "pounce is GPLv3 fwee softwawe ^w^ code is avaiwable fwom %s\r\n", - ORIGIN, self.nick, SOURCE_URL + clientOrigin, self.nick, SOURCE_URL ); + if (stateAccount) { + clientFormat( + client, ":%s 900 %s %s %s :You are now logged in as %s\r\n", + clientOrigin, self.nick, stateEcho(), stateAccount, stateAccount + ); + } + clientFormat( - client, - ":%s 001 %s :%s\r\n" - ":%s 002 %s :%s\r\n" - ":%s 003 %s :%s\r\n" - ":%s 004 %s %s %s %s %s%s%s\r\n", - intro.origin, self.nick, intro.welcome, - intro.origin, self.nick, intro.yourHost, - intro.origin, self.nick, intro.created, + client, ":%s 001 %s :%s\r\n", + intro.origin, self.nick, intro.welcome + ); + clientFormat( + client, ":%s 002 %s :%s\r\n", + intro.origin, self.nick, intro.yourHost + ); + clientFormat( + client, ":%s 003 %s :%s\r\n", + intro.origin, self.nick, intro.created + ); + clientFormat( + client, ":%s 004 %s %s %s %s %s%s%s\r\n", intro.origin, self.nick, intro.myInfo[0], intro.myInfo[1], intro.myInfo[2], intro.myInfo[3], (intro.myInfo[4] ? " " : ""), (intro.myInfo[4] ? intro.myInfo[4] : "") @@ -390,16 +446,18 @@ void stateSync(struct Client *client) { ); } if (i < support.len) { - clientFormat(client, ":%s 005 %s", intro.origin, self.nick); + char buf[512], *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf(ptr, end, ":%s 005 %s", intro.origin, self.nick); for (; i < support.len; ++i) { - clientFormat(client, " %s", support.tokens[i]); + ptr = seprintf(ptr, end, " %s", support.tokens[i]); } - clientFormat(client, " :are supported by this server\r\n"); + ptr = seprintf(ptr, end, " :are supported by this server\r\n"); + clientSend(client, buf, ptr - buf); } clientFormat( client, ":%s 422 %s :MOTD File is missing\r\n", - ORIGIN, self.nick + clientOrigin, self.nick ); if (chans.len) assert(self.origin); @@ -409,10 +467,13 @@ void stateSync(struct Client *client) { if (chan->topic) { clientFormat( client, ":%s 332 %s %s :%s\r\n", - ORIGIN, self.nick, chan->name, chan->topic + clientOrigin, self.nick, chan->name, chan->topic ); } + if (client->caps & CapReadMarker) { + clientGetMarker(client, chan->name); + } if (stateNoNames) continue; - serverFormat("NAMES %s\r\n", chan->name); + serverEnqueue("NAMES %s\r\n", chan->name); } } diff --git a/xdg.c b/xdg.c new file mode 100644 index 0000000..03f8c8d --- /dev/null +++ b/xdg.c @@ -0,0 +1,131 @@ +/* Copyright (C) 2019, 2020 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * 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 <err.h> +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sysexits.h> + +#include "bounce.h" + +#define SUBDIR "pounce" + +struct Base { + const char *envHome; + const char *envDirs; + const char *defHome; + const char *defDirs; +}; + +static const struct Base Config = { + .envHome = "XDG_CONFIG_HOME", + .envDirs = "XDG_CONFIG_DIRS", + .defHome = ".config", + .defDirs = "/etc/xdg", +}; + +static const struct Base Data = { + .envHome = "XDG_DATA_HOME", + .envDirs = "XDG_DATA_DIRS", + .defHome = ".local/share", + .defDirs = "/usr/local/share:/usr/share", +}; + +static char *basePath( + struct Base base, char *buf, size_t cap, const char *path, int i +) { + if (path[strspn(path, ".")] == '/') { + if (i > 0) return NULL; + snprintf(buf, cap, "%s", path); + return buf; + } + + if (i > 0) { + const char *dirs = getenv(base.envDirs); + if (!dirs) dirs = base.defDirs; + for (; i > 1; --i) { + dirs += strcspn(dirs, ":"); + dirs += (*dirs == ':'); + } + if (!*dirs) return NULL; + snprintf( + buf, cap, "%.*s/" SUBDIR "/%s", + (int)strcspn(dirs, ":"), dirs, path + ); + return buf; + } + + const char *home = getenv("HOME"); + const char *baseHome = getenv(base.envHome); + if (baseHome) { + snprintf(buf, cap, "%s/" SUBDIR "/%s", baseHome, path); + } else if (home) { + snprintf(buf, cap, "%s/%s/" SUBDIR "/%s", home, base.defHome, path); + } else { + errx(EX_USAGE, "HOME unset"); + } + return buf; +} + +char *configPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Config, buf, cap, path, i); +} + +char *dataPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Data, buf, cap, path, i); +} + +FILE *configOpen(const char *path, const char *mode) { + char buf[PATH_MAX]; + for (int i = 0; configPath(buf, sizeof(buf), path, i); ++i) { + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) warn("%s", buf); + } + warn("%s", configPath(buf, sizeof(buf), path, 0)); + return NULL; +} + +FILE *dataOpen(const char *path, const char *mode) { + char buf[PATH_MAX]; + for (int i = 0; dataPath(buf, sizeof(buf), path, i); ++i) { + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) warn("%s", buf); + } + if (mode[0] != 'r') { + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU); + if (error && errno != EEXIST) warn("%s", buf); + } + FILE *file = fopen(dataPath(buf, sizeof(buf), path, 0), mode); + if (!file) warn("%s", buf); + return file; +} |