changeset 941:e43ccb1f0ace

irccdctl: add basics commands
author David Demelier <markand@malikania.fr>
date Thu, 14 Jan 2021 17:44:48 +0100
parents 94cae3129870
children d6febe682db7
files .hgignore Makefile irccdctl/main.c lib/irccd/irccd.c lib/irccd/irccd.h lib/irccd/peer.c lib/irccd/transport.c
diffstat 7 files changed, 739 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Jan 14 10:46:41 2021 +0100
+++ b/.hgignore	Thu Jan 14 17:44:48 2021 +0100
@@ -18,7 +18,8 @@
 ^extern/libcompat/trylib$
 
 # executables.
-irccd/irccd
+^irccd/irccd$
+^irccdctl/irccdctl$
 
 # temporary files.
 ^lib/irccd/config\.h$
--- a/Makefile	Thu Jan 14 10:46:41 2021 +0100
+++ b/Makefile	Thu Jan 14 17:44:48 2021 +0100
@@ -30,6 +30,11 @@
 IRCCD_OBJS=             ${IRCCD_SRCS:.c=.o}
 IRCCD_DEPS=             ${IRCCD_SRCS:.c=.d}
 
+IRCCDCTL=               irccdctl/irccdctl
+IRCCDCTL_SRCS=          irccdctl/main.c
+IRCCDCTL_OBJS=          ${IRCCDCTL_SRCS:.c=.o}
+IRCCDCTL_DEPS=          ${IRCCDCTL_SRCS:.c=.d}
+
 LIBCOMPAT=              extern/libcompat/libirccd-compat.a
 
 ifeq (${WITH_JS},yes)
@@ -96,7 +101,7 @@
 LIBS+=                  -l ssl -l crypto
 endif
 
-all: ${IRCCD}
+all: ${IRCCD} ${IRCCDCTL}
 
 .c.o:
 	${CMD.cc}
@@ -134,6 +139,9 @@
 ${IRCCD}: ${IRCCD_OBJS} ${LIBCOMPAT} ${LIBDUKTAPE} ${LIBIRCCD}
 	${CMD.ccld}
 
+${IRCCDCTL}: ${IRCCDCTL_OBJS}
+	${CMD.ccld}
+
 # Unit tests.
 tests/test-%.o: tests/test-%.c
 	${CMD.cc}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/irccdctl/main.c	Thu Jan 14 17:44:48 2021 +0100
@@ -0,0 +1,296 @@
+/*
+ * main.c -- irccdctl(1) main file
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <stdio.h>
+
+#include <irccd/limits.h>
+#include <irccd/util.h>
+
+static bool verbose;
+static int sock;
+static struct sockaddr_un sockaddr = {
+	.sun_family = PF_LOCAL,
+	.sun_path = "/tmp/irccd.sock"
+};
+static char in[IRC_BUF_MAX];
+static char out[IRC_BUF_MAX];
+
+static char *
+poll(void)
+{
+	static char ret[IRC_BUF_MAX];
+	char *nl;
+
+	while (!(nl = strstr(in, "\n"))) {
+		char buf[IRC_BUF_MAX] = {0};
+
+		if (recv(sock, buf, sizeof (buf) - 1, 0) <= 0)
+			err(1, "abort");
+		if (strlcat(in, buf, sizeof (in)) >= sizeof (in))
+			errc(1, EMSGSIZE, "abort");
+	}
+
+	*nl = '\0';
+	strlcpy(ret, in, sizeof (ret));
+	memmove(in, nl + 1, sizeof (in) - (nl - in) - 1);
+
+	return ret;
+}
+
+static void
+dial(void)
+{
+	const struct timeval tv = {
+		.tv_sec = 30
+	};
+
+	if ((sock = socket(PF_LOCAL, SOCK_STREAM, 0)) < 0)
+		err(1, "socket");
+	if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof (tv)) < 0 ||
+	    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)) < 0)
+		err(1, "setsockopt");
+	if (connect(sock, (const struct sockaddr *)&sockaddr, SUN_LEN(&sockaddr)) < 0)
+		err(1, "connect");
+}
+
+static void
+check(void)
+{
+	/* Ensure we're talking to irccd. */
+	int major, minor, patch;
+
+	if ((sscanf(poll(), "IRCCD %d.%d.%d", &major, &minor, &patch) != 3))
+		errx(1, "abort: not irccd instance");
+	if (verbose)
+		printf("connected to irccd %d.%d.%d\n", major, minor, patch);
+}
+
+static void
+req(const char *fmt, ...)
+{
+	char buf[IRC_BUF_MAX];
+	va_list ap;
+
+	va_start(ap, fmt);
+	vsnprintf(buf, sizeof (buf), fmt, ap);
+	va_end(ap);
+
+	if (strlcat(out, buf, sizeof (out)) >= sizeof (out) ||
+	    strlcat(out, "\n", sizeof (out)) >= sizeof (out))
+		errc(1, EMSGSIZE, "abort");
+
+	while (out[0]) {
+		ssize_t ns, len;
+
+		len = strlen(out);
+
+		if ((ns = send(sock, out, len, MSG_NOSIGNAL)) <= 0)
+			err(1, "send");
+
+		if (ns >= len)
+			memset(out, 0, sizeof (out));
+		else
+			memmove(out, out + ns, sizeof (out) - ns);
+	}
+}
+
+static void
+ok(void)
+{
+	const char *response = poll();
+
+	if (strcmp(response, "OK") != 0)
+		errx(1, "abort: %s", response);
+}
+
+static void
+cmd_server_list(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	char *list;
+
+	req("SERVER-LIST");
+
+	if (strncmp(list = poll(), "OK ", 3) != 0)
+		errx(1, "failed to retrieve server list");
+
+	/* Skip "OK " */
+	list += 3;
+
+	/* Since list is separated by spaces, just convert them to \n */
+	for (char *p; (p = strchr(list, ' ')); )
+		*p = '\n';
+
+	puts(list);
+}
+
+static void
+cmd_server_message(int argc, char **argv)
+{
+	(void)argc;
+
+	req("SERVER-MESSAGE %s %s %s", argv[0], argv[1], argv[2]);
+	ok();
+}
+
+static void
+cmd_server_me(int argc, char **argv)
+{
+	(void)argc;
+
+	req("SERVER-ME %s %s %s", argv[0], argv[1], argv[2]);
+	ok();
+}
+
+static void
+cmd_server_mode(int argc, char **argv)
+{
+#if 0
+	req("MODE %s %s %s%c%s%c%s%c%s", argv[0], argv[1], argv[2],
+		argc >= 4 ? ' ', argv[3] : "",
+		argc >= 5 ? ' ', argv[4] : "",
+		argc >= 6 ? ' ', argv[5] : "");
+	ok();
+#endif
+}
+
+static void
+cmd_server_nick(int argc, char **argv)
+{
+	req("SERVER-NICK %s %s", argv[0], argv[1]);
+	ok();
+}
+
+static void
+cmd_server_notice(int argc, char **argv)
+{
+	req("SERVER-NOTICE %s %s %s", argv[0], argv[1], argv[2]);
+	ok();
+}
+
+static void
+cmd_server_part(int argc, char **argv)
+{
+	/* Let's advertise irccd a bit. */
+	req("SERVER-PART %s %s %s", argv[0], argv[1],
+	    argc >= 3 ? argv[2] : "irccd is shutting down");
+	ok();
+}
+
+static void
+cmd_server_topic(int argc, char **argv)
+{
+	req("SERVER-TOPIC %s %s %s", argv[0], argv[1], argv[2]);
+	ok();
+}
+
+static const struct cmd {
+	const char *name;
+	unsigned int minargs;
+	unsigned int maxargs;
+	void (*exec)(int, char **);
+} cmds[] = {
+	/* name                 min     max     exec                   */
+	{ "server-list",        0,      0,      cmd_server_list         },
+	{ "server-me",          3,      3,      cmd_server_me           },
+	{ "server-message",     3,      3,      cmd_server_message      },
+	{ "server-mode",        3,      6,      cmd_server_mode         },
+	{ "server-nick",        2,      2,      cmd_server_nick         },
+	{ "server-notice",      3,      3,      cmd_server_notice       },
+	{ "server-part",        3,      3,      cmd_server_part         },
+	{ "server-topic",       3,      3,      cmd_server_topic        }
+};
+
+static int
+cmp_cmd(const void *d1, const void *d2)
+{
+	return strcmp(d1, ((const struct cmd *)d2)->name);
+}
+
+static const struct cmd *
+find_cmd(const char *name)
+{
+	return bsearch(name, cmds, IRC_UTIL_SIZE(cmds), sizeof (cmds[0]), cmp_cmd);
+}
+
+static void
+run(int argc, char **argv)
+{
+	const struct cmd *c;
+
+	if (!(c = find_cmd(argv[0])))
+		errx(1, "abort: command not found");
+
+	--argc;
+	++argv;
+
+	if (argc < c->minargs || argc > c->maxargs)
+		errx(1, "abort: invalid number of arguments");
+
+	c->exec(argc, argv);
+}
+
+static noreturn void
+usage(void)
+{
+	/* TODO: getprogname() */
+	fprintf(stderr, "usage: irccdctl [-v] [-s sock] command [arguments...]\n");
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	for (int ch; (ch = getopt(argc, argv, "s:v")) != -1; ) {
+		switch (ch) {
+		case 's':
+			strlcpy(sockaddr.sun_path, optarg, sizeof (sockaddr.sun_path));
+			break;
+		case 'v':
+			verbose = true;
+			break;
+		default:
+			break;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc < 1)
+		usage();
+
+	dial();
+	check();
+	run(argc, argv);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/irccd.c	Thu Jan 14 17:44:48 2021 +0100
@@ -0,0 +1,310 @@
+/*
+ * irccd.c -- main irccd object
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <err.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "event.h"
+#include "irccd.h"
+#include "log.h"
+#include "plugin.h"
+#include "server.h"
+#include "peer.h"
+#include "transport.h"
+#include "util.h"
+
+/* TODO: TMP */
+#include <stdio.h>
+
+#define APPEND(a, l, o, f)                                              \
+do {                                                                    \
+        a = irc_util_reallocarray(a, ++l, sizeof (*o));                 \
+        memcpy(&a[l - 1], o, sizeof (*o));                              \
+        qsort(a, l, sizeof (*o), f);                                    \
+} while (0)
+
+#define REMOVE(a, l, f)                                                 \
+do {                                                                    \
+        if (--l == 0) {                                                 \
+                free(a);                                                \
+                a = NULL;                                               \
+        } else {                                                        \
+                qsort(a, l + 1, sizeof (*a), f);                        \
+                a = irc_util_reallocarray(a, --l, sizeof (*a));         \
+        }                                                               \
+} while (0)
+
+struct pkg {
+	struct pollfd *fds;
+	size_t fdsz;
+};
+
+struct defer {
+	void (*exec)(void *);
+	void (*data);
+};
+
+struct irc irc;
+
+static int pipes[2];
+
+static int
+cmp_server(const void *d1, const void *d2)
+{
+	return strcmp(
+		((const struct irc_server *)d1)->name,
+		((const struct irc_server *)d2)->name
+	);
+}
+
+static int
+cmp_plugin(const void *d1, const void *d2)
+{
+	return strcmp(
+		((const struct irc_plugin *)d1)->name,
+		((const struct irc_plugin *)d2)->name
+	);
+}
+
+static int
+cmp_peer(const void *d1, const void *d2)
+{
+	return ((const struct irc_peer *)d2)->fd - ((const struct irc_peer *)d1)->fd;
+}
+
+static int
+cmp_server_name(const void *d1, const void *d2)
+{
+	return strcmp(d1, ((const struct irc_server *)d2)->name);
+}
+
+static int
+cmp_plugin_name(const void *d1, const void *d2)
+{
+	return strcmp(d1, ((const struct irc_plugin *)d2)->name);
+}
+
+static struct pkg
+prepare(void)
+{
+	struct pkg pkg = {0};
+	size_t i = 0;
+
+	pkg.fdsz += 1;                  /* pipe  */
+	pkg.fdsz += 1;                  /* transport fd */
+	pkg.fdsz += irc.serversz;       /* servers */
+	pkg.fdsz += irc.peersz;         /* transport peers */
+
+	pkg.fds = irc_util_calloc(pkg.fdsz, sizeof (*pkg.fds));
+
+	/* pipe */
+	pkg.fds[i].fd = pipes[0];
+	pkg.fds[i++].events = POLLIN;
+
+	/* transport */
+	irc_transport_prepare(&pkg.fds[i++]);
+
+	for (size_t p = 0; p < irc.peersz; ++p)
+		irc_peer_prepare(&irc.peers[p], &pkg.fds[i++]);
+
+	for (size_t s = 0; s < irc.serversz; ++s)
+		irc_server_prepare(&irc.servers[s], &pkg.fds[i++]);
+
+	return pkg;
+}
+
+static void
+invoke(const struct irc_event *ev)
+{
+	for (size_t i = 0; i < irc.pluginsz; ++i)
+		irc_plugin_handle(&irc.plugins[i], ev);
+}
+
+static void
+pipe_flush(struct pollfd *fd)
+{
+	struct defer df = {0};
+
+	if (fd->fd != pipes[0] || !(fd->revents & POLLIN))
+		return;
+
+	if (read(fd->fd, &df, sizeof (df)) != sizeof (df))
+		err(1, "read");
+
+	df.exec(df.data);
+}
+
+static void
+process(struct pkg *pkg)
+{
+
+	if (poll(pkg->fds, pkg->fdsz, 1000) < 0)
+		err(1, "poll");
+
+	/*
+	 * We can't to what file descriptors belong to so pass every file to
+	 * all services and they must check if they are associated to it or
+	 * not.
+	 */
+	for (size_t i = 0; i < pkg->fdsz; ++i) {
+		struct irc_peer peer;
+
+		pipe_flush(&pkg->fds[i]);
+
+		for (size_t s = 0; s < irc.serversz; ++s)
+			irc_server_flush(&irc.servers[s], &pkg->fds[i]);
+
+		/* Accept new transport client. */
+		if (irc_transport_flush(&pkg->fds[i], &peer))
+			APPEND(irc.peers, irc.peersz, &peer, cmp_peer);
+
+		/* Flush clients. */
+		for (size_t p = 0; p < irc.peersz; ++p) {
+			if (!irc_peer_flush(&irc.peers[p], &pkg->fds[i])) {
+				irc_peer_finish(&irc.peers[p]);
+				REMOVE(irc.peers, irc.peersz, cmp_peer);
+			}
+		}
+	}
+
+	/*
+	 * For every server, poll any kind of new event and pass them to the
+	 * plugin unless the rules explicitly disallow us to do so.
+	 */
+	for (size_t s = 0; s < irc.serversz; ++s) {
+		struct irc_event ev;
+
+		while (irc_server_poll(&irc.servers[s], &ev))
+			invoke(&ev);
+	}
+}
+
+static void
+clean(struct pkg *pkg)
+{
+	free(pkg->fds);
+}
+
+void
+irc_init(void)
+{
+	irc_log_to_console();
+
+	if (pipe(pipes) < 0)
+		err(1, "pipe");
+}
+
+void
+irc_add_server(const struct irc_server *s)
+{
+	assert(s);
+
+	APPEND(irc.servers, irc.serversz, s, cmp_server);
+
+	irc_server_connect(&irc.servers[irc.serversz - 1]);
+}
+
+struct irc_server *
+irc_find_server(const char *name)
+{
+	return bsearch(name, irc.servers, irc.serversz, sizeof (*irc.servers),
+	    cmp_server_name);
+}
+
+void
+irc_del_server(const char *name)
+{
+	struct irc_server *s;
+
+	if (!(s = irc_find_server(name)))
+		return;
+
+	irc_server_disconnect(s);
+
+	/* Don't forget to notify plugins. */
+	invoke(&(const struct irc_event) {
+		.type = IRC_EVENT_DISCONNECT,
+		.server = s
+	});
+
+	/* Finally remove from array. */
+	REMOVE(irc.servers, irc.serversz, cmp_server);
+}
+
+void
+irc_add_plugin(const struct irc_plugin *p)
+{
+	assert(p);
+
+	APPEND(irc.plugins, irc.pluginsz, p, cmp_plugin);
+
+	irc_log_info("plugin %s: %s", p->name, p->description);
+	irc_log_info("plugin %s: version %s, from %s (%s license)", p->name,
+	    p->version, p->author, p->license);
+
+	irc_plugin_load(&irc.plugins[irc.pluginsz - 1]);
+}
+
+struct irc_plugin *
+irc_find_plugin(const char *name)
+{
+	return bsearch(name, irc.plugins, irc.pluginsz, sizeof (*irc.plugins),
+	    cmp_plugin_name);
+}
+
+void
+irc_del_plugin(const char *name)
+{
+	struct irc_plugin *p;
+
+	if (!(p = irc_find_plugin(name)))
+		return;
+
+	irc_plugin_unload(p);
+	irc_plugin_finish(p);
+
+	REMOVE(irc.plugins, irc.pluginsz, cmp_plugin);
+}
+
+void
+irc_post(void (*exec)(void *), void *data)
+{
+	struct defer df = {
+		.exec = exec,
+		.data = data
+	};
+
+	if (write(pipes[1], &df, sizeof (df)) != sizeof (df))
+		err(1, "write");
+}
+
+void
+irc_run(void)
+{
+	struct pkg pkg;
+
+	for (;;) {
+		pkg = prepare();
+		process(&pkg);
+		clean(&pkg);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/irccd.h	Thu Jan 14 17:44:48 2021 +0100
@@ -0,0 +1,64 @@
+/*
+ * irccd.h -- main irccd object
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_H
+#define IRCCD_H
+
+#include <stddef.h>
+
+struct irc_server;
+struct irc_plugin;
+struct irc_peer;
+
+extern struct irc {
+	struct irc_peer *peers;
+	size_t peersz;
+	struct irc_plugin *plugins;
+	size_t pluginsz;
+	struct irc_server *servers;
+	size_t serversz;
+} irc;
+
+void
+irc_init(void);
+
+void
+irc_add_server(const struct irc_server *);
+
+struct irc_server *
+irc_find_server(const char *);
+
+void
+irc_del_server(const char *);
+
+void
+irc_add_plugin(const struct irc_plugin *);
+
+struct irc_plugin *
+irc_find_plugin(const char *);
+
+void
+irc_del_plugin(const char *);
+
+void
+irc_post(void (*)(void *), void *);
+
+void
+irc_run(void);
+
+#endif /* !IRCCD_H */
--- a/lib/irccd/peer.c	Thu Jan 14 10:46:41 2021 +0100
+++ b/lib/irccd/peer.c	Thu Jan 14 17:44:48 2021 +0100
@@ -75,11 +75,19 @@
 	return s;
 }
 
+static int
+ok(struct irc_peer *p)
+{
+	irc_peer_send(p, "OK");
+
+	return 0;
+}
+
 /*
  * MESSAGE server channel message
  */
 static int
-cmd_message(struct irc_peer *p, char *line)
+cmd_server_message(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -91,14 +99,14 @@
 
 	irc_server_message(s, args[1], args[2]);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * ME server channel message
  */
 static int
-cmd_me(struct irc_peer *p, char *line)
+cmd_server_me(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -110,14 +118,14 @@
 
 	irc_server_me(s, args[1], args[2]);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * MODE server channel mode [limit] [user] [mask]
  */
 static int
-cmd_mode(struct irc_peer *p, char *line)
+cmd_server_mode(struct irc_peer *p, char *line)
 {
 	const char *args[6] = {0};
 	struct irc_server *s;
@@ -133,14 +141,14 @@
 	    args[5][0] ? args[5] : NULL
 	);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * NOTICE server channel message
  */
 static int
-cmd_notice(struct irc_peer *p, char *line)
+cmd_server_notice(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -152,14 +160,14 @@
 
 	irc_server_notice(s, args[1], args[2]);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * INVITE server channel target
  */
 static int
-cmd_invite(struct irc_peer *p, char *line)
+cmd_server_invite(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -171,14 +179,14 @@
 
 	irc_server_invite(s, args[1], args[2]);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * JOIN server channel [password]
  */
 static int
-cmd_join(struct irc_peer *p, char *line)
+cmd_server_join(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -190,14 +198,14 @@
 
 	irc_server_join(s, args[1], args[2][0] ? args[2] : NULL);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * KICK server channel target [reason]
  */
 static int
-cmd_kick(struct irc_peer *p, char *line)
+cmd_server_kick(struct irc_peer *p, char *line)
 {
 	const char *args[4] = {0};
 	struct irc_server *s;
@@ -209,6 +217,23 @@
 
 	irc_server_kick(s, args[1], args[2], args[3][0] ? args[3] : NULL);
 
+	return ok(p);
+}
+
+static int
+cmd_server_list(struct irc_peer *p, char *line)
+{
+	char out[IRC_BUF_MAX] = "OK ";
+
+	for (size_t i = 0; i < irc.serversz; ++i) {
+		if (strlcat(out, irc.servers[i].name, sizeof (out)) >= sizeof (out))
+			return EMSGSIZE;
+		if (i + 1 < irc.serversz && strlcat(out, " ", sizeof (out)) >= sizeof (out))
+			return EMSGSIZE;
+	}
+
+	irc_peer_send(p, out);
+
 	return 0;
 }
 
@@ -216,7 +241,7 @@
  * PART server channel [reason]
  */
 static int
-cmd_part(struct irc_peer *p, char *line)
+cmd_server_part(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -224,18 +249,18 @@
 	if (parse(line, args, 3) < 2)
 		return EINVAL;
 	if (!(s = require_server(p, args[0])))
-		return -10;
+		return 0;
 
 	irc_server_part(s, args[1], args[2][0] ? args[2] : NULL);
 
-	return 0;
+	return ok(p);
 }
 
 /*
  * TOPIC server channel topic
  */
 static int
-cmd_topic(struct irc_peer *p, char *line)
+cmd_server_topic(struct irc_peer *p, char *line)
 {
 	const char *args[3] = {0};
 	struct irc_server *s;
@@ -247,22 +272,23 @@
 
 	irc_server_topic(s, args[1], args[2]);
 
-	return 0;
+	return ok(p);
 }
 
 static const struct cmd {
 	const char *name;
 	int (*call)(struct irc_peer *, char *);
 } cmds[] = {
-	{ "INVITE",     cmd_invite      },
-	{ "JOIN",       cmd_join        },
-	{ "KICK",       cmd_kick        },
-	{ "ME",         cmd_me          },
-	{ "MESSAGE",    cmd_message     },
-	{ "MODE",       cmd_mode        },
-	{ "NOTICE",     cmd_notice      },
-	{ "PART",       cmd_part        },
-	{ "TOPIC",      cmd_topic       },
+	{ "SERVER-INVITE",      cmd_server_invite       },
+	{ "SERVER-JOIN",        cmd_server_join         },
+	{ "SERVER-KICK",        cmd_server_kick         },
+	{ "SERVER-LIST",        cmd_server_list         },
+	{ "SERVER-ME",          cmd_server_me           },
+	{ "SERVER-MESSAGE",     cmd_server_message      },
+	{ "SERVER-MODE",        cmd_server_mode         },
+	{ "SERVER-NOTICE",      cmd_server_notice       },
+	{ "SERVER-PART",        cmd_server_part         },
+	{ "SERVER-TOPIC",       cmd_server_topic        },
 };
 
 static int
@@ -271,7 +297,7 @@
 	const char *key = d1;
 	const struct cmd *cmd = d2;
 
-	return strncmp(cmd->name, key, strlen(cmd->name));
+	return strncmp(key, cmd->name, strlen(cmd->name));
 }
 
 static const struct cmd *
@@ -288,10 +314,8 @@
 
 	if (!c)
 		irc_peer_send(p, "command not found");
-	else if ((er = c->call(p, line)) != 0 && er != -1)
+	else if ((er = c->call(p, line)) != 0)
 		irc_peer_send(p, "%s", strerror(errno));
-	else
-		irc_peer_send(p, "OK");
 }
 
 static void
--- a/lib/irccd/transport.c	Thu Jan 14 10:46:41 2021 +0100
+++ b/lib/irccd/transport.c	Thu Jan 14 17:44:48 2021 +0100
@@ -40,12 +40,14 @@
 {
 	assert(path);
 
-	/* Silently remove the file first. */
+	addr.sun_family = PF_LOCAL;
+
 	if (strlcpy(addr.sun_path, path, sizeof (addr.sun_path)) >= sizeof (addr.sun_path)) {
 		errno = ENAMETOOLONG;
 		goto err;
 	}
 
+	/* Silently remove the file first. */
 	unlink(addr.sun_path);
 
 	if ((fd = socket(PF_LOCAL, SOCK_STREAM, 0)) < 0)