From 96839290fb34316425ef8d625fefa8e1a58dabcb Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 7 Sep 2016 23:29:49 +0200 Subject: [PATCH] routing: Added simple IRC library based on io_loop The IRC library can login and keep its connection alive by replying to PING messages. It also exposes a callback that handles PRIVMSG commands and can inject messages from outside, e.g., based on a timer or a lightning event. --- irc.c | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ irc.h | 67 ++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 irc.c create mode 100644 irc.h diff --git a/irc.c b/irc.c new file mode 100644 index 000000000..d1eebac1c --- /dev/null +++ b/irc.c @@ -0,0 +1,178 @@ +#include "irc.h" +#include "daemon/dns.h" +#include "daemon/log.h" + +void (*irc_privmsg_cb)(struct ircstate *, const struct privmsg *) = NULL; +void (*irc_disconnect_cb)(struct ircstate *) = NULL; + +static struct io_plan *irc_connected(struct io_conn *conn, struct lightningd_state *dstate, struct ircstate *state); +static void irc_disconnected(struct io_conn *conn, struct ircstate *state); + +bool irc_send_msg(struct ircstate *state, struct privmsg *m) +{ + return irc_send(state, "PRIVMSG", "%s :%s", m->channel, m->msg); +} + +/* Send a raw irccommand to the IRC server. */ +bool irc_send(struct ircstate *state, const char *command, const char *fmt, ...) +{ + va_list ap; + struct irccommand *c = tal(state, struct irccommand); + + c->prefix = NULL; + + if (!state->connected) + return false; + + va_start(ap, fmt); + c->command = tal_strdup(c, command); + c->params = tal_vfmt(c, fmt, ap); + va_end(ap); + + list_add_tail(&state->writequeue, &c->list); + io_wake(state); + return true; +} + +/* Write buffered irccommands to the IRC connection. Commands can be + buffered using irc_send. */ +static struct io_plan *irc_write_loop(struct io_conn *conn, struct ircstate *state) +{ + state->writebuffer = tal_free(state->writebuffer); + + struct irccommand *m = list_pop(&state->writequeue, struct irccommand, list); + if (m == NULL) + return io_out_wait(conn, state, irc_write_loop, state); + + bool hasprefix = m->prefix == NULL; + state->writebuffer = tal_fmt( + state, "%s%s%s %s\r\n", + hasprefix ? "" : m->prefix, + hasprefix ? "" : " ", + m->command, + m->params); + + tal_free(m); + + log_debug(state->log, "Sending: \"%s\"", state->writebuffer); + + return io_write( + conn, + state->writebuffer, strlen(state->writebuffer), + irc_write_loop, state + ); +} + +/* + * Called by the read loop to handle individual lines. This splits the + * line into a struct irccommand and passes it on to the specific + * handlers for the irccommand type. It silently drops any irccommand + * that has an unhandled type. + */ +static void handle_irc_command(struct ircstate *state, const char *line) +{ + log_debug(state->log, "Received: \"%s\"", line); + + struct irccommand *m = talz(state, struct irccommand); + char** splits = tal_strsplit(m, line, " ", STR_NO_EMPTY); + int numsplits = tal_count(splits) - 1; + + if (numsplits > 2 && strstarts(splits[0], ":")) { + m->prefix = splits[0]; + splits++; + } + m->command = splits[0]; + m->params = tal_strjoin(m, splits + 1, " ", STR_NO_TRAIL); + + if (streq(m->command, "PING")) { + irc_send(state, "PONG", "%s", m->params); + + } else if (streq(m->command, "PRIVMSG")) { + struct privmsg *pm = talz(m, struct privmsg); + pm->sender = m->prefix; + pm->channel = splits[1]; + pm->msg = tal_strjoin(m, splits + 2, " ", STR_NO_TRAIL); + irc_privmsg_cb(state, pm); + } + tal_free(m); +} + +/* + * Read incoming data and split it along the newline boundaries. Takes + * care of buffering incomplete lines and passes the lines to the + * handle_irc_command handler. + */ +static struct io_plan *irc_read_loop(struct io_conn *conn, struct ircstate *state) +{ + + size_t len = state->readlen + state->buffered; + char *start = state->buffer, *end; + + while ((end = memchr(start, '\n', len)) != NULL) { + /* Strip "\r\n" from lines. */ + const char *line = tal_strndup(state, start, end - 1 - start); + handle_irc_command(state, line); + tal_free(line); + len -= (end + 1 - start); + start = end + 1; + } + + /* Move any partial data back down. */ + memmove(state->buffer, start, len); + state->buffered = len; + + return io_read_partial(conn, state->buffer + state->buffered, + sizeof(state->buffer) - state->buffered, + &state->readlen, irc_read_loop, state); +} + +static void irc_failed(struct lightningd_state *dstate, struct ircstate *state) +{ + irc_disconnected(state->conn, state); + state->connected = false; +} + +static void irc_disconnected(struct io_conn *conn, struct ircstate *state) +{ + log_debug(state->log, "Lost connection to IRC server"); + state->connected = false; + state->conn = NULL; + state->readlen = 0; + state->buffered = 0; + memset(state->buffer, 0, sizeof(state->buffer)); + + /* Clear any pending commands, they're no longer useful */ + while (!list_empty(&state->writequeue)) + tal_free(list_pop(&state->writequeue, struct irccommand, list)); + + /* Same goes for partially written commands */ + state->writebuffer = tal_free(state->writebuffer); + + if (irc_disconnect_cb != NULL) + irc_disconnect_cb(state); +} + +void irc_connect(struct ircstate *state) +{ + state->connected = false; + list_head_init(&state->writequeue); + + log_debug(state->log, "Connecting to IRC server %s", state->server); + dns_resolve_and_connect(state->dstate, state->server, "6667", irc_connected, irc_failed, state); +} + +static struct io_plan *irc_connected(struct io_conn *conn, struct lightningd_state *dstate, struct ircstate *state) +{ + io_set_finish(conn, irc_disconnected, state); + state->conn = conn; + state->connected = true; + irc_send(state, "USER", "%s 0 * :A lightning node", state->nick); + irc_send(state, "NICK", "%s", state->nick); + irc_send(state, "JOIN", "#lightning-nodes"); + + return io_duplex(conn, + io_read_partial(conn, + state->buffer, sizeof(state->buffer), + &state->readlen, irc_read_loop, state), + irc_write_loop(conn, state)); +} diff --git a/irc.h b/irc.h new file mode 100644 index 000000000..6d9cf5f46 --- /dev/null +++ b/irc.h @@ -0,0 +1,67 @@ +#ifndef LIGHTNING_IRC_H +#define LIGHTNING_IRC_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "daemon/lightningd.h" + +struct irccommand { + struct list_node list; + const char *prefix; + const char *command; + const char *params; +}; + +struct privmsg { + const char *channel; + const char *sender; + const char *msg; +}; + +struct ircstate { + /* Meta information */ + const char *nick; + const char *server; + + /* Connection and reading */ + struct io_conn *conn; + char buffer[512]; + size_t readlen; + size_t buffered; + + /* Write queue related */ + struct list_head writequeue; + char *writebuffer; + + /* Pointer to external state, making it available to callbacks */ + struct lightningd_state *dstate; + + struct log *log; + + /* Are we currently connected? */ + bool connected; + + /* Time to wait after getting disconnected before reconnecting. */ + struct timerel reconnect_timeout; +}; + +/* Callback to register for incoming messages */ +extern void (*irc_privmsg_cb)(struct ircstate *, const struct privmsg *); +extern void (*irc_disconnect_cb)(struct ircstate *); + +/* Send messages to IRC */ +bool irc_send(struct ircstate *state, const char *command, const char *fmt, ...) PRINTF_FMT(3,4); +bool irc_send_msg(struct ircstate *state, struct privmsg *m); + +/* Register IRC connection with io */ +void irc_connect(struct ircstate *state); + +#endif /* LIGHTNING_IRC_H */