Blindly implement login flow
parent
f76145645e
commit
8431602363
|
@ -0,0 +1,2 @@
|
|||
*.o
|
||||
catgirl
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue