changeset 956:5e682f1cebcc

irccd: improve SSL support and add basic whois
author David Demelier <markand@malikania.fr>
date Thu, 21 Jan 2021 09:49:42 +0100
parents 9b167c5c4b78
children 3167c51f0c84
files CMakeLists.txt extern/libcompat/CMakeLists.txt extern/libcompat/src/compat.h.in extern/libcompat/src/strtok_r.c irccdctl/main.c lib/irccd/channel.h lib/irccd/event.c lib/irccd/event.h lib/irccd/js-plugin.c lib/irccd/jsapi-server.c lib/irccd/limits.h lib/irccd/server.c lib/irccd/server.h
diffstat 13 files changed, 462 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Jan 20 17:01:51 2021 +0100
+++ b/CMakeLists.txt	Thu Jan 21 09:49:42 2021 +0100
@@ -32,7 +32,7 @@
 option(IRCCD_WITH_TESTS "Enable unit tests" Off)
 
 if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
-	set(CMAKE_C_FLAGS "-Wall -Wextra")
+	set(CMAKE_C_FLAGS "-Wall -Wextra ${CMAKE_C_FLAGS}")
 endif ()
 
 include(GNUInstallDirs)
--- a/extern/libcompat/CMakeLists.txt	Wed Jan 20 17:01:51 2021 +0100
+++ b/extern/libcompat/CMakeLists.txt	Thu Jan 21 09:49:42 2021 +0100
@@ -60,6 +60,7 @@
 	strdup
 	strndup
 	strnlen
+	strtok_r
 )
 
 set(
--- a/extern/libcompat/src/compat.h.in	Wed Jan 20 17:01:51 2021 +0100
+++ b/extern/libcompat/src/compat.h.in	Thu Jan 21 09:49:42 2021 +0100
@@ -16,6 +16,7 @@
 #cmakedefine COMPAT_HAVE_STRNDUP
 #cmakedefine COMPAT_HAVE_STRNLEN
 #cmakedefine COMPAT_HAVE_STRSEP
+#cmakedefine COMPAT_HAVE_STRTOK_R
 #cmakedefine COMPAT_HAVE_VERR
 #cmakedefine COMPAT_HAVE_VERRC
 #cmakedefine COMPAT_HAVE_VERRX
@@ -169,4 +170,9 @@
 strsep(char **, const char *);
 #endif
 
+#ifndef COMPAT_HAVE_STRTOK_R
+char *
+strtok_r(char *, const char *, char **);
+#endif
+
 #endif /* !LIBCOMPAT_COMPAT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/libcompat/src/strtok_r.c	Thu Jan 21 09:49:42 2021 +0100
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 1988 Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+char *
+strtok_r(char *s, const char *delim, char **last)
+{
+	const char *spanp;
+	int c, sc;
+	char *tok;
+
+	if (s == NULL && (s = *last) == NULL)
+		return (NULL);
+
+	/*
+	 * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
+	 */
+cont:
+	c = *s++;
+	for (spanp = delim; (sc = *spanp++) != 0;) {
+		if (c == sc)
+			goto cont;
+	}
+
+	if (c == 0) {		/* no non-delimiter characters */
+		*last = NULL;
+		return (NULL);
+	}
+	tok = s - 1;
+
+	/*
+	 * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
+	 * Note that delim must have one NUL; we stop if we see that, too.
+	 */
+	for (;;) {
+		c = *s++;
+		spanp = delim;
+		do {
+			if ((sc = *spanp++) == c) {
+				if (c == 0)
+					s = NULL;
+				else
+					s[-1] = '\0';
+				*last = s;
+				return (tok);
+			}
+		} while (sc != 0);
+	}
+	/* NOTREACHED */
+}
--- a/irccdctl/main.c	Wed Jan 20 17:01:51 2021 +0100
+++ b/irccdctl/main.c	Thu Jan 21 09:49:42 2021 +0100
@@ -51,9 +51,10 @@
 
 	while (!(nl = strstr(in, "\n"))) {
 		char buf[IRC_BUF_MAX] = {0};
+		ssize_t nr;
 
-		if (recv(sock, buf, sizeof (buf) - 1, 0) <= 0)
-			err(1, "abort");
+		if ((nr = recv(sock, buf, sizeof (buf) - 1, 0)) <= 0)
+			errc(1, nr == 0 ? ECONNRESET : errno, "abort");
 		if (strlcat(in, buf, sizeof (in)) >= sizeof (in))
 			errc(1, EMSGSIZE, "abort");
 	}
@@ -236,16 +237,80 @@
 }
 
 static void
+show_connect(char *line)
+{
+	const char *args[2] = {0};
+
+	if (irc_util_split(line, args, 2) == 2) {
+		printf("event:     onConnect\n");
+		printf("server:    %s\n", args[0]);
+	}
+}
+
+static void
+show_disconnect(char *line)
+{
+	const char *args[2] = {0};
+
+	if (irc_util_split(line, args, 2) == 2) {
+		printf("event:     onDisonnect\n");
+		printf("server:    %s\n", args[0]);
+	}
+}
+
+static void
 show_invite(char *line)
 {
 	const char *args[5] = {0};
 
 	if (irc_util_split(line, args, 5) == 5) {
 		printf("event:     onInvite\n");
-		printf("server:    %s", args[1]);
-		printf("origin:    %s", args[2]);
-		printf("channel:   %s", args[3]);
-		printf("nickname:  %s", args[4]);
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("nickname:  %s\n", args[4]);
+	}
+}
+
+static void
+show_join(char *line)
+{
+	const char *args[4] = {0};
+
+	if (irc_util_split(line, args, 4) == 4) {
+		printf("event:     onJoin\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+	}
+}
+
+static void
+show_kick(char *line)
+{
+	const char *args[6] = {0};
+
+	if (irc_util_split(line, args, 6) >= 5) {
+		printf("event:     onKick\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("target:    %s\n", args[4]);
+		printf("reason:    %s\n", args[5] ? args[5] : "");
+	}
+}
+
+static void
+show_me(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) == 5) {
+		printf("event:     onMe\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("message:   %s\n", args[4]);
 	}
 }
 
@@ -263,12 +328,114 @@
 	}
 }
 
+static void
+show_mode(char *line)
+{
+	const char *args[8] = {0};
+
+	if (irc_util_split(line, args, 8) >= 5) {
+		printf("event:     onMode\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("mode:      %s\n", args[4]);
+		printf("limit:     %s\n", (args[5] ? args[5] : ""));
+		printf("user:      %s\n", (args[6] ? args[6] : ""));
+		printf("mask:      %s\n", (args[7] ? args[7] : ""));
+	}
+}
+
+static void
+show_nick(char *line)
+{
+	const char *args[4] = {0};
+
+	if (irc_util_split(line, args, 4) == 4) {
+		printf("event:     onNick\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("nickname:  %s\n", args[3]);
+	}
+}
+
+static void
+show_notice(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) == 5) {
+		printf("event:     onNotice\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("message:   %s\n", args[4]);
+	}
+}
+
+static void
+show_part(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) >= 4) {
+		printf("event:     onPart\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("reason:    %s\n", (args[4] ? args[4] : ""));
+	}
+}
+
+static void
+show_topic(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) >= 4) {
+		printf("event:     onTopic\n");
+		printf("server:    %s\n", args[1]);
+		printf("origin:    %s\n", args[2]);
+		printf("channel:   %s\n", args[3]);
+		printf("topic:     %s\n", args[4]);
+	}
+}
+
+static void
+show_whois(char *line)
+{
+	const char *args[6] = {0};
+	char *p, *token;
+
+	if (irc_util_split(line, args, 6) >= 4) {
+		printf("event:     onWhois\n");
+		printf("server:    %s\n", args[1]);
+		printf("nickname:  %s\n", args[2]);
+		printf("username:  %s\n", args[3]);
+		printf("hostname:  %s\n", args[4]);
+		printf("username:  %s\n", args[5]);
+		printf("channels:  ");
+
+		
+	}
+}
+
 static const struct {
 	const char *event;
 	void (*show)(char *);
 } watchtable[] = {
-	{ "EVENT-INVITE", show_invite },
-	{ "EVENT-MESSAGE", show_message }
+	{ "EVENT-CONNECT",      show_connect    },
+	{ "EVENT-DISCONNECT",   show_disconnect },
+	{ "EVENT-INVITE",       show_invite     },
+	{ "EVENT-JOIN",         show_join       },
+	{ "EVENT-KICK",         show_kick       },
+	{ "EVENT-MESSAGE",      show_message    },
+	{ "EVENT-ME",           show_me         },
+	{ "EVENT-MODE",         show_mode       },
+	{ "EVENT-NICK",         show_nick       },
+	{ "EVENT-NOTICE",       show_notice     },
+	{ "EVENT-PART",         show_part       },
+	{ "EVENT-TOPIC",        show_topic      },
+	{ "EVENT-WHOIS",        show_whois      }
 };
 
 static void
@@ -277,6 +444,7 @@
 	for (size_t i = 0; i < IRC_UTIL_SIZE(watchtable); ++i) {
 		if (strncmp(watchtable[i].event, ev, strlen(watchtable[i].event)) == 0) {
 			watchtable[i].show(ev);
+			printf("\n");
 			break;
 		}
 	}
--- a/lib/irccd/channel.h	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/channel.h	Thu Jan 21 09:49:42 2021 +0100
@@ -24,9 +24,6 @@
 
 #include "limits.h"
 
-#define IRC_CHANNEL_NAME_MAX            128
-#define IRC_CHANNEL_PASSWORD_MAX        128
-
 struct irc_channel_user {
 	char nickname[IRC_NICKNAME_MAX];
 	char mode;
--- a/lib/irccd/event.c	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/event.c	Thu Jan 21 09:49:42 2021 +0100
@@ -113,6 +113,12 @@
 	int written = 0;
 
 	switch (ev->type) {
+	case IRC_EVENT_CONNECT:
+		written = snprintf(str, strsz, "EVENT-CONNECT %s", ev->server->name);
+		break;
+	case IRC_EVENT_DISCONNECT:
+		written = snprintf(str, strsz, "EVENT-DISCONNECT %s", ev->server->name);
+		break;
 	case IRC_EVENT_INVITE:
 		written = snprintf(str, strsz, "EVENT-INVITE %s %s %s %s", ev->server->name,
 		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
@@ -123,7 +129,8 @@
 		break;
 	case IRC_EVENT_KICK:
 		written = snprintf(str, strsz, "EVENT-KICK %s %s %s %s %s", ev->server->name,
-		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1], ev->msg.args[2]);
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1],
+		    ev->msg.args[2] ? ev->msg.args[2] : "");
 		break;
 	case IRC_EVENT_ME:
 		written = snprintf(str, strsz, "EVENT-ME %s %s %s %s", ev->server->name,
@@ -134,9 +141,12 @@
 		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
 		break;
 	case IRC_EVENT_MODE:
-		written = snprintf(str, strsz, "EVENT-MODE %s %s %s %s %s %s %s", ev->server->name,
-		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1], ev->msg.args[2],
-		    ev->msg.args[3], ev->msg.args[4]);
+		written = snprintf(str, strsz, "EVENT-MODE %s %s %s %s %s %s %s",
+		    ev->server->name, ev->msg.prefix, ev->msg.args[0],
+		    ev->msg.args[1] ? ev->msg.args[1] : "",
+		    ev->msg.args[2] ? ev->msg.args[2] : "",
+		    ev->msg.args[3] ? ev->msg.args[3] : "",
+		    ev->msg.args[4] ? ev->msg.args[4] : "");
 		break;
 	case IRC_EVENT_NICK:
 		written = snprintf(str, strsz, "EVENT-NICK %s %s %s", ev->server->name,
@@ -148,15 +158,38 @@
 		break;
 	case IRC_EVENT_PART:
 		written = snprintf(str, strsz, "EVENT-PART %s %s %s %s", ev->server->name,
-		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		    ev->msg.prefix, ev->msg.args[0],
+		    ev->msg.args[1] ? ev->msg.args[1] : "");
 		break;
 	case IRC_EVENT_TOPIC:
 		written = snprintf(str, strsz, "EVENT-TOPIC %s %s %s %s", ev->server->name,
 		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
 		break;
+	case IRC_EVENT_WHOIS:
+		snprintf(str, strsz, "EVENT-WHOIS %s %s %s %s %s ",
+		    ev->server->name, ev->whois->nickname, ev->whois->username,
+		    ev->whois->realname, ev->whois->hostname);
+
+		for (size_t i = 0; i < ev->whois->channelsz; ++i) {
+			size_t s;
+
+			/* Concat channel and their modes. */
+			strlcat(str, (char []) {ev->whois->channels[i].mode, '\0' }, strsz);
+			strlcat(str, ev->whois->channels[i].channel, strsz);
+
+			if ((s = strlcat(str, " ", strsz)) >= strsz)
+				goto emsgsize;
+
+			written = s;
+		}
+		break;
 	default:
 		break;
 	}
 
 	return written > 0;
+
+emsgsize:
+	errno = EMSGSIZE;
+	return false;
 }
--- a/lib/irccd/event.h	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/event.h	Thu Jan 21 09:49:42 2021 +0100
@@ -55,7 +55,10 @@
 struct irc_event {
 	enum irc_event_type type;
 	struct irc_server *server;
-	struct irc_event_msg msg;
+	union {
+		struct irc_event_msg msg;
+		struct irc_server_whois *whois;
+	};
 };
 
 bool
--- a/lib/irccd/js-plugin.c	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/js-plugin.c	Thu Jan 21 09:49:42 2021 +0100
@@ -97,6 +97,31 @@
 	}
 }
 
+static void
+push_whois(duk_context *ctx, const struct irc_event *ev)
+{
+	duk_push_object(ctx);
+	duk_push_string(ctx, ev->whois->nickname);
+	duk_put_prop_string(ctx, -2, "nickname");
+	duk_push_string(ctx, ev->whois->username);
+	duk_put_prop_string(ctx, -2, "username");
+	duk_push_string(ctx, ev->whois->realname);
+	duk_put_prop_string(ctx, -2, "realname");
+	duk_push_string(ctx, ev->whois->hostname);
+	duk_put_prop_string(ctx, -2, "hostname");
+	duk_push_array(ctx);
+	for (size_t i = 0; i < ev->whois->channelsz; ++i) {
+		printf("[%s] = [%c]\n", ev->whois->channels[i].channel, ev->whois->channels[i].mode);
+		duk_push_object(ctx);
+		duk_push_string(ctx, ev->whois->channels[i].channel);
+		duk_put_prop_string(ctx, -2, "channel");
+		duk_push_sprintf(ctx, "%c", ev->whois->channels[i].mode);
+		duk_put_prop_string(ctx, -2, "mode");
+		duk_put_prop_index(ctx, -2, i);
+	}
+	duk_put_prop_string(ctx, -2, "channels");
+}
+
 static const char **
 get_table(duk_context *ctx, const char *name, char ***ptable)
 {
@@ -315,7 +340,7 @@
 		break;
 	case IRC_EVENT_NAMES:
 		call(plg, "onNames", "Ss x", ev->server, ev->msg.args[1],
-		    push_names);
+		    push_names, ev);
 		break;
 	case IRC_EVENT_NICK:
 		call(plg, "onNick", "Ss s", ev->server, ev->msg.prefix,
@@ -334,6 +359,7 @@
 		    ev->msg.args[0], ev->msg.args[1]);
 		break;
 	case IRC_EVENT_WHOIS:
+		call(plg, "onWhois", "Sx", ev->server, push_whois, ev);
 		break;
 	default:
 		break;
--- a/lib/irccd/jsapi-server.c	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/jsapi-server.c	Thu Jan 21 09:49:42 2021 +0100
@@ -415,15 +415,15 @@
 static duk_ret_t
 Server_prototype_whois(duk_context *ctx)
 {
-	(void)ctx;
+	struct irc_server *s = self(ctx);
+	const char *target = duk_require_string(ctx, 0);
+
 #if 0
 	if (target.empty())
 		throw server_error(server_error::invalid_nickname);
 #endif
 
-#if 0
 	duk_push_boolean(ctx, irc_server_whois(s, target));
-#endif
 
 	return 1;
 }
--- a/lib/irccd/limits.h	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/limits.h	Thu Jan 21 09:49:42 2021 +0100
@@ -19,7 +19,10 @@
 #ifndef IRCCD_LIMITS_H
 #define IRCCD_LIMITS_H
 
-/* IRC limits. */
+/*
+ * Those were IRC limits but not strictly following RFC 1459 because lots of
+ * servers allow higher limits.
+ */
 #define IRC_NICKNAME_MAX        32
 #define IRC_USERNAME_MAX        32
 #define IRC_REALNAME_MAX        64
@@ -29,11 +32,11 @@
 #define IRC_USERMODES_MAX       16
 
 #define IRC_MESSAGE_MAX         512
-#define IRC_ARGS_MAX            16
+#define IRC_ARGS_MAX            32
 
 /* Network limits. */
 #define IRC_HOST_MAX            32
-#define IRC_BUF_MAX             8192
+#define IRC_BUF_MAX             128000
 
 /* Types limits. */
 #define IRC_NAME_MAX            16
--- a/lib/irccd/server.c	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/server.c	Thu Jan 21 09:49:42 2021 +0100
@@ -303,6 +303,56 @@
 	ev->type = IRC_EVENT_NAMES;
 }
 
+static void
+handle_whoisuser(struct irc_server *s, struct irc_event *ev)
+{
+	(void)ev;
+
+	strlcpy(s->whois.nickname, ev->msg.args[1], sizeof (s->whois.nickname));
+	strlcpy(s->whois.username, ev->msg.args[2], sizeof (s->whois.username));
+	strlcpy(s->whois.hostname, ev->msg.args[3], sizeof (s->whois.hostname));
+	strlcpy(s->whois.realname, ev->msg.args[5], sizeof (s->whois.realname));
+}
+
+static void
+add_whois_channel(struct irc_server *s, const char *channel)
+{
+	char mode = 0;
+
+	s->whois.channels = irc_util_reallocarray(s->whois.channels,
+	    ++s->whois.channelsz, sizeof (*s->whois.channels));
+
+	/* TODO: split this to refactor add_nick. */
+	for (size_t i = 0; i < IRC_UTIL_SIZE(s->prefixes); ++i) {
+		if (channel[0] == s->prefixes[i].token) {
+			mode = s->prefixes[i].mode;
+			++channel;
+			break;
+		}
+	}
+
+	s->whois.channels[s->whois.channelsz - 1].mode = mode;
+	strlcpy(s->whois.channels[s->whois.channelsz - 1].channel, channel,
+	    sizeof (s->whois.channels[0].channel));
+}
+
+static void
+handle_whoischannels(struct irc_server *s, struct irc_event *ev)
+{
+	char *p, *token;
+
+	for (p = ev->msg.args[2]; (token = strtok_r(p, " ", &p)); )
+		if (strlen(token) > 0)
+			add_whois_channel(s, token);
+}
+
+static void
+handle_endofwhois(struct irc_server *s, struct irc_event *ev)
+{
+	ev->type = IRC_EVENT_WHOIS;
+	ev->whois = &s->whois;
+}
+
 static const struct handler {
 	const char *command;
 	void (*handle)(struct irc_server *, struct irc_event *);
@@ -310,6 +360,9 @@
 	/* Must be kept ordered. */
 	{ "001",        handle_connect          },
 	{ "005",        handle_support          },
+	{ "311",        handle_whoisuser        },
+	{ "318",        handle_endofwhois       },
+	{ "319",        handle_whoischannels    },
 	{ "353",        handle_names            },
 	{ "366",        handle_endofnames       },
 	{ "INVITE",     handle_invite           },
@@ -419,9 +472,11 @@
 
 	switch ((r = SSL_get_error(s->ssl, ret))) {
 	case SSL_ERROR_WANT_READ:
+		printf("new ssl state: %d\n", s->ssl_state);
 		s->ssl_state = IRC_SERVER_SSL_NEED_READ;
 		break;
 	case SSL_ERROR_WANT_WRITE:
+		printf("new ssl state: %d\n", s->ssl_state);
 		s->ssl_state = IRC_SERVER_SSL_NEED_WRITE;
 		break;
 	case SSL_ERROR_SSL:
@@ -465,8 +520,6 @@
 		handshake(s);
 	else {
 #if defined(IRCCD_WITH_SSL)
-		int r;
-
 		if (!s->ctx)
 			s->ctx = SSL_CTX_new(TLS_method());
 		if (!s->ssl) {
@@ -474,10 +527,8 @@
 			SSL_set_fd(s->ssl, s->fd);
 		}
 
-		if ((r = SSL_connect(s->ssl)) > 0)
-			handshake(s);
-
-		update(s, r);
+		SSL_set_connect_state(s->ssl);
+		handshake(s);
 #endif
 	}
 }
@@ -543,28 +594,50 @@
 	}
 }
 
+static size_t
+input_ssl(struct irc_server *s, char *dst, size_t dstsz)
+{
+	int nr;
+
+	if ((nr = SSL_read(s->ssl, dst, dstsz)) <= 0) {
+		update(s, nr);
+		return 0;
+	}
+
+	s->ssl_state = IRC_SERVER_SSL_NONE;
+
+	return nr;
+}
+
+static size_t
+input_clear(struct irc_server *s, char *buf, size_t bufsz)
+{
+	ssize_t nr;
+
+	if ((nr = recv(s->fd, buf, bufsz, 0)) <= 0) {
+		clear(s);
+		return 0;
+	}
+
+	return nr;
+}
+
 static void
 input(struct irc_server *s)
 {
-	char buf[IRC_MESSAGE_MAX] = {0};
-	ssize_t nr = 0;
+	size_t len = strlen(s->in);
+	size_t cap = sizeof (s->in) - len - 1;
+	size_t nr = 0;
 
 	if (s->flags & IRC_SERVER_FLAGS_SSL) {
 #if defined(IRCCD_WITH_SSL)
-		nr = SSL_read(s->ssl, buf, sizeof (buf) - 1);
-		update(s, nr);
+		nr = input_ssl(s, s->in + len, cap);
 #endif
-	} else {
-		if ((nr = recv(s->fd, buf, sizeof (buf) - 1, 0)) < 0)
-			clear(s);
-	}
+	} else
+		nr = input_clear(s, s->in + len, cap);
 
-	if (nr > 0) {
-		if (strlcat(s->in, buf, sizeof (s->in)) >= sizeof (s->in)) {
-			irc_log_warn("server %s: input buffer too long", s->name);
-			clear(s);
-		}
-	}
+	if (nr > 0)
+		s->in[len + nr] = '\0';
 }
 
 static void
@@ -949,6 +1022,19 @@
 	return irc_server_send(s, "NOTICE %s: %s", channel, message);
 }
 
+bool
+irc_server_whois(struct irc_server *s, const char *target)
+{
+	assert(s);
+	assert(target);
+
+	/* Cleanup previous result. */
+	free(s->whois.channels);
+	memset(&s->whois, 0, sizeof (s->whois));
+
+	return irc_server_send(s, "WHOIS %s", target);
+}
+
 void
 irc_server_incref(struct irc_server *s)
 {
@@ -965,6 +1051,7 @@
 
 	if (--s->refc == 0) {
 		clear(s);
+		free(s->whois.channels);
 		free(s->channels);
 		free(s);
 	}
--- a/lib/irccd/server.h	Wed Jan 20 17:01:51 2021 +0100
+++ b/lib/irccd/server.h	Thu Jan 21 09:49:42 2021 +0100
@@ -67,6 +67,18 @@
 
 #endif
 
+struct irc_server_whois {
+	char nickname[IRC_NICKNAME_MAX];
+	char username[IRC_USERNAME_MAX];
+	char realname[IRC_REALNAME_MAX];
+	char hostname[IRC_HOST_MAX];
+	struct {
+		char channel[IRC_CHANNEL_MAX];
+		char mode;
+	} *channels;
+	size_t channelsz;
+};
+
 struct irc_server {
 	/* Connection settings. */
 	char name[IRC_NAME_MAX];
@@ -103,6 +115,9 @@
 	enum irc_server_ssl_state ssl_state;
 #endif
 
+	/* Whois being stored. */
+	struct irc_server_whois whois;
+
 	/* Reference count. */
 	size_t refc;
 	struct irc_server *next;
@@ -172,6 +187,9 @@
 bool
 irc_server_notice(struct irc_server *, const char *, const char *);
 
+bool
+irc_server_whois(struct irc_server *, const char *);
+
 void
 irc_server_incref(struct irc_server *);