Mercurial > irccd
changeset 937:ffe985308567
irccd: add server functions
author | David Demelier <markand@malikania.fr> |
---|---|
date | Mon, 11 Jan 2021 10:28:49 +0100 |
parents | 6866d0d0e360 |
children | 7b74df7e8913 |
files | Makefile irccd/event.h irccd/limits.h irccd/main.c irccd/server.c irccd/server.h |
diffstat | 6 files changed, 1319 insertions(+), 3 deletions(-) [+] |
line wrap: on
line diff
--- a/Makefile Sun Jan 10 18:10:52 2021 +0100 +++ b/Makefile Mon Jan 11 10:28:49 2021 +0100 @@ -26,6 +26,7 @@ IRCCD= irccd/irccd IRCCD_SRCS= extern/libduktape/duktape.c \ irccd/log.c \ + irccd/server.c \ irccd/subst.c \ irccd/util.c IRCCD_OBJS= ${IRCCD_SRCS:.c=.o} @@ -62,7 +63,7 @@ clean: ${MAKE} -C extern/libcompat clean - rm -f irccd/main.o ${IRCCD} ${IRCCD_OBJS} ${IRCCD_DEPS} + rm -f irccd/main.o irccd/main.d ${IRCCD} ${IRCCD_OBJS} ${IRCCD_DEPS} ${TESTS_OBJS}: ${IRCCD_OBJS}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccd/event.h Mon Jan 11 10:28:49 2021 +0100 @@ -0,0 +1,131 @@ +/* + * event.h -- IRC event + * + * 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_EVENT_H +#define IRCCD_EVENT_H + +#include <stddef.h> + +#include "limits.h" + +struct irc_server; + +enum irc_event_type { + IRC_EVENT_UNKNOWN, + IRC_EVENT_CONNECT, + IRC_EVENT_DISCONNECT, + IRC_EVENT_INVITE, + IRC_EVENT_JOIN, + IRC_EVENT_KICK, + IRC_EVENT_ME, + IRC_EVENT_MESSAGE, + IRC_EVENT_MODE, + IRC_EVENT_NAMES, + IRC_EVENT_NICK, + IRC_EVENT_NOTICE, + IRC_EVENT_PART, + IRC_EVENT_TOPIC, + IRC_EVENT_WHOIS +}; + +struct irc_event { + enum irc_event_type type; + struct irc_server *server; + + /* + * Raw arguments. + * [0]: prefix + */ + char args[IRC_ARGS_MAX][IRC_MESSAGE_MAX]; + size_t argsz; + + /* Conveniently organized union depending on the type. */ + union { + struct { + char *origin; + char *channel; + char *nickname; + } invite; + + struct { + char *origin; + char *channel; + } join; + + struct { + char *origin; + char *channel; + char *target; + char *reason; + } kick; + + struct { + char *origin; + char *channel; + char *message; + } me; + + struct { + char *origin; + char *channel; + char *message; + } message; + + struct { + char *origin; + char *channel; + char *mode; + char *limit; + char *user; + char *mask; + } mode; + + struct { + char *origin; + char *nickname; + } nick; + + struct { + char *origin; + char *channel; + char *message; + } notice; + + struct { + char *origin; + char *channel; + char *reason; + } part; + + struct { + char *origin; + char *channel; + char *topic; + } topic; + + struct { + char *nick; + char *user; + char *hostname; + char *realname; + char **channels; + } whois; + }; +}; + +#endif /* !IRCCD_EVENT_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccd/limits.h Mon Jan 11 10:28:49 2021 +0100 @@ -0,0 +1,41 @@ +/* + * limits.h -- irccd limits + * + * 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_LIMITS_H +#define IRCCD_LIMITS_H + +/* IRC limits. */ +#define IRC_NICKNAME_MAX 32 +#define IRC_USERNAME_MAX 32 +#define IRC_REALNAME_MAX 64 +#define IRC_CHANNEL_MAX 64 +#define IRC_PASSWORD_MAX 64 +#define IRC_CTCPVERSION_MAX 128 +#define IRC_USERMODES_MAX 16 + +#define IRC_MESSAGE_MAX 512 +#define IRC_ARGS_MAX 16 + +/* Network limits. */ +#define IRC_HOST_MAX 32 +#define IRC_BUF_MAX 8192 + +/* Types limits. */ +#define IRC_NAME_MAX 16 + +#endif /* !IRCCD_LIMITS_H */
--- a/irccd/main.c Sun Jan 10 18:10:52 2021 +0100 +++ b/irccd/main.c Mon Jan 11 10:28:49 2021 +0100 @@ -16,9 +16,50 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <poll.h> +#include <stdio.h> +#include <err.h> + +#include "event.h" +#include "server.h" + int main(int argc, char **argv) { - (void)argc; - (void)argv; + struct irc_server s = { + .name = "malikania", + .host = "malikania.fr", + .port = 6667, + .nickname = "circ", + .username = "circ", + .realname = "circ" + }; + struct irc_event ev; + + struct pollfd fd; + + irc_server_connect(&s); + irc_server_join(&s, "#test", NULL); + + for (;;) { + irc_server_prepare(&s, &fd); + + if (poll(&fd, 1, -1) < 0) + err(1, "poll"); + + irc_server_flush(&s, &fd); + + while (irc_server_poll(&s, &ev)) { + switch (ev.type) { + case IRC_EVENT_MESSAGE: + printf("message, origin=%s,channel=%s,message=%s\n", + ev.message.origin,ev.message.channel, ev.message.message); + break; + case IRC_EVENT_ME: + printf("me, origin=%s,channel=%s,message=%s\n", + ev.me.origin,ev.me.channel, ev.me.message); + break; + } + } + } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccd/server.c Mon Jan 11 10:28:49 2021 +0100 @@ -0,0 +1,955 @@ +/* + * server.c -- an IRC server + * + * 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/types.h> +#include <sys/socket.h> +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <netdb.h> +#include <poll.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(IRCCD_HAVE_SSL) +# include <openssl/err.h> +#endif + +#include "event.h" +#include "log.h" +#include "server.h" +#include "util.h" + +struct origin { + char nickname[IRC_NICKNAME_MAX]; + char username[IRC_USERNAME_MAX]; + char host[IRC_HOST_MAX]; +}; + +static bool +is_ctcp(const char *line) +{ + assert(line); + + const size_t length = strlen(line); + + if (length < 2) + return false; + + return line[0] == 0x1 && line[length - 1] == 0x1; +} + +static char * +ctcp(char *line) +{ + assert(line); + + /* Remove last \001. */ + line[strcspn(line, "\001")] = 0; + + if (strncmp(line, "ACTION ", 7) == 0) + line += 7; + + return line; +} + +static int +compare_chan(const void *d1, const void *d2) +{ + return strcmp( + ((const struct irc_server_channel *)d1)->name, + ((const struct irc_server_channel *)d2)->name + ); +} + +static const struct origin * +parse_origin(const char *prefix) +{ + static struct origin origin; + char fmt[128]; + + memset(&origin, 0, sizeof (origin)); + snprintf(fmt, sizeof (fmt), "%%%zu[^!]!%%%zu[^@]@%%%zus", + sizeof (origin.nickname) - 1, + sizeof (origin.username) - 1, + sizeof (origin.host) - 1); + sscanf(prefix, fmt, origin.nickname, origin.username, origin.host); + + return &origin; +} + +static inline void +sort(struct irc_server *s) +{ + qsort(s->channels, s->channelsz, sizeof (*s->channels), compare_chan); +} + +static struct irc_server_channel * +add_channel(struct irc_server *s, const char *name, const char *password, bool joined) +{ + struct irc_server_channel ch = { + .joined = joined + }; + + strlcpy(ch.name, name, sizeof (ch.name)); + + if (password) + strlcpy(ch.password, password, sizeof (ch.password)); + + s->channels = irc_util_reallocarray(s->channels, ++s->channelsz, sizeof (ch)); + + memcpy(&s->channels[s->channelsz - 1], &ch, sizeof (ch)); + sort(s); + + return irc_server_find(s, name); +} + +static void +remove_channel(struct irc_server *s, struct irc_server_channel *ch) +{ + /* Null channel name will be moved at the end. */ + memset(ch, 0, sizeof (*ch)); + + s->channels = irc_util_reallocarray(s->channels, --s->channelsz, sizeof (*ch)); + sort(s); +} + +static void +read_support_prefix(struct irc_server *s, const char *value) +{ + char modes[16 + 1] = { 0 }; + char tokens[16 + 1] = { 0 }; + + if (sscanf(value, "(%16[^)])%16s", modes, tokens) == 2) { + char *pm = modes; + char *tk = tokens; + + for (size_t i = 0; i < 16 && *pm && *tk; ++i) { + s->prefixes[i].mode = *pm++; + s->prefixes[i].token = *tk++; + } + } +} + +static void +read_support_chantypes(struct irc_server *s, const char *value) +{ + strlcpy(s->chantypes, value, sizeof (s->chantypes)); +} + +static void +convert_connect(struct irc_server *s, struct irc_event *ev) +{ + s->state = IRC_SERVER_STATE_CONNECTED; + + ev->type = IRC_EVENT_CONNECT; + ev->server = s; + + /* Now join all channels that were requested. */ + for (size_t i = 0; i < s->channelsz; ++i) + irc_server_join(s, s->channels[i].name, s->channels[i].password); +} + +static void +convert_support(struct irc_server *s, struct irc_event *ev) +{ + char key[64]; + char value[64]; + + for (size_t i = 4; i < ev->argsz; ++i) { + if (sscanf(ev->args[i], "%63[^=]=%63s", key, value) != 2) + continue; + + if (strcmp(key, "PREFIX") == 0) + read_support_prefix(s, value); + if (strcmp(key, "CHANTYPES") == 0) + read_support_chantypes(s, value); + } +} + +static void +convert_join(struct irc_server *s, struct irc_event *ev) +{ + const struct origin *origin = parse_origin(ev->args[0]); + struct irc_server_channel *ch; + + ev->type = IRC_EVENT_JOIN; + ev->server = s; + ev->join.origin = ev->args[0]; + ev->join.channel = ev->args[2]; + + /* Also add a channel if the bot joined. */ + if (strcmp(s->nickname, origin->nickname)) { + if ((ch = irc_server_find(s, ev->args[2]))) + ch->joined = true; + else + add_channel(s, ev->args[2], NULL, true); + } +} + +static void +convert_kick(struct irc_server *s, struct irc_event *ev) +{ + ev->type = IRC_EVENT_KICK; + ev->server = s; + ev->kick.origin = ev->args[0]; + ev->kick.channel = ev->args[2]; + ev->kick.target = ev->args[3]; + ev->kick.reason = ev->args[4]; + + /* + * If the bot was kicked itself mark the channel as not joined and + * rejoin it automatically if the option is set. + */ + if (strcmp(ev->args[3], s->nickname) == 0) { + struct irc_server_channel *ch = irc_server_find(s, ev->args[2]); + + if (ch) { + ch->joined = false; + + if (s->flags & IRC_SERVER_FLAGS_AUTO_REJOIN) + irc_server_join(s, ch->name, ch->password); + } + } +} + +static void +convert_mode(struct irc_server *s, struct irc_event *ev) +{ + (void)s; + (void)ev; + + for (size_t i = 0; i < ev->argsz; ++i) { + printf("MODE: %zu=%s\n", i, ev->args[i]); + } + +#if 0 + if (strcmp(m->args[0], s->nickname) == 0) { + /* Own user modes. */ + strlcpy(s->usermodes, m->args[1], sizeof (s->usermodes); + } else { + /* TODO: channel modes. */ + } +#endif +} + +static void +convert_part(struct irc_server *s, struct irc_event *ev) +{ + const struct origin *origin = parse_origin(ev->args[0]); + struct irc_server_channel *ch = irc_server_find(s, ev->args[2]); + + ev->type = IRC_EVENT_PART; + ev->server = s; + ev->part.origin = ev->args[0]; + ev->part.channel = ev->args[2]; + ev->part.reason = ev->args[3]; + + if (ch && strcmp(origin->nickname, s->nickname) == 0) + remove_channel(s, ch); +} + +static void +convert_msg(struct irc_server *s, struct irc_event *ev) +{ + ev->type = IRC_EVENT_MESSAGE; + ev->server = s; + ev->message.origin = ev->args[0]; + ev->message.channel = ev->args[2]; + ev->message.message = ev->args[3]; + + /* + * Detect CTCP commands which are PRIVMSG with a special boundaries. + * + * Example: + * PRIVMSG jean :\001ACTION I'm eating\001. + */ + if (is_ctcp(ev->args[3])) { + ev->type = IRC_EVENT_ME; + ev->message.message = ctcp(ev->args[3] + 1); + } +} + +static void +convert_nick(struct irc_server *s, struct irc_event *ev) +{ + const struct origin *origin = parse_origin(ev->args[0]); + + /* Update nickname if it is myself. */ + if (strcmp(origin->nickname, s->nickname) == 0) + strlcpy(s->nickname, ev->args[2], sizeof (s->nickname)); +} + +static void +convert_notice(struct irc_server *s, struct irc_event *ev) +{ + ev->type = IRC_EVENT_NOTICE; + ev->server = s; + ev->notice.origin = ev->args[0]; + ev->notice.channel = ev->args[2]; + ev->notice.message = ev->args[3]; +} + +static void +convert_topic(struct irc_server *s, struct irc_event *ev) +{ + ev->type = IRC_EVENT_TOPIC; + ev->server = s; + ev->topic.origin = ev->args[0]; + ev->topic.channel = ev->args[2]; + ev->topic.topic = ev->args[3]; +} + +static void +convert_ping(struct irc_server *s, struct irc_event *ev) +{ + irc_server_send(s, "PONG %s", ev->args[0]); +} + +static void +convert_names(struct irc_server *s, struct irc_event *ev) +{ + (void)s; + (void)ev; +#if 0 + struct irc_server_channel *chan; + char *p, *n; + + if (m->argsz < 3 || !(chan = irc_server_find(s, m->args[2]))) + return; + + /* + * Message arguments are as following: + * 0------- 1 2------- 3-------------------- + * yourself = #channel nick1 nick2 nick3 ... + */ + for (p = m->args[3]; p; p = n ? n + 1 : NULL) { + if ((n = strpbrk(p, " "))) + *n = 0; + + channel_add(chan, s, p); + } +#endif +} + +static const struct convert { + const char *command; + void (*convert)(struct irc_server *, struct irc_event *); +} converters[] = { + /* Must be kept ordered. */ + { "001", convert_connect }, + { "005", convert_support }, + { "353", convert_names }, + { "JOIN", convert_join }, + { "KICK", convert_kick }, + { "MODE", convert_mode }, + { "NICK", convert_nick }, + { "NOTICE", convert_notice }, + { "PART", convert_part }, + { "PING", convert_ping }, + { "PRIVMSG", convert_msg }, + { "TOPIC", convert_topic } +}; + +static int +compare_converter(const void *d1, const void *d2) +{ + return strcmp(d1, ((const struct convert *)d2)->command); +} + +static void +convert(struct irc_server *s, struct irc_event *ev) +{ + const struct convert *c = bsearch(ev->args[1], converters, IRC_UTIL_SIZE(converters), + sizeof (*c), &(compare_converter)); + + if (c) + c->convert(s, ev); +} + +static inline bool +scan(struct irc_event *ev, const char **line) +{ + const char *p = *line; + size_t i = 0; + + /* Copy argument. */ + while (i < IRC_MESSAGE_MAX && *p && !isspace(*p)) + ev->args[ev->argsz][i++] = *p++; + + /* Skip optional spaces. */ + while (*p && isspace(*p)) + ++p; + + if (i >= IRC_MESSAGE_MAX) + return false; + + *line = p; + ev->argsz++; + + return true; +} + +static void +parse(struct irc_server *s, struct irc_event *ev, const char *line) +{ + if (!*line || *line++ != ':') + return; + if (!scan(ev, &line)) /* Prefix */ + return; + if (!scan(ev, &line)) /* Command */ + return; + + /* Arguments. */ + while (*line && ev->argsz < IRC_ARGS_MAX) { + /* Last argument: read until end. */ + if (*line == ':') { + strlcpy(ev->args[ev->argsz++], ++line, IRC_MESSAGE_MAX); + break; + } + + if (!scan(ev, &line)) + return; + } + + convert(s, ev); +} + +static void +clear(struct irc_server *s) +{ + s->state = IRC_SERVER_STATE_DISCONNECTED; + + if (s->fd != 0) { + close(s->fd); + s->fd = 0; + } + + if (s->ai) { + freeaddrinfo(s->ai); + s->ai = NULL; + s->aip = NULL; + } + +#if defined(IRCCD_HAVE_SSL) + if (s->ssl) { + SSL_free(s->ssl); + s->ssl = NULL; + } + if (s->ctx) { + SSL_CTX_free(s->ctx); + s->ctx = NULL; + } +#endif +} + +static bool +lookup(struct irc_server *s) +{ + struct addrinfo hints = { + .ai_socktype = SOCK_STREAM, + }; + char service[16]; + int ret; + + snprintf(service, sizeof (service), "%hu", s->port); + + if ((ret = getaddrinfo(s->host, service, &hints, &s->ai)) != 0) + irc_log_warn("server %s: %s", s->name, gai_strerror(ret)); + + s->aip = s->ai; + + return true; +} + +static void +auth(struct irc_server *s) +{ + s->state = IRC_SERVER_STATE_CONNECTED; + + irc_server_send(s, "NICK %s", s->nickname); + irc_server_send(s, "USER %s %s %s :%s", s->username, + s->username, s->username, s->realname); + /* TODO: server password as well. */ +} + +#if defined(IRCCD_HAVE_SSL) + +static void +secure_update(struct irc_server *s, int ret) +{ + (void)s; + (void)ret; + + assert(s); + + int r; + + if (!(s->flags & SERVER_FL_SSL)) + return; + + switch ((r = SSL_get_error(s->ssl, ret))) { + case SSL_ERROR_WANT_READ: + s->ssl_state = SERVER_SSL_NEED_READ; + break; + case SSL_ERROR_WANT_WRITE: + s->ssl_state = SERVER_SSL_NEED_WRITE; + break; + case SSL_ERROR_SSL: + clear(s); + break; + default: + s->ssl_state = SERVER_SSL_NONE; + break; + } +} + +#endif + +static void +handshake(struct irc_server *s) +{ + assert(s); + + if (!(s->flags & IRC_SERVER_FLAGS_SSL)) + auth(s); + else { +#if defined(IRCCD_HAVE_SSL) + int r; + + s->state = SERVER_ST_HANDSHAKING; + + if ((r = SSL_do_handshake(s->ssl)) > 0) + auth(s); + + secure_update(s, r); +#endif + } +} + +static void +secure_connect(struct irc_server *s) +{ + assert(s); + + if (!(s->flags & IRC_SERVER_FLAGS_SSL)) + handshake(s); + else { +#if defined(IRCCD_HAVE_SSL) + int r; + + if (!s->ctx) + s->ctx = SSL_CTX_new(TLS_method()); + if (!s->ssl) { + s->ssl = SSL_new(s->ctx); + SSL_set_fd(s->ssl, s->fd); + } + + if ((r = SSL_connect(s->ssl)) > 0) + ssl_handshake(s); + + secure_update(s, r); +#endif + } +} + +static void +dial(struct irc_server *s) +{ + /* No more address available. */ + if (s->aip == NULL) { + clear(s); + return; + } + + for (; s->aip; s->aip = s->aip->ai_next) { + /* We may need to close a socket that was open earlier. */ + if (s->fd != 0) + close(s->fd); + + s->fd = socket(s->aip->ai_family, s->aip->ai_socktype, + s->aip->ai_protocol); + + if (s->fd < 0) { + irc_log_warn("server %s: %s", s->name, strerror(errno)); + continue; + } + + /* TODO: is F_GETFL required before? */ + fcntl(s->fd, F_SETFL, O_NONBLOCK); + + /* + * With some luck, the connection completes immediately, + * otherwise we will need to wait until the socket is writable. + */ + if (connect(s->fd, s->aip->ai_addr, s->aip->ai_addrlen) == 0) { + secure_connect(s); + break; + } + + /* Connect failed, check why. */ + switch (errno) { + case EINPROGRESS: + case EAGAIN: + /* Let the writable state to determine. */ + return; + default: + irc_log_warn("server %s: %s", s->name, strerror(errno)); + break; + } + } +} + +static void +input(struct irc_server *s) +{ + char buf[IRC_MESSAGE_MAX] = {0}; + ssize_t nr = 0; + + if (s->flags & IRC_SERVER_FLAGS_SSL) { +#if defined(IRCCD_HAVE_SSL) + nr = SSL_read(s->ssl, buf, sizeof (buf) - 1); + secure_update(s, nr); +#endif + } else { + if ((nr = recv(s->fd, buf, sizeof (buf) - 1, 0)) < 0) + clear(s); + } + + 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); + } + } +} + +static void +output(struct irc_server *s) +{ + ssize_t ns = 0; + + if (s->flags & IRC_SERVER_FLAGS_SSL) { +#if defined(IRCCD_HAVE_SSL) + ns = SSL_write(s->ssl, s->out.data, s->out.size); + secure_update(s, ns); +#endif + } else if ((ns = send(s->fd, s->out, strlen(s->out), 0)) <= 0) + clear(s); + + if (ns > 0) { + /* Optimize if everything was sent. */ + if ((size_t)ns >= sizeof (s->out)) + s->out[0] = '\0'; + else + memmove(s->out, s->out + ns, sizeof (s->out) - ns); + } +} + +static void +prepare_connecting(const struct irc_server *s, struct pollfd *pfd) +{ + (void)s; + +#if defined(IRCCD_HAVE_SSL) + if (s->flags & IRC_SERVER_FLAGS_SSL && s->ssl && s->ctx) { + switch (s->ssl_state) { + case IRC_SERVER_SSL_NEED_READ: + pfd->events |= POLLIN; + break; + case IRC_SERVER_SSL_NEED_WRITE: + pfd->events |= POLLOUT; + break; + default: + break; + } + } else +#endif + pfd->events |= POLLOUT; +} + +static void +prepare_ready(const struct irc_server *s, struct pollfd *pfd) +{ +#if defined(IRCCD_HAVE_SSL) + if (s->flags & IRC_SERVER_FLAGS_SSL && s->ssl_state) { + switch (s->ssl_state) { + case SERVER_SSL_NEED_READ: + pfd->events |= POLLIN; + break; + case SERVER_SSL_NEED_WRITE: + pfd->events |= POLLOUT; + break; + default: + break; + } + } else { +#endif + pfd->events |= POLLIN; + + if (s->out[0]) + pfd->events |= POLLOUT; +#if defined(IRCCD_HAVE_SSL) + } +#endif +} + +static void +flush_connecting(struct irc_server *s, const struct pollfd *pfd) +{ + (void)pfd; + + int res, err = -1; + socklen_t len = sizeof (int); + + if ((res = getsockopt(s->fd, SOL_SOCKET, SO_ERROR, &err, &len)) < 0 || err) { + irc_log_warn("server %s: %s", s->name, strerror(res ? err : errno)); + dial(s); + } else + secure_connect(s); +} + +static void +flush_handshaking(struct irc_server *s, const struct pollfd *pfd) +{ + (void)pfd; + + handshake(s); +} + +static void +flush_ready(struct irc_server *s, const struct pollfd *pfd) +{ + if (pfd->revents & POLLERR || pfd->revents & POLLHUP) + clear(s); + if (pfd->revents & POLLIN) + input(s); + if (pfd->revents & POLLOUT) + output(s); +} + +static const struct { + void (*prepare)(const struct irc_server *, struct pollfd *); + void (*flush)(struct irc_server *, const struct pollfd *); +} io_table[] = { + [IRC_SERVER_STATE_CONNECTING] = { + prepare_connecting, + flush_connecting + }, + [IRC_SERVER_STATE_HANDSHAKING] = { + prepare_ready, + flush_handshaking + }, + [IRC_SERVER_STATE_CONNECTED] = { + prepare_ready, + flush_ready + }, +}; + +void +irc_server_connect(struct irc_server *s) +{ + assert(s); + + s->state = IRC_SERVER_STATE_CONNECTING; + + if (!lookup(s)) + clear(s); + else + dial(s); +} + +void +irc_server_disconnect(struct irc_server *s) +{ + assert(s); + + clear(s); +} + +void +irc_server_prepare(const struct irc_server *s, struct pollfd *pfd) +{ + pfd->fd = s->fd; + pfd->events = 0; + + if (io_table[s->state].prepare) + io_table[s->state].prepare(s, pfd); +} + +void +irc_server_flush(struct irc_server *s, const struct pollfd *pfd) +{ + if (io_table[s->state].flush) + io_table[s->state].flush(s, pfd); +} + +bool +irc_server_poll(struct irc_server *s, struct irc_event *ev) +{ + assert(s); + assert(ev); + + char *pos; + size_t length; + + if (!(pos = strstr(s->in, "\r\n"))) + return false; + + /* Turn end of the string at delimiter. */ + *pos = 0; + length = pos - s->in; + + /* Clear event in case we don't understand this message. */ + memset(ev, 0, sizeof (*ev)); + ev->type = IRC_EVENT_UNKNOWN; + + if (length > 0) + parse(s, ev, s->in); + + memmove(s->in, pos + 2, sizeof (s->in) - (length + 2)); + + return true; +} + +struct irc_server_channel * +irc_server_find(struct irc_server *s, const char *name) +{ + assert(s); + assert(name); + + struct irc_server_channel key = {0}; + + strlcpy(key.name, name, sizeof (key.name)); + + return bsearch(&key, s->channels, s->channelsz, sizeof (key), compare_chan); +} + +bool +irc_server_send(struct irc_server *s, const char *fmt, ...) +{ + assert(s); + assert(fmt); + + char buf[sizeof (s->out)]; + va_list ap; + size_t len, avail, required; + + va_start(ap, fmt); + required = vsnprintf(buf, sizeof (buf), fmt, ap); + va_end(ap); + + len = strlen(s->out); + avail = sizeof (s->out) - len; + + /* Don't forget \r\n. */ + if (required + 2 >= avail) + return false; + + strlcat(s->out, buf, sizeof (s->out)); + strlcat(s->out, "\r\n", sizeof (s->out)); + + return true; +} + +bool +irc_server_join(struct irc_server *s, const char *name, const char *pass) +{ + assert(s); + assert(name); + + struct irc_server_channel *ch; + bool ret = true; + + /* + * Search if there is already a channel pending or joined. If the + * server is connected we send a join command otherwise we put it there + * and wait for connection. + */ + if (!(ch = irc_server_find(s, name))) + ch = add_channel(s, name, pass, false); + + if (!ch->joined && s->state == IRC_SERVER_STATE_CONNECTED) { + if (pass) + ret = irc_server_send(s, "JOIN %s %s", name, pass); + else + ret = irc_server_send(s, "JOIN %s", name); + } + + return ret; +} + +bool +irc_server_part(struct irc_server *s, const char *name, const char *reason) +{ + assert(s); + assert(name); + + bool ret; + + if (reason && strlen(reason) > 0) + ret = irc_server_send(s, "PART %s :%s", name, reason); + else + ret = irc_server_send(s, "PART %s", name); + + return ret; +} + +bool +irc_server_topic(struct irc_server *s, const char *name, const char *topic) +{ + assert(s); + assert(name); + assert(topic); + + return irc_server_send(s, "TOPIC %s :%s", name, topic); +} + +bool +irc_server_message(struct irc_server *s, const char *chan, const char *msg) +{ + assert(s); + assert(chan); + assert(msg); + + return irc_server_send(s, "PRIVMSG %s :%s", chan, msg); +} + +bool +irc_server_me(struct irc_server *s, const char *chan, const char *message) +{ + assert(s); + assert(chan); + assert(message); + + return irc_server_send(s, "PRIVMSG %s :\001ACTION %s\001", chan, message); +} + +void +irc_server_finish(struct irc_server *s) +{ + assert(s); + + clear(s); + free(s->channels); + memset(s, 0, sizeof (*s)); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccd/server.h Mon Jan 11 10:28:49 2021 +0100 @@ -0,0 +1,147 @@ +/* + * server.h -- an IRC server + * + * 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_SERVER_H +#define IRCCD_SERVER_H + +#include <stdbool.h> +#include <stddef.h> + +#if defined(IRC_HAVE_SSL) +# include <openssl/ssl.h> +#endif + +#include "limits.h" + +struct pollfd; + +struct irc_event; + +struct irc_server_channel { + char name[IRC_CHANNEL_MAX]; + char password[IRC_PASSWORD_MAX]; + bool joined; +}; + +enum irc_server_state { + IRC_SERVER_STATE_DISCONNECTED, + IRC_SERVER_STATE_CONNECTING, + IRC_SERVER_STATE_HANDSHAKING, + IRC_SERVER_STATE_CONNECTED, + IRC_SERVER_STATE_WAITING, + IRC_SERVER_STATE_NUM +}; + +enum irc_server_flags { + IRC_SERVER_FLAGS_SSL = (1 << 0), + IRC_SERVER_FLAGS_AUTO_REJOIN = (1 << 1) +}; + +struct irc_server_prefix { + char mode; + char token; +}; + +#if defined(IRCCD_HAVE_SSL) + +enum irc_server_ssl_state { + IRC_SERVER_SSL_NONE, + IRC_SERVER_SSL_NEED_READ, + IRC_SERVER_SSL_NEED_WRITE, +}; + +#endif + +struct irc_server { + /* Connection settings. */ + char name[IRC_NAME_MAX]; + char host[IRC_HOST_MAX]; + unsigned short port; + enum irc_server_flags flags; + + /* IRC identity. */ + char nickname[IRC_NICKNAME_MAX]; + char username[IRC_USERNAME_MAX]; + char realname[IRC_REALNAME_MAX]; + char ctcpversion[IRC_CTCPVERSION_MAX]; + char usermodes[IRC_USERMODES_MAX]; + + /* Joined channels. */ + struct irc_server_channel *channels; + size_t channelsz; + + /* Network connectivity. */ + int fd; + struct addrinfo *ai; + struct addrinfo *aip; + char in[IRC_BUF_MAX]; + char out[IRC_BUF_MAX]; + enum irc_server_state state; + + /* OpenSSL support. */ +#if defined(IRCCD_HAVE_SSL) + SSL_CTX *ctx; + SSL *ssl; + enum irc_server_ssl_state ssl_state; +#endif + + /* IRC server settings. */ + char chantypes[8]; + struct irc_server_prefix prefixes[16]; +}; + +void +irc_server_connect(struct irc_server *); + +void +irc_server_disconnect(struct irc_server *); + +void +irc_server_prepare(const struct irc_server *, struct pollfd *); + +void +irc_server_flush(struct irc_server *, const struct pollfd *); + +bool +irc_server_poll(struct irc_server *, struct irc_event *); + +struct irc_server_channel * +irc_server_find(struct irc_server *, const char *); + +bool +irc_server_send(struct irc_server *, const char *, ...); + +bool +irc_server_join(struct irc_server *, const char *, const char *); + +bool +irc_server_part(struct irc_server *, const char *, const char *); + +bool +irc_server_topic(struct irc_server *, const char *, const char *); + +bool +irc_server_message(struct irc_server *, const char *, const char *); + +bool +irc_server_me(struct irc_server *, const char *, const char *); + +void +irc_server_finish(struct irc_server *); + +#endif /* !IRCCD_SERVER_H */