diff options
| author | C. McEnroe | 2020-02-01 01:18:01 -0500 | 
|---|---|---|
| committer | C. McEnroe | 2020-02-01 01:18:01 -0500 | 
| commit | 843160236381d0c76bef1eac89e556920d700a9d (patch) | |
| tree | 469dc9d9e9c6b6928984f3fad8a545ef27385184 | |
| parent | f76145645e6e183c53c5601294c985246c00fa92 (diff) | |
Blindly implement login flow
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Makefile | 18 | ||||
| -rw-r--r-- | chat.c | 73 | ||||
| -rw-r--r-- | chat.h | 102 | ||||
| -rw-r--r-- | handle.c | 163 | ||||
| -rw-r--r-- | irc.c | 205 | 
6 files changed, 563 insertions, 0 deletions
| diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab80afd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +catgirl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30bcf66 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +LIBRESSL_PREFIX = /usr/local +CFLAGS += -I${LIBRESSL_PREFIX}/include +LDFLAGS += -L${LIBRESSL_PREFIX}/lib + +CFLAGS += -std=c11 -Wall -Wextra -Wpedantic +LDLIBS = -lcrypto -ltls + +OBJS += chat.o +OBJS += handle.o +OBJS += irc.o + +catgirl: ${OBJS} +	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ + +${OBJS}: chat.h + +clean: +	rm -f catgirl ${OBJS} @@ -0,0 +1,73 @@ +/* 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/>. + */ + +#include <err.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <sysexits.h> +#include <unistd.h> + +#include "chat.h" + +struct Self self; + +int main(int argc, char *argv[]) { +	bool insecure = false; +	const char *host = NULL; +	const char *port = "6697"; +	const char *cert = NULL; +	const char *priv = NULL; + +	bool sasl = false; +	const char *pass = NULL; +	const char *nick = NULL; +	const char *user = NULL; +	const char *real = NULL; + +	int opt; +	while (0 < (opt = getopt(argc, argv, "!a:c:eh:j:k:n:p:r:u:w:"))) { +		switch (opt) { +			break; case '!': insecure = true; +			break; case 'a': sasl = true; self.plain = optarg; +			break; case 'c': cert = optarg; +			break; case 'e': sasl = true; +			break; case 'h': host = optarg; +			break; case 'j': self.join = optarg; +			break; case 'k': priv = optarg; +			break; case 'n': nick = optarg; +			break; case 'p': port = optarg; +			break; case 'r': real = optarg; +			break; case 'u': user = optarg; +			break; case 'w': pass = optarg; +		} +	} +	if (!host) errx(EX_USAGE, "host required"); + +	if (!nick) nick = getenv("USER"); +	if (!nick) errx(EX_CONFIG, "USER unset"); +	if (!user) user = nick; +	if (!real) real = nick; + +	ircConfig(insecure, cert, priv); +	 +	int irc = ircConnect(host, port); +	if (pass) ircFormat("PASS :%s\r\n", pass); +	if (sasl) ircFormat("CAP REQ :sasl\r\n"); +	ircFormat("CAP LS\r\n"); +	ircFormat("NICK :%s\r\n", nick); +	ircFormat("USER %s 0 * :%s\r\n", user, real); +} @@ -0,0 +1,102 @@ +/* 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/>. + */ + +#include <stdbool.h> + +#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) +#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit + +typedef unsigned char byte; + +#define ENUM_CAP \ +	X("sasl", CapSASL) \ +	X("server-time", CapServerTime) \ +	X("userhost-in-names", CapUserhostInNames) + +enum Cap { +#define X(name, id) BIT(id), +	ENUM_CAP +#undef X +}; + +extern struct Self { +	enum Cap caps; +	char *plain; +	char *nick; +	const char *join; +} self; + +#define ENUM_TAG \ +	X("time", TagTime) + +enum Tag { +#define X(name, id) id, +	ENUM_TAG +#undef X +	TagCap, +}; + +enum { ParamCap = 15 }; +struct Message { +	char *tags[TagCap]; +	char *nick; +	char *user; +	char *host; +	char *cmd; +	char *params[ParamCap]; +}; + +void ircConfig(bool insecure, const char *cert, const char *priv); +int ircConnect(const char *host, const char *port); +void ircRecv(void); +void ircSend(const char *ptr, size_t len); +void ircFormat(const char *format, ...) +	__attribute__((format(printf, 1, 2))); + +void handle(struct Message msg); + +#define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) + +static const char Base64[64] = { +	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +}; + +static inline void base64(char *dst, const byte *src, size_t len) { +	size_t i = 0; +	while (len > 2) { +		dst[i++] = Base64[0x3F & (src[0] >> 2)]; +		dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)]; +		dst[i++] = Base64[0x3F & (src[1] << 2 | src[2] >> 6)]; +		dst[i++] = Base64[0x3F & src[2]]; +		src += 3; +		len -= 3; +	} +	if (len) { +		dst[i++] = Base64[0x3F & (src[0] >> 2)]; +		if (len > 1) { +			dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)]; +			dst[i++] = Base64[0x3F & (src[1] << 2)]; +		} else { +			dst[i++] = Base64[0x3F & (src[0] << 4)]; +			dst[i++] = '='; +		} +		dst[i++] = '='; +	} +	dst[i] = '\0'; +} + +// Defined in libcrypto if missing from libc: +void explicit_bzero(void *b, size_t len); diff --git a/handle.c b/handle.c new file mode 100644 index 0000000..4084525 --- /dev/null +++ b/handle.c @@ -0,0 +1,163 @@ +/* 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/>. + */ + +#include <err.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> + +#include "chat.h" + +static const char *CapNames[] = { +#define X(name, id) [id] = name, +	ENUM_CAP +#undef X +}; + +static enum Cap capParse(const char *list) { +	enum Cap caps = 0; +	while (*list) { +		enum Cap cap = 0; +		size_t len = strcspn(list, " "); +		for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { +			if (len != strlen(CapNames[i])) continue; +			if (strncmp(list, CapNames[i], len)) continue; +			cap = 1 << i; +			break; +		} +		caps |= cap; +		list += len; +		if (*list) list++; +	} +	return caps; +} + +static const char *capList(enum Cap caps) { +	static char buf[1024]; +	buf[0] = '\0'; +	for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { +		if (caps & (1 << i)) { +			if (buf[0]) strlcat(buf, " ", sizeof(buf)); +			strlcat(buf, CapNames[i], sizeof(buf)); +		} +	} +	return buf; +} + +static void set(char **field, const char *value) { +	free(*field); +	*field = strdup(value); +	if (!*field) err(EX_OSERR, "strdup"); +} + +typedef void Handler(struct Message *msg); + +static void require(const struct Message *msg, bool origin, size_t len) { +	if (origin && !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); +	} +} + +static void handleCap(struct Message *msg) { +	require(msg, false, 3); +	enum Cap caps = capParse(msg->params[2]); +	if (!strcmp(msg->params[1], "LS")) { +		caps &= ~CapSASL; +		ircFormat("CAP REQ :%s\r\n", capList(caps)); +	} else if (!strcmp(msg->params[1], "ACK")) { +		self.caps |= caps; +		if (caps & CapSASL) { +			ircFormat("AUTHENTICATE %s\r\n", (self.plain ? "PLAIN" : "EXTERNAL")); +		} +		if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); +	} else if (!strcmp(msg->params[1], "NAK")) { +		errx(EX_CONFIG, "server does not support %s", msg->params[2]); +	} +} + +static void handleAuthenticate(struct Message *msg) { +	(void)msg; +	if (!self.plain) { +		ircFormat("AUTHENTICATE +\r\n"); +		return; +	} + +	byte buf[299]; +	size_t len = 1 + strlen(self.plain); +	if (sizeof(buf) < len) errx(EX_CONFIG, "SASL PLAIN is too long"); +	buf[0] = 0; +	for (size_t i = 0; self.plain[i]; ++i) { +		buf[1 + i] = (self.plain[i] == ':' ? 0 : self.plain[i]); +	} + +	char b64[BASE64_SIZE(sizeof(buf))]; +	base64(b64, buf, len); +	ircFormat("AUTHENTICATE "); +	ircSend(b64, BASE64_SIZE(len)); +	ircFormat("\r\n"); + +	explicit_bzero(b64, sizeof(b64)); +	explicit_bzero(buf, sizeof(buf)); +	explicit_bzero(self.plain, strlen(self.plain)); +} + +static void handleReplyLoggedIn(struct Message *msg) { +	(void)msg; +	ircFormat("CAP END\r\n"); +} + +static void handleErrorSASLFail(struct Message *msg) { +	require(msg, false, 2); +	errx(EX_CONFIG, "%s", msg->params[1]); +} + +static void handleReplyWelcome(struct Message *msg) { +	require(msg, false, 1); +	set(&self.nick, msg->params[0]); +	if (self.join) ircFormat("JOIN :%s\r\n", self.join); +} + +static const struct Handler { +	const char *cmd; +	Handler *fn; +} Handlers[] = { +	{ "001", handleReplyWelcome }, +	{ "900", handleReplyLoggedIn }, +	{ "904", handleErrorSASLFail }, +	{ "905", handleErrorSASLFail }, +	{ "906", handleErrorSASLFail }, +	{ "AUTHENTICATE", handleAuthenticate }, +	{ "CAP", handleCap }, +}; + +static int compar(const void *cmd, const void *_handler) { +	const struct Handler *handler = _handler; +	return strcmp(cmd, handler->cmd); +} + +void handle(struct Message msg) { +	if (!msg.cmd) return; +	const struct Handler *handler = bsearch( +		msg.cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar +	); +	if (handler) handler->fn(&msg); +} @@ -0,0 +1,205 @@ +/* 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/>. + */ + +#include <assert.h> +#include <err.h> +#include <netdb.h> +#include <netinet/in.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sysexits.h> +#include <tls.h> +#include <unistd.h> + +#include "chat.h" + +struct tls *client; + +void ircConfig(bool insecure, const char *cert, const char *priv) { +	struct tls_config *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 (cert) { +		error = tls_config_set_keypair_file(config, cert, (priv ? priv : cert)); +		if (error) { +			errx( +				EX_SOFTWARE, "tls_config_set_keypair_file: %s", +				tls_config_error(config) +			); +		} +	} + +	client = tls_client(); +	if (!client) errx(EX_SOFTWARE, "tls_client"); + +	error = tls_configure(client, config); +	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); +	tls_config_free(config); +} + +int ircConnect(const char *host, const char *port) { +	assert(client); + +	struct addrinfo *head; +	struct addrinfo hints = { +		.ai_family = AF_UNSPEC, +		.ai_socktype = SOCK_STREAM, +		.ai_protocol = IPPROTO_TCP, +	}; +	int error = getaddrinfo(host, port, &hints, &head); +	if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error)); + +	int sock = -1; +	for (struct addrinfo *ai = head; ai; ai = ai->ai_next) { +		sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); +		if (sock < 0) err(EX_OSERR, "socket"); + +		error = connect(sock, ai->ai_addr, ai->ai_addrlen); +		if (!error) break; + +		close(sock); +		sock = -1; +	} +	if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port); +	freeaddrinfo(head); + +	error = tls_connect_socket(client, sock, host); +	if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client)); + +	error = tls_handshake(client); +	if (error) errx(EX_PROTOCOL, "tls_handshake: %s", tls_error(client)); + +	return sock; +} + +void ircSend(const char *ptr, size_t len) { +	assert(client); +	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; +	} +} + +void ircFormat(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)); +	ircSend(buf, len); +} + +static const char *TagNames[TagCap] = { +#define X(name, id) [id] = name, +	ENUM_TAG +#undef X +}; + +static void unescape(char *tag) { +	for (;;) { +		tag = strchr(tag, '\\'); +		if (!tag) break; +		switch (tag[1]) { +			break; case ':': tag[1] = ';'; +			break; case 's': tag[1] = ' '; +			break; case 'r': tag[1] = '\r'; +			break; case 'n': tag[1] = '\n'; +		} +		memmove(tag, &tag[1], strlen(&tag[1]) + 1); +		if (tag[0]) tag = &tag[1]; +	} +} + +static struct Message parse(char *line) { +	struct Message msg = { .cmd = NULL }; + +	if (line[0] == '@') { +		char *tags = 1 + strsep(&line, " "); +		while (tags) { +			char *tag = strsep(&tags, ";"); +			char *key = strsep(&tag, "="); +			for (size_t i = 0; i < TagCap; ++i) { +				if (strcmp(key, TagNames[i])) continue; +				unescape(tag); +				msg.tags[i] = tag; +				break; +			} +		} +	} + +	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; +} + +void ircRecv(void) { +	static char buf[8191 + 512]; +	static size_t len = 0; + +	assert(client); +	ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); +	if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) return; +	if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); +	if (!ret) errx(EX_PROTOCOL, "server closed connection"); +	len += ret; + +	char *crlf; +	char *line = buf; +	for (;;) { +		crlf = memmem(line, &buf[len] - line, "\r\n", 2); +		if (!crlf) break; +		*crlf = '\0'; +		handle(parse(line)); +		line = crlf + 2; +	} + +	len -= line - buf; +	memmove(buf, line, len); +} | 
