diff options
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | Makefile | 71 | ||||
-rw-r--r-- | QUIRKS.7 | 66 | ||||
-rw-r--r-- | README.7 | 99 | ||||
-rw-r--r-- | bounce.c | 349 | ||||
-rw-r--r-- | bounce.h | 111 | ||||
-rw-r--r-- | calico.1 | 29 | ||||
-rw-r--r-- | cert.c | 95 | ||||
-rw-r--r-- | client.c | 433 | ||||
-rw-r--r-- | config.c | 22 | ||||
-rwxr-xr-x | configure | 31 | ||||
-rw-r--r-- | contrib/palaver/.gitignore | 4 | ||||
-rw-r--r-- | contrib/palaver/Makefile | 27 | ||||
-rwxr-xr-x | contrib/palaver/configure | 45 | ||||
-rw-r--r-- | dispatch.c | 176 | ||||
-rw-r--r-- | local.c | 160 | ||||
-rw-r--r-- | notify.c | 353 | ||||
-rw-r--r-- | palaver.c (renamed from contrib/palaver/notify.c) | 191 | ||||
-rw-r--r-- | pounce-notify.1 | 115 | ||||
-rw-r--r-- | pounce-palaver.1 (renamed from contrib/palaver/pounce-palaver.1) | 16 | ||||
-rw-r--r-- | pounce.1 | 683 | ||||
-rw-r--r-- | ring.c | 28 | ||||
-rw-r--r-- | server.c | 60 | ||||
-rw-r--r-- | state.c | 128 | ||||
-rw-r--r-- | xdg.c | 103 |
25 files changed, 2040 insertions, 1361 deletions
diff --git a/.gitignore b/.gitignore index c66cc95..5249f84 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,9 @@ *.o calico config.mk -localhost.crt -localhost.key +localhost.pem pounce +pounce-edit +pounce-notify +pounce-palaver tags diff --git a/Makefile b/Makefile index 75b020d..c7c1ef7 100644 --- a/Makefile +++ b/Makefile @@ -1,53 +1,70 @@ PREFIX ?= /usr/local -MANDIR ?= ${PREFIX}/share/man +BINDIR ?= ${PREFIX}/bin +MANDIR ?= ${PREFIX}/man CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -LDLIBS = -lcrypt -ltls +LDADD.crypt = -lcrypt +LDADD.libcurl = -lcurl +LDADD.libtls = -ltls +LDADD.sqlite3 = -lsqlite3 BINS = calico pounce MANS = ${BINS:=.1} -include config.mk -OBJS += bounce.o -OBJS += cert.o -OBJS += client.o -OBJS += config.o -OBJS += local.o -OBJS += ring.o -OBJS += server.o -OBJS += state.o -OBJS += xdg.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 -o $@ +calico: ${OBJS.calico} +pounce: ${OBJS.pounce} +pounce-notify: ${OBJS.pounce-notify} +pounce-palaver: ${OBJS.pounce-palaver} -pounce: ${OBJS} - ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ +${BINS}: + ${CC} ${LDFLAGS} ${OBJS.$@} ${LDLIBS.$@} -o $@ -${OBJS}: bounce.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} - install -d ${DESTDIR}${PREFIX}/bin ${DESTDIR}${MANDIR}/man1 - install ${BINS} ${DESTDIR}${PREFIX}/bin + install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 + install ${BINS} ${DESTDIR}${BINDIR} install -m 644 ${MANS} ${DESTDIR}${MANDIR}/man1 uninstall: - rm -f ${BINS:%=${DESTDIR}${PREFIX}/bin/%} + rm -f ${BINS:%=${DESTDIR}${BINDIR}/%} rm -f ${MANS:%=${DESTDIR}${MANDIR}/man1/%} -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 +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 241627c..b15a8e2 100644 --- a/README.7 +++ b/README.7 @@ -1,4 +1,4 @@ -.Dd August 30, 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 @@ -48,22 +75,26 @@ provided by either .Lk https://git.causal.agency/libretls/about LibreTLS (for OpenSSL) or by LibreSSL. -It primarily targets -.Fx , -where it is sandboxed with -.Xr capsicum 4 , +.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. -On -.Ox , -configure with -.Fl \-mandir=/usr/local/man . .Bd -literal -offset indent \&./configure make all @@ -96,30 +127,32 @@ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure . .Pp On -.Fx -and .Ox the recommended way to run .Nm is with the process supervisor -.Lk https://git.causal.agency/catsit catsit . +.Lk https://git.causal.agency/kitd kitd . . .Ss Additional Components Additional functionality can be provided by special-purpose clients connected to .Nm . -.Bl -bullet -.It -Logging can be provided by -.Lk https://git.causal.agency/litterbox litterbox . -.It -Push notifications for the Palaver IRC app -can be provided by -.Nm pounce-palaver , -located in the -.Pa contrib/palaver -directory. -It additionally requires +.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 . @@ -141,8 +174,6 @@ remote client connections state shared between clients .It Pa ring.c buffer between server and clients -.It Pa cert.c -sandboxed certificate reloading .It Pa config.c .Xr getopt_long 3 Ns -integrated configuration parsing @@ -159,6 +190,12 @@ 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 , @@ -169,7 +206,7 @@ Central logging with full-text search: .Lk https://git.causal.agency/litterbox "litterbox" . .Rs -.%A June Bug +.%A June McEnroe .%T IRC Suite .%U https://text.causal.agency/010-irc-suite.txt .%D June 19, 2020 diff --git a/bounce.c b/bounce.c index 381e334..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 @@ -25,7 +25,6 @@ * covered work. */ -#include <assert.h> #include <err.h> #include <errno.h> #include <fcntl.h> @@ -44,18 +43,10 @@ #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> -#endif - -// For getentropy(2): -#ifdef __APPLE__ -#include <sys/random.h> -#endif - #ifndef SIGINFO #define SIGINFO SIGUSR2 #endif @@ -126,51 +117,6 @@ static void saveLoad(const char *path) { atexit(saveSave); } -#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"); -} -#endif - -#ifdef __OpenBSD__ -static void unveilParent(const char *path, const char *mode) { - char buf[PATH_MAX]; - strlcpy(buf, path, sizeof(buf)); - char *base = strrchr(buf, '/'); - if (base) *base = '\0'; - int error = unveil((base ? buf : "."), mode); - if (error && errno != ENOENT) err(EX_OSERR, "unveil"); -} - -static void unveilTarget(const char *path, const char *mode) { - char buf[PATH_MAX]; - strlcpy(buf, path, sizeof(buf)); - char *base = strrchr(buf, '/'); - base = (base ? base + 1 : buf); - ssize_t len = readlink(path, base, sizeof(buf) - (base - buf) - 1); - if (len < 0) return; - base[len] = '\0'; - unveilParent(buf, mode); -} - -static void unveilConfig(const char *path) { - const char *dirs = NULL; - for (const char *abs; NULL != (abs = configPath(&dirs, path));) { - unveilParent(abs, "r"); - unveilTarget(abs, "r"); - } -} - -static void unveilData(const char *path) { - const char *dirs = NULL; - for (const char *abs; NULL != (abs = dataPath(&dirs, path));) { - int error = unveil(abs, "rwc"); - if (error && errno != ENOENT) err(EX_OSERR, "unveil"); - } -} -#endif /* __OpenBSD__ */ - static size_t parseSize(const char *str) { char *rest; size_t size = strtoull(str, &rest, 0); @@ -206,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; @@ -219,6 +167,7 @@ int main(int argc, char *argv[]) { const char *user = NULL; const char *real = NULL; + const char *mode = NULL; const char *join = NULL; const char *quit = "connection reset by purr"; @@ -245,11 +194,14 @@ 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 }, @@ -287,42 +239,40 @@ int main(int argc, char *argv[]) { 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_CONFIG, "unsupported capability"); + 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)) { + 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"); @@ -334,24 +284,27 @@ 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__ - unveilConfig(certPath); - unveilConfig(privPath); - if (caPath) unveilConfig(caPath); - if (clientCert) unveilConfig(clientCert); - if (clientPriv) unveilConfig(clientPriv); - if (savePath) unveilData(savePath); - if (bindPath[0]) unveilParent(bindPath, "rwc"); - - error = unveil(tls_default_ca_cert_file(), "r"); - if (error) err(EX_OSFILE, "%s", tls_default_ca_cert_file()); - - error = pledge("stdio rpath wpath cpath inet flock unix dns recvfd", NULL); - if (error) err(EX_OSERR, "pledge"); + 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); @@ -361,78 +314,47 @@ int main(int argc, char *argv[]) { ringAlloc(ringSize); if (savePath) saveLoad(savePath); + serverConfig(insecure, trust, clientCert, clientPriv); - struct Cert localCA = { -1, -1, "" }; - if (caPath) { - error = 0; - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, caPath));) { - error = certOpen(&localCA, path); - if (!error) break; +#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); } - if (error) err(EX_NOINPUT, "%s", caPath); } + error = unveil(tls_default_ca_cert_file(), "r"); + if (error) err(EX_OSFILE, "%s", tls_default_ca_cert_file()); - const char *dirs; - struct Cert cert; - struct Cert priv; - dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, certPath));) { - error = certOpen(&cert, path); - if (!error) break; - } - if (error) err(EX_NOINPUT, "%s", certPath); - dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, privPath));) { - error = certOpen(&priv, path); - if (!error) break; + 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_NOINPUT, "%s", privPath); - - FILE *certRead = certFile(&cert); - if (!certRead) err(EX_NOINPUT, "%s", certPath); - FILE *privRead = certFile(&priv); - if (!privRead) err(EX_NOINPUT, "%s", privPath); - FILE *caRead = (caPath ? certFile(&localCA) : NULL); - if (caPath && !caRead) err(EX_NOINPUT, "%s", caPath); + if (error) err(EX_OSERR, "pledge"); +#endif - localConfig(certRead, privRead, caRead, !clientPass); - fclose(certRead); - fclose(privRead); - if (caPath) fclose(caRead); + 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__ - 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); - capLimit(cert.parent, &fileRights); - capLimit(cert.target, &fileRights); - capLimit(priv.parent, &fileRights); - capLimit(priv.target, &fileRights); - if (caPath) { - capLimit(localCA.parent, &fileRights); - capLimit(localCA.target, &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, blindReq, plain, nick, user, real); @@ -441,7 +363,8 @@ int main(int argc, char *argv[]) { 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); @@ -456,120 +379,120 @@ int main(int argc, char *argv[]) { eventAdd(bind[i], NULL); } eventAdd(server, NULL); + size_t clientIndex = event.len; + enum { + NeedTime = 10, + IdleTime = 15 * 60, + }; for (;;) { - for (size_t i = binds + 1; i < event.len; ++i) { - assert(event.clients[i]); - if (clientDiff(event.clients[i])) { + 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; - } else { - event.fds[i].events &= ~POLLOUT; } } - int nfds = poll(event.fds, event.len, -1); - if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); + 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); + } + } - for (size_t i = event.len - 1; nfds > 0 && 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; } - - 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)); } } - if (signals[SIGINT] || signals[SIGTERM]) break; - + 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; - certRead = certFile(&cert); - if (!certRead) { - warn("%s", certPath); - continue; - } - privRead = certFile(&priv); - if (!privRead) { - warn("%s", privPath); - continue; - } - caRead = (caPath ? certFile(&localCA) : NULL); - if (caPath && !caRead) { - warn("%s", caPath); - continue; - } - localConfig(certRead, privRead, caRead, !clientPass); - fclose(certRead); - fclose(privRead); - if (caPath) fclose(caRead); + 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: "); - int error = crypt_newhash(pass, "bcrypt,a", hash, sizeof(hash)); + 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]; - int error = getentropy(rand, sizeof(rand)); - if (error) err(EX_OSERR, "getentropy"); - + 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)); } @@ -599,8 +522,14 @@ 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]; - int error = pipe(rw); + error = pipe(rw); if (error) err(EX_OSERR, "pipe"); pid_t pid = fork(); diff --git a/bounce.h b/bounce.h index 6b376ae..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 @@ -25,30 +25,46 @@ * 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> -#ifndef CERTBOT_PATH -#define CERTBOT_PATH "/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 }; @@ -61,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]; @@ -83,7 +100,10 @@ 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) \ @@ -94,6 +114,7 @@ static inline struct Message parse(char *line) { X("setname", CapSetname) \ X("sts", CapSTS) \ X("userhost-in-names", CapUserhostInNames) \ + X("znc.in/self-message", CapSelfMessage) \ X("", CapUnsupported) enum Cap { @@ -138,23 +159,28 @@ 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'; - size_t len = 0; + char *ptr = buf, *end = &buf[sizeof(buf)]; for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { if (caps & (1 << i)) { - len += snprintf( - &buf[len], sizeof(buf) - len, - "%s%s%s%s", - (len ? " " : ""), CapNames[i], - (values && values[i] ? "=" : ""), - (values && values[i] ? values[i] : "") + ptr = seprintf( + ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i] ); - if (len >= sizeof(buf)) break; + if (values && values[i]) { + ptr = seprintf(ptr, end, "=%s", values[i]); + } } } return buf; } 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); @@ -168,14 +194,19 @@ 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); extern struct timeval serverQueueInterval; -void serverConfig(bool insecure, const char *cert, const char *priv); +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, ...) @@ -183,22 +214,46 @@ void serverFormat(const char *format, ...) void serverEnqueue(const char *format, ...) __attribute__((format(printf, 1, 2))); void serverDequeue(void); +void serverClose(void); +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, enum Cap blind, const char *plain, const char *nick, const char *user, const char *real @@ -209,16 +264,8 @@ void stateSync(struct Client *client); const char *stateNick(void); const char *stateEcho(void); -struct Cert { - int parent; - int target; - char name[NAME_MAX]; -}; -int certOpen(struct Cert *cert, const char *path); -FILE *certFile(const struct Cert *cert); - -const char *configPath(const char **dirs, const char *path); -const char *dataPath(const char **dirs, const char *path); +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); diff --git a/calico.1 b/calico.1 index 1b9ec67..37e61a9 100644 --- a/calico.1 +++ b/calico.1 @@ -16,7 +16,7 @@ .Sh DESCRIPTION The .Nm -daemon +program dispatches incoming TLS connections to instances of .Xr pounce 1 @@ -67,21 +67,18 @@ UNIX-domain sockets. .El . .Sh EXAMPLES -Obtain certificates for -and dispatch to two instances of +Start and dispatch to two instances of .Xr pounce 1 : .Bd -literal -offset indent -certbot certonly -d oftc.example.org -certbot certonly -d freenode.example.org -pounce -U /var/run/calico -H oftc.example.org oftc.conf -pounce -U /var/run/calico -H freenode.example.org freenode.conf -calico -H example.org /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 two instances can be connected to via .Li oftc.example.org:6697 and -.Li freenode.example.org:6697 , +.Li libera.example.org:6697 , respectively. . .Sh SEE ALSO @@ -91,28 +88,26 @@ respectively. .Bl -item .It .Rs -.%R RFC 6066 .%A D. Eastlake 3rd -.%Q Huawei .%T Transport Layer Security (TLS) Extensions: Extension Definitions .%I IETF -.%D January 2011 +.%R RFC 6066 .%U https://tools.ietf.org/html/rfc6066 +.%D January 2011 .Re -. .It .Rs -.%R RFC 8446 .%A E. Rescorla .%T The Transport Layer Security (TLS) Protocol Version 1.3 .%I IETF -.%D August 2018 +.%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 @@ -120,4 +115,4 @@ Send mail to or join .Li #ascii.town on -.Li chat.freenode.net . +.Li irc.tilde.chat . diff --git a/cert.c b/cert.c deleted file mode 100644 index 23c9ce8..0000000 --- a/cert.c +++ /dev/null @@ -1,95 +0,0 @@ -/* Copyright (C) 2020 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/>. - * - * 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 <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sysexits.h> -#include <unistd.h> - -#include "bounce.h" - -// This basically exists to work around certbot's symlinks from "live" into -// "archive" under capsicum. - -int certOpen(struct Cert *cert, const char *path) { - char buf[PATH_MAX]; - snprintf(buf, sizeof(buf), "%s", path); - - char *base = strrchr(buf, '/'); - if (base) { - *base = '\0'; - snprintf(cert->name, sizeof(cert->name), "%s", &base[1]); - cert->parent = open(buf, O_DIRECTORY); - } else { - snprintf(cert->name, sizeof(cert->name), "%s", path); - cert->parent = open(".", O_DIRECTORY); - } - if (cert->parent < 0) return -1; - - cert->target = cert->parent; - ssize_t len = readlinkat(cert->parent, cert->name, buf, sizeof(buf) - 1); - if (len < 0 && errno == EINVAL) return 0; - if (len < 0) return -1; - buf[len] = '\0'; - - base = strrchr(buf, '/'); - if (base) { - *base = '\0'; - cert->target = openat(cert->parent, buf, O_DIRECTORY); - if (cert->target < 0) return -1; - } - return 0; -} - -FILE *certFile(const struct Cert *cert) { - const char *name = cert->name; - - char buf[PATH_MAX]; - ssize_t len = readlinkat(cert->parent, cert->name, buf, sizeof(buf) - 1); - if (len < 0) { - if (errno != EINVAL) return NULL; - } else { - // XXX: Assume only the target base name has changed. - buf[len] = '\0'; - name = strrchr(buf, '/'); - if (name) { - name = &name[1]; - } else { - name = buf; - } - } - - int fd = openat(cert->target, name, O_RDONLY); - if (fd < 0) return NULL; - - return fdopen(fd, "r"); -} diff --git a/client.c b/client.c index 36f8008..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 @@ -27,6 +27,7 @@ #include <assert.h> #include <err.h> +#include <fcntl.h> #include <regex.h> #include <stdarg.h> #include <stdbool.h> @@ -41,69 +42,87 @@ #include "bounce.h" -enum Cap clientCaps = CapServerTime | CapConsumer | CapPassive | CapSTS; +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 ((clientCaps & CapSASL) && 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) { - serverEnqueue("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, ...) { @@ -121,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) { @@ -131,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++) { - serverEnqueue("AWAY\r\n"); - } + activeIncr(client); } } @@ -147,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) { @@ -163,7 +180,7 @@ 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; } #ifdef __OpenBSD__ @@ -171,13 +188,13 @@ static void handlePass(struct Client *client, struct Message *msg) { #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); } - explicit_bzero(msg->params[0], strlen(msg->params[0])); } static void handleCap(struct Client *client, struct Message *msg) { @@ -202,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) ); } @@ -215,102 +232,152 @@ 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 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; + } +} + +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 ) { - size_t len = 0; + char *ptr = buf, *end = &buf[cap]; if (msg->tags) { - len += snprintf(&buf[len], cap - len, "@%s ", msg->tags); - if (len >= cap) return; + ptr = seprintf(ptr, end, "@%s ", msg->tags); } - if (!origin) origin = msg->origin; - if (origin) { - len += snprintf(&buf[len], cap - len, ":%s ", origin); - if (len >= cap) return; + if (origin || msg->origin) { + ptr = seprintf(ptr, end, ":%s ", (origin ? origin : msg->origin)); } - len += snprintf(&buf[len], cap - len, "%s", msg->cmd); - if (len >= cap) return; + 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]) { - len += snprintf(&buf[len], cap - len, " :%s", msg->params[i]); + ptr = seprintf(ptr, end, " :%s", msg->params[i]); } else { - len += snprintf(&buf[len], cap - len, " %s", msg->params[i]); + ptr = seprintf(ptr, end, " %s", msg->params[i]); } - if (len >= cap) return; } } static void clientProduce(struct Client *client, const char *line) { size_t diff = ringDiff(client->consumer); ringProduce(line); - if (!diff) ringConsume(NULL, client->consumer); + 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]; - reserialize(buf, sizeof(buf), stateEcho(), msg); - clientProduce(client, buf); - if (!strcmp(msg->params[0], stateNick())) return; - reserialize(buf, sizeof(buf), NULL, msg); - serverFormat("%s\r\n", buf); -} - -static void handleTagmsg(struct Client *client, struct Message *msg) { - if (!msg->params[0]) return; - char buf[MessageCap]; - reserialize(buf, sizeof(buf), stateEcho(), msg); - clientProduce(client, buf); - if (!strcmp(msg->params[0], stateNick())) return; + 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); - serverFormat("%s\r\n", buf); + if (stateCaps & CapEchoMessage && !hasTag(msg->tags, "label")) { + serverFormat( + "@%s%c%s\r\n", + synthLabel(client), + (buf[0] == '@' ? ';' : ' '), + (buf[0] == '@' ? &buf[1] : buf) + ); + } else { + serverFormat("%s\r\n", buf); + } } static void handlePalaver(struct Client *client, struct Message *msg) { @@ -320,6 +387,103 @@ static void handlePalaver(struct Client *client, struct Message *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( + buf, sizeof(buf), ":%s MARKREAD %s timestamp=%s", + clientOrigin, marker->target, marker->timestamp + ); + 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; @@ -327,15 +491,18 @@ static const struct { Handler *fn; } Handlers[] = { { 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", handleTagmsg }, + { true, true, "TAGMSG", handlePrivmsg }, }; static void clientParse(struct Client *client, char *line) { @@ -347,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) { @@ -369,6 +536,11 @@ static bool intercept(const char *line, size_t len) { } 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 @@ -376,7 +548,7 @@ 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; @@ -386,9 +558,7 @@ void clientRecv(struct Client *client) { for (;;) { lf = memchr(line, '\n', &client->buf[client->len] - line); if (!lf) break; - if (verbose) { - fprintf(stderr, "\x1B[33m%.*s\x1B[m\n", (int)(lf - line), line); - } + verboseLog("->", line, lf - line); if (client->need || intercept(line, lf - line)) { lf[0] = '\0'; if (lf - line && lf[-1] == '\r') lf[-1] = '\0'; @@ -400,11 +570,7 @@ void clientRecv(struct Client *client) { } 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) { @@ -426,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 += snprintf(&dst[len], cap - len, "%s", src); - 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) { @@ -531,6 +685,10 @@ 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); } @@ -549,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, @@ -567,18 +719,21 @@ static Filter *Filters[] = { [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) { @@ -586,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 ^ (clientCaps | stateCaps); for (size_t i = 0; line && i < ARRAY_LEN(Filters); ++i) { if (!Filters[i]) continue; if (diff & (1 << i)) line = Filters[i](line); @@ -599,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); @@ -620,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/config.c b/config.c index 958ee45..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 @@ -54,21 +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 = configOpen(path, "r"); - if (!file) 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 4384104..29587a2 100755 --- a/configure +++ b/configure @@ -4,57 +4,60 @@ set -eu cflags() { echo "CFLAGS += $*" } -ldlibs() { - echo "LDLIBS ${o:-}= $*" - o=+ -} -config() { - pkg-config --print-errors "$@" - cflags $(pkg-config --cflags "$@") - ldlibs $(pkg-config --libs "$@") -} 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 (FreeBSD) - ldlibs -lcrypt config libtls defstr OPENSSL_BIN /usr/bin/openssl - defstr CERTBOT_PATH /usr/local/etc/letsencrypt ;; (OpenBSD) - ldlibs -ltls + ldadd crypt '' defstr OPENSSL_BIN /usr/bin/openssl ;; (Linux) cflags -D_GNU_SOURCE - ldlibs -lcrypt 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 ;; (*) - ldlibs -lcrypt config libtls defvar OPENSSL_BIN openssl exec_prefix /bin/openssl ;; diff --git a/contrib/palaver/.gitignore b/contrib/palaver/.gitignore deleted file mode 100644 index 0644d94..0000000 --- a/contrib/palaver/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.o -config.mk -pounce-palaver -rc.d/pounce_palaver diff --git a/contrib/palaver/Makefile b/contrib/palaver/Makefile deleted file mode 100644 index 8c8f2d4..0000000 --- a/contrib/palaver/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -PREFIX ?= /usr/local -MANDIR ?= ${PREFIX}/share/man -ETCDIR ?= ${PREFIX}/etc - -CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -LDLIBS = -lcurl -lsqlite3 -ltls - --include config.mk - -OBJS = notify.o - -all: pounce-palaver - -pounce-palaver: ${OBJS} - ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ - -clean: - rm -f ${OBJS} pounce-palaver - -install: pounce-palaver pounce-palaver.1 - install -d ${DESTDIR}${PREFIX}/bin ${DESTDIR}${MANDIR}/man1 - install pounce-palaver ${DESTDIR}${PREFIX}/bin - install -m 644 pounce-palaver.1 ${DESTDIR}${MANDIR}/man1 - -uninstall: - rm -f ${DESTDIR}${PREFIX}/bin/pounce-palaver - rm -f ${DESTDIR}${MANDIR}/man1/pounce-palaver.1 diff --git a/contrib/palaver/configure b/contrib/palaver/configure deleted file mode 100755 index 65c82fe..0000000 --- a/contrib/palaver/configure +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -set -eu - -cflags() { - echo "CFLAGS += $*" -} -ldlibs() { - echo "LDLIBS ${o:-}= $*" - o=+ -} -config() { - pkg-config --print-errors "$@" - cflags $(pkg-config --cflags "$@") - ldlibs $(pkg-config --libs "$@") -} -defstr() { - cflags "-D'$1=\"$2\"'" -} -defvar() { - defstr "$1" "$(pkg-config --variable=$3 $2)${4:-}" -} - -exec >config.mk - -for opt; do - case "${opt}" in - (--prefix=*) echo "PREFIX = ${opt#*=}" ;; - (--mandir=*) echo "MANDIR = ${opt#*=}" ;; - (*) echo "warning: unsupported option ${opt}" >&2 ;; - esac -done - -case "$(uname)" in - (OpenBSD) - ldlibs -ltls - config libcurl sqlite3 - ;; - (Linux) - cflags -D_GNU_SOURCE - config libcurl libtls sqlite3 - ;; - (*) - config libcurl libtls sqlite3 - ;; -esac diff --git a/dispatch.c b/dispatch.c index c0964e4..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 @@ -25,6 +25,7 @@ * covered work. */ +#include <assert.h> #include <err.h> #include <fcntl.h> #include <netdb.h> @@ -40,53 +41,6 @@ #include <sysexits.h> #include <unistd.h> -#ifdef __FreeBSD__ -#include <sys/capsicum.h> -#endif - -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; @@ -121,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(); @@ -154,6 +110,27 @@ 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; @@ -181,19 +158,20 @@ int main(int argc, char *argv[]) { } #ifdef __OpenBSD__ - error = unveil(path, "r"); + error = unveil(path, "rw"); if (error) err(EX_OSERR, "unveil"); error = pledge("stdio rpath inet unix dns sendfd", NULL); if (error) err(EX_OSERR, "pledge"); #endif - int dir = open(path, O_DIRECTORY); - if (dir < 0) err(EX_NOINPUT, "%s", path); - - error = fchdir(dir); + 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, @@ -204,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"); @@ -221,83 +199,63 @@ 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); + } - if (!nfds) { - for (size_t i = event.len - 1; i >= binds; --i) { - eventRemove(i); + int ready = poll(fds, nfds, (nfds > binds ? timeout : -1)); + if (ready < 0) err(EX_IOERR, "poll"); + + 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; } - 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 }; @@ -306,30 +264,22 @@ int main(int argc, char *argv[]) { 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 a697e15..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 @@ -27,82 +27,51 @@ #include <err.h> #include <errno.h> -#include <fcntl.h> #include <limits.h> #include <netdb.h> #include <netinet/in.h> -#include <netinet/tcp.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" -#ifdef __APPLE__ -#define TCP_KEEPIDLE TCP_KEEPALIVE -#endif - 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 { @@ -113,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) { @@ -150,50 +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 }; - int len = snprintf( - addr.sun_path, sizeof(addr.sun_path), "%s", 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; + + int check = socket(PF_UNIX, SOCK_STREAM, 0); + if (check < 0) err(EX_OSERR, "socket"); - char dir[PATH_MAX] = "."; - const char *base = strrchr(path, '/'); - if (base) { - snprintf(dir, sizeof(dir), "%.*s", (int)(base - path), path); - base++; - } else { - base = path; + 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; @@ -221,29 +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 on = 1; - int error = setsockopt(*fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); - if (error) err(EX_OSERR, "setsockopt"); - -#ifdef TCP_KEEPIDLE - int idle = 15 * 60; - error = setsockopt(*fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)); - if (error) err(EX_OSERR, "setsockopt"); -#endif - - 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/contrib/palaver/notify.c b/palaver.c index a8ca220..1453551 100644 --- a/contrib/palaver/notify.c +++ b/palaver.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 @@ -43,11 +43,11 @@ #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 DATABASE_PATH "pounce/palaver.sqlite" - #define SQL(...) #__VA_ARGS__ #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) @@ -70,48 +70,27 @@ static void dbOpen(const char *path, int flags) { sqlite3_busy_timeout(db, 10000); } -static void dbFind(char *path) { +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); } - const char *home = getenv("HOME"); - const char *dataHome = getenv("XDG_DATA_HOME"); - const char *dataDirs = getenv("XDG_DATA_DIRS"); - if (!dataDirs) dataDirs = "/usr/local/share:/usr/share"; - char buf[PATH_MAX]; - if (dataHome) { - snprintf(buf, sizeof(buf), "%s/" DATABASE_PATH, dataHome); - } else { - if (!home) errx(EX_CONFIG, "HOME unset"); - snprintf(buf, sizeof(buf), "%s/.local/share/" DATABASE_PATH, home); - } - dbOpen(buf, SQLITE_OPEN_READWRITE); - if (db) return; - - char create[PATH_MAX]; - snprintf(create, sizeof(create), "%s", buf); - - while (*dataDirs) { - size_t len = strcspn(dataDirs, ":"); - snprintf(buf, sizeof(buf), "%.*s/" DATABASE_PATH, (int)len, dataDirs); + for (int i = 0; dataPath(buf, sizeof(buf), "palaver.sqlite", i); ++i) { dbOpen(buf, SQLITE_OPEN_READWRITE); if (db) return; - dataDirs += len; - if (*dataDirs) dataDirs++; } - char *base = strrchr(create, '/'); - *base = '\0'; - int error = mkdir(create, 0700); - if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", create); - *base = '/'; + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), 0700); + if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", buf); - dbOpen(create, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); - if (!db) errx(EX_CANTCREAT, "%s: cannot create database", create); + 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) { @@ -148,7 +127,6 @@ static void dbInit(void) { port INTEGER NOT NULL, client TEXT NOT NULL, version TEXT NOT NULL, - network TEXT, UNIQUE (host, port, client) ); CREATE TABLE IF NOT EXISTS preferences ( @@ -158,6 +136,12 @@ static void dbInit(void) { ); 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); @@ -247,7 +231,6 @@ static void handleError(struct Message *msg) { static char *nick; static bool away; -static int badge; static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); @@ -279,6 +262,8 @@ static void handleReplyUserHost(struct Message *msg) { } } +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]); @@ -307,7 +292,7 @@ static void keyword(sqlite3_context *ctx, int n, sqlite3_value *args[]) { size_t len = strlen(needle); const char *match = haystack; sqlite3_result_int(ctx, false); - while (NULL != (match = strcasestr(match, needle))) { + while (NULL != (match = (sensitive ? strstr : strcasestr)(match, needle))) { char a = (match > haystack ? match[-1] : ' '); char b = (match[len] ? match[len] : ' '); if (b == '\1') b = ' '; @@ -325,8 +310,11 @@ enum { Begin, Set, End, - Clear, + Each, Notify, + Increment, + Reset, + Badge, QueriesLen, }; @@ -348,14 +336,14 @@ static const char *Queries[QueriesLen] = { ), [End] = SQL( - INSERT INTO clients (host, port, client, version, network) - VALUES (:host, :port, :client, :version, :network) + INSERT INTO clients (host, port, client, version) + VALUES (:host, :port, :client, :version) ON CONFLICT (host, port, client) DO - UPDATE SET version = :version, network = :network + UPDATE SET version = :version WHERE host = :host AND port = :port AND client = :client; ), - [Clear] = SQL( + [Each] = SQL( SELECT pushToken.value, pushEndpoint.value FROM clients JOIN preferences AS pushToken USING (client) @@ -391,8 +379,7 @@ static const char *Queries[QueriesLen] = { SELECT pushToken.value, pushEndpoint.value, - coalesce(showMessagePreview.value, 'true'), - clients.network + coalesce(showMessagePreview.value, 'true') FROM clients JOIN matches USING (client) JOIN preferences AS pushToken USING (client) @@ -403,8 +390,38 @@ static const char *Queries[QueriesLen] = { 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]); @@ -413,7 +430,6 @@ static void palaverIdentify(struct Message *msg) { int result = sqlite3_step(stmts[Identify]); if (result == SQLITE_DONE) { format("PALAVER REQ\r\n"); - dbBindCopy(stmts[End], ":network", msg->params[3]); } else if (result != SQLITE_ROW) { errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); } @@ -465,18 +481,6 @@ static void handlePalaver(struct Message *msg) { } } -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 void pushNotify(const char *endpoint, const char *token, char *body) { CURLcode code = curl_easy_setopt(curl, CURLOPT_URL, endpoint); if (code) { @@ -514,35 +518,48 @@ static void handleReplyNowAway(struct Message *msg) { static void handleReplyUnaway(struct Message *msg) { (void)msg; + if (!away) return; away = false; - if (!badge) return; - badge = 0; - dbVerbose(stmts[Clear]); + char json[32]; + snprintf(json, sizeof(json), "{\"badge\":%d}", badgeCount(Reset)); + int result; - while (SQLITE_ROW == (result = sqlite3_step(stmts[Clear]))) { + dbVerbose(stmts[Each]); + while (SQLITE_ROW == (result = sqlite3_step(stmts[Each]))) { int i = 0; - const char *token = sqlite3_column_text(stmts[Clear], i++); - const char *endpoint = sqlite3_column_text(stmts[Clear], i++); - pushNotify(endpoint, token, "{\"badge\":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[Clear]); + sqlite3_reset(stmts[Each]); } static bool noPreview; static bool noPrivatePreview; -static void jsonBody( - char *buf, size_t cap, - struct Message *msg, const char *network, bool preview -) { +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; - FILE *file = fmemopen(buf, cap, "w"); - if (!file) err(EX_OSERR, "fmemopen"); + 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\":"); @@ -551,10 +568,6 @@ static void jsonBody( fprintf(file, ",\"channel\":"); jsonString(file, msg->params[0]); } - if (network) { - fprintf(file, ",\"network\":"); - jsonString(file, network); - } if (preview) { if (!strncmp(msg->params[1], "\1ACTION ", 8)) { size_t len = strlen(msg->params[1]); @@ -570,9 +583,10 @@ static void jsonBody( } fprintf(file, "}"); - // XXX: fmemopen only null-terminates if there is room. - fclose(file); - buf[cap - 1] = '\0'; + int error = fclose(file); + if (error) err(EX_IOERR, "fclose"); + + return buf; } static void handlePrivmsg(struct Message *msg) { @@ -592,21 +606,17 @@ static void handlePrivmsg(struct Message *msg) { ); dbVerbose(stmts[Notify]); int result; - bool badged = false; + 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++); - const char *network = sqlite3_column_text(stmts[Notify], i++); - char body[4096]; - if (!badged) { - badge++; - badged = true; - } - jsonBody(body, sizeof(body), msg, network, !strcmp(preview, "true")); + 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]); @@ -662,9 +672,10 @@ int main(int argc, char *argv[]) { 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:u:vw:"));) { + 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; @@ -673,9 +684,12 @@ int main(int argc, char *argv[]) { 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"); @@ -724,6 +738,11 @@ int main(int argc, char *argv[]) { } 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) { 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/contrib/palaver/pounce-palaver.1 b/pounce-palaver.1 index f88420e..2d5aa1d 100644 --- a/contrib/palaver/pounce-palaver.1 +++ b/pounce-palaver.1 @@ -1,4 +1,4 @@ -.Dd August 27, 2020 +.Dd November 28, 2021 .Dt POUNCE-PALAVER 1 .Os . @@ -8,11 +8,12 @@ . .Sh SYNOPSIS .Nm -.Op Fl PNv +.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 @@ -56,6 +57,13 @@ Load the TLS client private key from 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 . @@ -93,7 +101,7 @@ The most likely default path to the database file. .Lk https://github.com/cocodelabs/palaver-irc-capability "Palaver IRC Capability" . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency +.An June McEnroe Aq Mt june@causal.agency . .Sh BUGS Send mail to @@ -101,4 +109,4 @@ Send mail to or join .Li #ascii.town on -.Li chat.freenode.net . +.Li irc.tilde.chat . diff --git a/pounce.1 b/pounce.1 index ed64ebf..e4919d2 100644 --- a/pounce.1 +++ b/pounce.1 @@ -1,4 +1,4 @@ -.Dd August 28, 2020 +.Dd July 16, 2023 .Dt POUNCE 1 .Os . @@ -9,26 +9,28 @@ .Sh SYNOPSIS .Nm .Op Fl LNTev -.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 Q Ar time -.Op Fl R Ar caps +.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 @@ -36,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 . @@ -45,15 +54,34 @@ .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. -The IRCv3.2 +Clients must uniquely identify themselves to +.Nm +by their IRC username +(not nickname). +The IRCv3 .Sy server-time 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 @@ -62,42 +90,43 @@ must be configured for each IRC network. Instances of .Nm must either use different local ports with -.Fl P -or different local hosts with -.Fl H +.Cm local-port +or different local host names with +.Cm local-host and -.Fl U +.Cm local-path to be dispatched from the same port by .Xr calico 1 . . .Pp -TLS certificates can be automatically loaded from -.Pa /usr/local/etc/letsencrypt -(or equivalent) -based on the local host set by -.Fl H . -These certificates can be obtained using -.Xr certbot 8 . -. -.Pp -Clients must uniquely identify themselves to +Client connections are not accepted +until successful login to the server. +If the server connection is lost, +the .Nm -by their IRC username. -See -.Sx Client Configuration -for details. +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 / , +.Ql \&./ or -.Ql \&. . +.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 # @@ -105,49 +134,58 @@ 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 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 . +is set by +.Cm local-host . . -.It Fl L , Cm palaver +.It Fl L | Cm palaver Advertise the .Sy palaverapp.com IRCv3 vendor-specific capability to clients. @@ -156,47 +194,17 @@ push notifications must be provided by the .Xr pounce-palaver 1 special-purpose client. . -.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 +.It Fl P Ar port | Cm local-port No = Ar port Bind to .Ar port . The default port is 6697. . -.It Fl Q Ar ms , Cm queue-interval = Ar ms -Set the server send queue interval in milliseconds. -The queue is only used -for automated messages sent by -.Nm . -Messages from clients -are sent to the server immediately. -The default interval is 200 milliseconds. -. -.It Fl R Ar caps , Cm blind-req = Ar caps -Blindly request the IRCv3 capabilities -.Ar caps . -This can be used to enable hidden capabilities, -such as -.Sy userhost-in-names -on freenode. -. -.It Fl S Ar host , Cm bind = Ar host -Bind to source address -.Ar host -when connecting to the server. -. -.It Fl T , Cm no-sts +.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 only accepted as dispatched by @@ -207,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. @@ -223,12 +231,80 @@ The string must be hashed using .Fl x . If -.Fl A +.Cm local-ca is also set, 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 @@ -236,166 +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 . +.Cm client-cert . See .Sx Configuring CertFP . . -.It Fl f Ar path , Cm save = Ar path -Load and save the contents of the buffer from -.Ar path -in -.Pa $XDG_DATA_DIRS/pounce , -or an absolute or relative path if -.Ar path -starts with -.Ql / -or -.Ql \&. . -The file is truncated after loading. -. -.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 the following colors: +.It Fl w Ar pass | Cm pass No = Ar pass +Log in with the server password +.Ar pass . +. +.It Fl y Ar mesg | Cm away No = Ar mesg +Set away status to +.Ar mesg +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 -.Bl -tag -width Ds -compact -.It red +.Bl -tag -width "<<" -compact +.It << from .Nm to the server -.It green +.It >> from the server to .Nm -.It yellow +.It -> from clients to .Nm -.It blue +.It <- from .Nm to clients .El . -.It Fl w Ar pass , Cm pass = 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 -Set away status to -.Ar mesg -when no clients are connected. +.Cm local-pass . .El . -.Pp -Client connections are not accepted -until successful login to the server. -If the server connection is lost, -the -.Nm -daemon exits. -. -.Pp -Upon receiving the -.Dv SIGUSR1 -signal, -the certificate, private key and local CA -will be reloaded from the paths -specified by -.Fl C , -.Fl K -and -.Fl A . -. .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. @@ -403,7 +461,7 @@ If both are used, clients may authenticate with either method. . .Pp -Clients must 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 @@ -414,6 +472,26 @@ 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 @@ -431,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 , @@ -451,14 +531,16 @@ not to the server. .It Generate self-signed client certificates and private keys: .Bd -literal -offset indent -pounce -g client1.pem -pounce -g client2.pem +$ 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 >> ~/.config/pounce/auth.pem -openssl x509 -subject -in client2.pem >> ~/.config/pounce/auth.pem +$ 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 @@ -467,7 +549,7 @@ to verify client certificates against the CA file: .Bd -literal -offset indent local-ca = auth.pem -# or: pounce -A auth.pem +# or: $ pounce -A auth.pem .Ed .El . @@ -480,21 +562,21 @@ by a generated certificate authority: .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 ~/.config/pounce/auth.crt +$ openssl x509 -in auth.pem -out ~/.config/pounce/auth.crt .Ed .It Configure @@ -503,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 . @@ -512,13 +594,13 @@ local-ca = auth.crt .It Generate a new TLS client certificate: .Bd -literal -offset indent -pounce -g ~/.config/pounce/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 @@ -533,7 +615,25 @@ 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 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 -o -h irc.example.org > ~/.config/pounce/example.pem +.Ed +.It +Configure +.Nm +to trust the certificate: +.Bd -literal -offset indent +trust = example.pem +# or: $ pounce -t example.pem .Ed .El . @@ -574,30 +674,85 @@ The most likely location of save files. .El . .Sh EXAMPLES -Obtain a certificate -and make its private key available to +Start .Nm : .Bd -literal -offset indent -sudo certbot certonly -d irc.example.org -sudo chown :$USER /etc/letsencrypt/live/irc.example.org/privkey.pem -sudo chmod g+r /etc/letsencrypt/live/irc.example.org/privkey.pem +$ pounce -H irc.example.org -h irc.tilde.chat -j '#ascii.town' +.Ed +.Pp +Write an equivalent configuration file to +.Pa ~/.config/pounce/tilde.conf : +.Bd -literal -offset indent +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 -Start +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 -pounce -H irc.example.org -h chat.freenode.net -j '#ascii.town' +# acme-client irc.example.org +# chown user /home/user/.config/pounce/irc.example.org.key .Ed -. .Pp -Equivalent configuration file: +Renew and reload the certificate with a +.Xr cron 8 +job: .Bd -literal -offset indent -local-host = irc.example.org -host = chat.freenode.net -join = #ascii.town +~ * * * * 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 . @@ -605,34 +760,54 @@ join = #ascii.town .Bl -item .It .Rs -.%R RFC 2812 -.%A C. Kalt -.%T Internet Relay Chat: Client Protocol -.%I IETF -.%D April 2000 -.%U https://tools.ietf.org/html/rfc2812 +.%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 -.%R RFC 4616 -.%A K. Zeilenga, Ed. -.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism -.%I IETF -.%D August 2006 -.%U https://tools.ietf.org/html/rfc4616 +.%A Kyle Fuller +.%A St\('ephan Kochen +.%A Alexey Sokolov +.%A James Wheare +.%T server-time Extension +.%I IRCv3 Working Group +.%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 +.%A James Wheare +.%T IRCv3 Client Capability Negotiation +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/core/capability-negotiation .Re -. .It .Rs .%A S. Josefsson .%T The Base16, Base32, and Base64 Data Encodings .%I IETF .%R RFC 4648 -.%D October 2006 .%U https://tools.ietf.org/html/rfc4648 +.%D October 2006 +.Re +.It +.Rs +.%A C. Kalt +.%T Internet Relay Chat: Client Protocol +.%I IETF +.%R RFC 2812 +.%U https://tools.ietf.org/html/rfc2812 +.%D April 2000 .Re -. .It .Rs .%A Attila Molnar @@ -641,7 +816,6 @@ join = #ascii.town .%I IRCv3 Working Group .%U https://ircv3.net/specs/extensions/sts .Re -. .It .Rs .%A Attila Molnar @@ -650,40 +824,22 @@ join = #ascii.town .%I IRCv3 Working Group .%U https://ircv3.net/specs/extensions/sasl-3.2 .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 -.%I IRCv3 Working Group -.%U https://ircv3.net/specs/core/capability-negotiation -.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 Waldo Bastian -.%A Ryan Lortie -.%A Lennart Poettering -.%T XDG Base Directory Specification -.%D November 24, 2010 -.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +.%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 . @@ -724,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 @@ -738,24 +894,7 @@ 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 -is required for each server connection. -The -.Nm -daemon must be restarted -if the server connection is lost. -. -.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 @@ -763,7 +902,7 @@ Send mail to 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/ring.c b/ring.c index e675526..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 @@ -27,6 +27,8 @@ #include <assert.h> #include <err.h> +#include <inttypes.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <sys/time.h> @@ -42,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"); @@ -51,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); @@ -110,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); @@ -139,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) { @@ -163,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; @@ -202,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 89e3e36..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 @@ -27,6 +27,7 @@ #include <assert.h> #include <err.h> +#include <limits.h> #include <netdb.h> #include <netinet/in.h> #include <stdarg.h> @@ -41,37 +42,44 @@ #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) { - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, cert));) { + for (int i = 0; configPath(buf, sizeof(buf), cert, i); ++i) { if (priv) { - error = tls_config_set_cert_file(config, path); + error = tls_config_set_cert_file(config, buf); } else { - error = tls_config_set_keypair_file(config, path, path); + 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) { - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, priv));) { - error = tls_config_set_key_file(config, path); + 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)); @@ -82,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) { @@ -139,14 +146,29 @@ int serverConnect(const char *bindHost, const char *host, const char *port) { 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; @@ -222,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 59a61ed..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 @@ -38,6 +38,8 @@ bool stateNoNames; enum Cap stateCaps; +char *stateAccount; +bool stateAway; typedef void Handler(struct Message *msg); @@ -59,26 +61,34 @@ void stateLogin( const char *pass, enum Cap blind, const char *plain, const char *nick, const char *user, const char *real ) { - serverFormat("CAP LS 302\r\n"); - if (pass) serverFormat("PASS :%s\r\n", pass); - if (blind) serverFormat("CAP REQ :%s\r\n", capList(blind, NULL)); if (plain) { - byte buf[AuthLen]; + byte buf[AuthLen] = {0}; 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]); - } + 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, sizeof(buf)); + explicit_bzero(buf, len); } + + serverFormat("CAP LS 302\r\n"); + if (pass) serverFormat("PASS :%s\r\n", pass); + 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; @@ -89,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; @@ -120,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"); } @@ -159,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); @@ -198,6 +210,7 @@ static void handleReplyMyInfo(struct Message *msg) { } static struct { + bool done; char **tokens; size_t cap, len; } support; @@ -215,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; @@ -267,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; @@ -280,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; @@ -317,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]); @@ -331,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 }, @@ -364,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] : "") @@ -399,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); @@ -418,9 +467,12 @@ 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; serverEnqueue("NAMES %s\r\n", chan->name); } diff --git a/xdg.c b/xdg.c index 7400d80..03f8c8d 100644 --- a/xdg.c +++ b/xdg.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2019, 2020 C. McEnroe <june@causal.agency> +/* 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 @@ -32,6 +32,7 @@ #include <stdlib.h> #include <string.h> #include <sys/stat.h> +#include <sysexits.h> #include "bounce.h" @@ -58,93 +59,73 @@ static const struct Base Data = { .defDirs = "/usr/local/share:/usr/share", }; -static const char * -basePath(struct Base base, const char **dirs, const char *path) { - static char buf[PATH_MAX]; - - if (*dirs) { - if (!**dirs) return NULL; - size_t len = strcspn(*dirs, ":"); - snprintf(buf, sizeof(buf), "%.*s/" SUBDIR "/%s", (int)len, *dirs, path); - *dirs += len; - if (**dirs) *dirs += 1; +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 (path[0] == '/' || path[0] == '.') { - *dirs = ""; - return path; + 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; } - *dirs = getenv(base.envDirs); - if (!*dirs) *dirs = base.defDirs; - const char *home = getenv("HOME"); const char *baseHome = getenv(base.envHome); if (baseHome) { - snprintf(buf, sizeof(buf), "%s/" SUBDIR "/%s", baseHome, path); + snprintf(buf, cap, "%s/" SUBDIR "/%s", baseHome, path); } else if (home) { - snprintf( - buf, sizeof(buf), "%s/%s/" SUBDIR "/%s", - home, base.defHome, path - ); + snprintf(buf, cap, "%s/%s/" SUBDIR "/%s", home, base.defHome, path); } else { - return NULL; + errx(EX_USAGE, "HOME unset"); } return buf; } -const char *configPath(const char **dirs, const char *path) { - return basePath(Config, dirs, path); +char *configPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Config, buf, cap, path, i); } -const char * -dataPath(const char **dirs, const char *path) { - return basePath(Data, dirs, path); +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) { - const char *dirs = NULL; - for (const char *abs; NULL != (abs = configPath(&dirs, path));) { - FILE *file = fopen(abs, 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", abs); + if (errno != ENOENT) warn("%s", buf); } - FILE *file = fopen(path, mode); - if (!file) warn("%s", path); - return file; -} - -static void dataMkdir(const char *path) { - const char *dirs = NULL; - const char *abs = dataPath(&dirs, path); - if (!abs) return; - int error = mkdir(abs, S_IRWXU); - if (error && errno != EEXIST) warn("%s", abs); + warn("%s", configPath(buf, sizeof(buf), path, 0)); + return NULL; } FILE *dataOpen(const char *path, const char *mode) { - const char *dirs = NULL; - for (const char *abs; NULL != (abs = dataPath(&dirs, path));) { - FILE *file = fopen(abs, 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", abs); + if (errno != ENOENT) warn("%s", buf); } - if (mode[0] != 'r') { - dataMkdir(""); - dirs = NULL; - path = dataPath(&dirs, path); - if (!path) { - warn("HOME unset"); - return NULL; - } - FILE *file = fopen(path, mode); - if (!file) warn("%s", path); - return file; + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU); + if (error && errno != EEXIST) warn("%s", buf); } - - FILE *file = fopen(path, mode); - if (!file) warn("%s", path); + FILE *file = fopen(dataPath(buf, sizeof(buf), path, 0), mode); + if (!file) warn("%s", buf); return file; } |