diff options
-rw-r--r-- | litterbox.1 | 4 | ||||
-rw-r--r-- | litterbox.c | 170 |
2 files changed, 171 insertions, 3 deletions
diff --git a/litterbox.1 b/litterbox.1 index 0c9a06c..73d5f9e 100644 --- a/litterbox.1 +++ b/litterbox.1 @@ -8,6 +8,7 @@ . .Sh SYNOPSIS .Nm +.Op Fl v .Op Fl d Ar path .Op Fl h Ar host .Op Fl j Ar join @@ -69,6 +70,9 @@ Set the username to .Ar user . The default username is the same as the nickname. . +.It Fl v +Write sent and received IRC messages to standard error. +. .It Fl w Ar pass Log in with the server password .Ar pass . diff --git a/litterbox.c b/litterbox.c index 2fa52fb..2b2736d 100644 --- a/litterbox.c +++ b/litterbox.c @@ -14,35 +14,152 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +#include <assert.h> #include <err.h> #include <sqlite3.h> +#include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> +#include <string.h> #include <sysexits.h> +#include <tls.h> #include <unistd.h> #include "database.h" +static sqlite3 *db; +static struct tls *client; +static bool verbose; + +static void writeAll(const char *ptr, size_t len) { + 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; + } +} + +static void format(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)); + if (verbose) printf("%s", buf); + writeAll(buf, len); +} + +enum { ParamCap = 15 }; +struct Message { + char *time; + char *nick; + char *user; + char *host; + char *cmd; + char *params[ParamCap]; +}; + +static struct Message parse(char *line) { + if (verbose) fprintf(stderr, "%s\n", line); + struct Message msg = {0}; + if (line[0] == '@') { + char *tags = 1 + strsep(&line, " "); + while (tags) { + char *tag = strsep(&tags, ";"); + char *key = strsep(&tag, "="); + if (!strcmp(key, "time")) msg.time = tag; + } + } + if (line[0] == ':') { + char *origin = 1 + strsep(&line, " "); + msg.nick = strsep(&origin, "!"); + msg.user = strsep(&origin, "@"); + if (origin) { + msg.host = origin; + } else { + msg.host = msg.nick; + msg.nick = NULL; + } + } + 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; +} + +typedef void Handler(struct Message *msg); + +static void handleCap(struct Message *msg) { + (void)msg; + format("CAP END\r\n"); +} + +static void handlePing(struct Message *msg) { + // TODO: Require params[0]. + format("PONG :%s\r\n", msg->params[0]); +} + +static const struct { + const char *cmd; + Handler *fn; +} Handlers[] = { + { "CAP", handleCap }, + { "PING", handlePing }, +}; + +static void handle(struct Message msg) { + if (!msg.cmd) return; + for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) { + if (strcmp(msg.cmd, Handlers[i].cmd)) continue; + Handlers[i].fn(&msg); + break; + } +} + int main(int argc, char *argv[]) { char *path = NULL; bool init = false; bool migrate = false; + bool insecure = false; + const char *host = NULL; + const char *port = "6697"; + + const char *nick = "litterbox"; + const char *user = NULL; + const char *pass = NULL; + int opt; - while (0 < (opt = getopt(argc, argv, "d:im"))) { + while (0 < (opt = getopt(argc, argv, "!d:h:imn:p:u:vw:"))) { switch (opt) { + break; case '!': insecure = true; break; case 'd': path = optarg; + break; case 'h': host = optarg; break; case 'i': init = true; break; case 'm': migrate = true; + break; case 'n': nick = optarg; + break; case 'p': port = optarg; + break; case 'u': user = optarg; + break; case 'v': verbose = true; + break; case 'w': pass = optarg; break; default: return EX_USAGE; } } + if (!user) user = nick; int flags = SQLITE_OPEN_READWRITE; if (init) flags |= SQLITE_OPEN_CREATE; - sqlite3 *db = (path ? dbOpen(path, flags) : dbFind(flags)); + db = (path ? dbOpen(path, flags) : dbFind(flags)); if (!db) errx(EX_NOINPUT, "database not found"); if (init) { @@ -53,8 +170,55 @@ int main(int argc, char *argv[]) { dbMigrate(db); return EX_OK; } - if (dbVersion(db) != DatabaseVersion) { errx(EX_CONFIG, "database out of date; migrate with -m"); } + + if (!host) errx(EX_USAGE, "host required"); + + client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + 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); + } + + error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); + + error = tls_connect(client, host, port); + if (error) errx(EX_UNAVAILABLE, "tls_connect: %s", tls_error(client)); + + if (pass) format("PASS :%s\r\n", pass); + format("CAP REQ :server-time\r\n"); + format("NICK :%s\r\nUSER %s 0 * :Litterbox\r\n", nick, user); + + char buf[8191 + 512]; + size_t len = 0; + for (;;) { + ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); + len += ret; + + char *line = buf; + for (;;) { + char *crlf = memmem(line, &buf[len] - line, "\r\n", 2); + if (!crlf) break; + crlf[0] = '\0'; + handle(parse(line)); + line = crlf + 2; + } + len -= line - buf; + memmove(buf, line, len); + } } |