/* Copyright (C) 2019 C. McEnroe * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include "database.h" int getopt_config( int argc, char *const *argv, const char *optstring, const struct option *longopts, int *longindex ); static const char *host; static const char *port = "6697"; static struct tls *client; static void clientWrite(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) fprintf(stderr, "%s", buf); clientWrite(buf, len); } enum { ParamCap = 15 }; struct Message { size_t pos; 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 (!strcmp(key, "causal.agency/pos")) { msg.pos = strtoull(tag, NULL, 10); } } } if (line[0] == ':') { char *origin = 1 + strsep(&line, " "); msg.nick = strsep(&origin, "!"); msg.user = strsep(&origin, "@"); msg.host = origin; if (!msg.user) msg.user = msg.nick; if (!msg.host) msg.host = msg.user; } 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; } static void require(const struct Message *msg, bool nick, size_t len) { if (nick && !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 const char *join; static enum { None, Private, Public, } searchQuery; static int searchLimit = 10; static char *self; static char *network; static char *chanTypes; static char *prefixes; static char *prefixModes; static char *listModes; static char *paramModes; static char *setParamModes; 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 handleCap(struct Message *msg) { require(msg, false, 3); if (strcmp(msg->params[2], "sasl")) return; if (!strcmp(msg->params[1], "ACK")) { format("AUTHENTICATE EXTERNAL\r\n"); } else if (!strcmp(msg->params[1], "NAK")) { errx(EX_CONFIG, "server does not support SASL"); } } static void handleAuthenticate(struct Message *msg) { (void)msg; format("AUTHENTICATE +\r\n"); } static void handleReplyLoggedIn(struct Message *msg) { (void)msg; format("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, msg->params[0]); if (join) format("JOIN :%s\r\n", join); } static void handleReplyISupport(struct Message *msg) { for (size_t i = 0; i < ParamCap; ++i) { if (!msg->params[i]) break; char *key = strsep(&msg->params[i], "="); if (!msg->params[i]) continue; if (!strcmp(key, "NETWORK")) { set(&network, msg->params[i]); } else if (!strcmp(key, "CHANTYPES")) { set(&chanTypes, msg->params[i]); } else if (!strcmp(key, "PREFIX")) { strsep(&msg->params[i], "("); set(&prefixModes, strsep(&msg->params[i], ")")); set(&prefixes, msg->params[i]); } else if (!strcmp(key, "CHANMODES")) { set(&listModes, strsep(&msg->params[i], ",")); set(¶mModes, strsep(&msg->params[i], ",")); set(&setParamModes, strsep(&msg->params[i], ",")); } } } static struct { char *buf; size_t cap, len; } motd; static void handleReplyMOTDStart(struct Message *msg) { (void)msg; motd.len = 0; motd.cap = 80; motd.buf = malloc(motd.cap); if (!motd.buf) err(EX_OSERR, "malloc"); } static void handleReplyMOTD(struct Message *msg) { require(msg, false, 2); char *line = msg->params[1]; if (!strncmp(line, "- ", 2)) line += 2; size_t len = strlen(line); if (motd.len + len + 1 > motd.cap) { motd.cap *= 2; motd.buf = realloc(motd.buf, motd.cap); if (!motd.buf) err(EX_OSERR, "realloc"); } memcpy(&motd.buf[motd.len], line, len); motd.len += len; motd.buf[motd.len++] = '\n'; } static void handleReplyEndOfMOTD(struct Message *msg) { const char *sql = SQL( INSERT OR IGNORE INTO motds (time, network, motd) VALUES (coalesce(datetime(:time), datetime('now')), :network, :motd); ); sqlite3_stmt *stmt = dbPrepare(sql); dbBindText(stmt, ":time", msg->time); dbBindText(stmt, ":network", network); dbBindTextLen(stmt, ":motd", motd.buf, motd.len); dbRun(stmt); sqlite3_finalize(stmt); free(motd.buf); memset(&motd, 0, sizeof(motd)); } static int color(const char *user) { return 2 + hash(user) % 74; } static void querySearch(struct Message *msg) { static sqlite3_stmt *stmt; const char *sql = SQL( WITH results AS ( SELECT contexts.name AS context, strftime('%Y-%m-%dT%H:%M:%SZ', events.time) AS time, events.type, names.nick, names.user, events.target, highlight(search, 6, :bold, :bold), events.event FROM events JOIN contexts USING (context) JOIN names USING (name) JOIN search ON search.rowid = events.event WHERE contexts.network = :network AND coalesce(contexts.query = :query, true) AND search MATCH :search ORDER BY time DESC, event DESC LIMIT :limit ) SELECT * FROM results ORDER BY time, event; ); dbPersist(&stmt, sql); dbBindText(stmt, ":bold", "\2"); dbBindInt(stmt, ":limit", searchLimit); dbBindText(stmt, ":network", network); if (searchQuery == Public) { dbBindInt(stmt, ":query", false); } dbBindText(stmt, ":search", msg->params[1]); int result; while (SQLITE_ROW == (result = sqlite3_step(stmt))) { int i = 0; const char *context = (const char *)sqlite3_column_text(stmt, i++); const char *time = (const char *)sqlite3_column_text(stmt, i++); enum Type type = sqlite3_column_int(stmt, i++); const char *nick = (const char *)sqlite3_column_text(stmt, i++); const char *user = (const char *)sqlite3_column_text(stmt, i++); const char *target = (const char *)sqlite3_column_text(stmt, i++); const char *message = (const char *)sqlite3_column_text(stmt, i++); if (!target) target = ""; if (!message) message = ""; format("NOTICE %s :(%s) [%s] ", msg->nick, context, time); switch (type) { break; case Privmsg: format("\3%d<%s>\3 %s\r\n", color(user), nick, message); break; case Notice: format("\3%d-%s-\3 %s\r\n", color(user), nick, message); break; case Action: format("\3%d* %s\3 %s\r\n", color(user), nick, message); break; case Join: format("\3%02d%s\3 joined\r\n", color(user), nick); break; case Part: format("\3%02d%s\3 parted: %s\r\n", color(user), nick, message); break; case Quit: format("\3%02d%s\3 quit: %s\r\n", color(user), nick, message); break; case Kick: format( "\3%02d%s\3 kicked %s: %s\r\n", color(user), nick, target, message ); break; case Nick: format( "\3%02d%s\3 changed nick to \3%02d%s\3\r\n", color(user), nick, color(user), target ); break; case Topic: format( "\3%02d%s\3 set the topic: %s\r\n", color(user), nick, message ); break; case Ban: format("\3%02d%s\3 banned %s\r\n", color(user), nick, target); break; case Unban: format("\3%02d%s\3 unbanned %s\r\n", color(user), nick, target); } } if (result != SQLITE_DONE) { const char *errmsg = sqlite3_errmsg(db); if (!strncmp(errmsg, "fts5:", 5)) { format("NOTICE %s :%s\r\n", msg->nick, errmsg); } else { warnx("%s", sqlite3_errmsg(db)); } } sqlite3_reset(stmt); } static void insertContext(const char *context, bool query) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT OR IGNORE INTO contexts (network, name, query) VALUES (:network, :context, :query); ); dbPersist(&stmt, sql); dbBindText(stmt, ":network", network); dbBindText(stmt, ":context", context); dbBindInt(stmt, ":query", query); dbRun(stmt); } static void insertName(const struct Message *msg) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT OR IGNORE INTO names (nick, user, host) VALUES (:nick, :user, :host); ); dbPersist(&stmt, sql); dbBindText(stmt, ":nick", msg->nick); dbBindText(stmt, ":user", msg->user); dbBindText(stmt, ":host", msg->host); dbRun(stmt); } static void insertEvent( const struct Message *msg, enum Type type, const char *context, const char *target, const char *message ) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT INTO events (time, type, context, name, target, message) SELECT coalesce(datetime(:time), datetime('now')), :type, context, names.name, :target, :message FROM contexts, names WHERE contexts.network = :network AND contexts.name = :context AND names.nick = :nick AND names.user = :user AND names.host = :host; ); dbPersist(&stmt, sql); dbBindText(stmt, ":time", msg->time); dbBindInt(stmt, ":type", type); dbBindText(stmt, ":network", network); dbBindText(stmt, ":context", context); dbBindText(stmt, ":nick", msg->nick); dbBindText(stmt, ":user", msg->user); dbBindText(stmt, ":host", msg->host); dbBindText(stmt, ":target", target); dbBindText(stmt, ":message", message); dbRun(stmt); } static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); bool query = true; const char *context = msg->params[0]; if (strchr(chanTypes, context[0])) query = false; if (!strcmp(context, self)) context = msg->nick; enum Type type = (!strcmp(msg->cmd, "NOTICE") ? Notice : Privmsg); char *message = msg->params[1]; if (!strncmp(message, "\1ACTION ", 8)) { message += 8; message[strcspn(message, "\1")] = '\0'; type = Action; } bool selfMessage = !strcmp(msg->nick, msg->params[0]); if (query && searchQuery && type == Privmsg) { if (searchQuery == Public || selfMessage) { querySearch(msg); return; } } if (selfMessage) return; insertContext(context, query); insertName(msg); insertEvent(msg, type, context, NULL, message); } static void insertTopic( const char *time, const char *context, const char *topic ) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT OR IGNORE INTO topics (time, context, topic) SELECT coalesce(datetime(:time), datetime('now')), context, :topic FROM contexts WHERE network = :network AND name = :context; ); dbPersist(&stmt, sql); dbBindText(stmt, ":time", time); dbBindText(stmt, ":network", network); dbBindText(stmt, ":context", context); dbBindText(stmt, ":topic", topic); dbRun(stmt); } static void handleReplyTopic(struct Message *msg) { require(msg, false, 2); if (!strcmp(msg->cmd, "331")) msg->params[2] = ""; insertContext(msg->params[1], false); insertTopic(msg->time, msg->params[1], msg->params[2]); } static void createJoins(void) { const char *sql = SQL( CREATE TEMPORARY TABLE joins ( nick TEXT NOT NULL, channel TEXT NOT NULL, UNIQUE (nick, channel) ); ); dbExec(sql); } static void insertJoin(const char *nick, const char *channel) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT OR IGNORE INTO joins (nick, channel) VALUES (:nick, :channel); ); dbPersist(&stmt, sql); dbBindText(stmt, ":nick", nick); dbBindText(stmt, ":channel", channel); dbRun(stmt); } static void deleteJoin(const char *nick, const char *channel) { static sqlite3_stmt *stmt; const char *sql = SQL( DELETE FROM joins WHERE nick = :nick AND channel = :channel; ); dbPersist(&stmt, sql); dbBindText(stmt, ":nick", nick); dbBindText(stmt, ":channel", channel); dbRun(stmt); } static void clearJoins(const char *nick, const char *channel) { static sqlite3_stmt *stmt; const char *sql = SQL( DELETE FROM joins WHERE nick = :nick OR channel = :channel; ); dbPersist(&stmt, sql); dbBindText(stmt, ":nick", nick); dbBindText(stmt, ":channel", channel); dbRun(stmt); } static void handleReplyNames(struct Message *msg) { require(msg, false, 3); for (char *names = msg->params[3]; names;) { char *nick = strsep(&names, " "); nick += strspn(nick, prefixes); insertJoin(nick, msg->params[2]); } } static void handleJoin(struct Message *msg) { require(msg, true, 1); insertContext(msg->params[0], false); insertName(msg); insertEvent(msg, Join, msg->params[0], NULL, NULL); insertJoin(msg->nick, msg->params[0]); } static void handlePart(struct Message *msg) { require(msg, true, 1); insertContext(msg->params[0], false); insertName(msg); insertEvent(msg, Part, msg->params[0], NULL, msg->params[1]); if (!strcmp(msg->nick, self)) { clearJoins(NULL, msg->params[0]); } else { deleteJoin(msg->nick, msg->params[0]); } } static void handleKick(struct Message *msg) { require(msg, true, 2); insertContext(msg->params[0], false); insertName(msg); insertEvent(msg, Kick, msg->params[0], msg->params[1], msg->params[2]); if (!strcmp(msg->params[1], self)) { clearJoins(NULL, msg->params[0]); } else { deleteJoin(msg->params[1], msg->params[0]); } } static void insertEvents( const struct Message *msg, enum Type type, const char *target, const char *message ) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT INTO events (time, type, context, name, target, message) SELECT coalesce(datetime(:time), datetime('now')), :type, context, names.name, :target, :message FROM joins, contexts, names WHERE joins.nick = :nick AND contexts.name = joins.channel AND contexts.network = :network AND names.nick = :nick AND names.user = :user AND names.host = :host; ); dbPersist(&stmt, sql); dbBindText(stmt, ":time", msg->time); dbBindInt(stmt, ":type", type); dbBindText(stmt, ":network", network); dbBindText(stmt, ":nick", msg->nick); dbBindText(stmt, ":user", msg->user); dbBindText(stmt, ":host", msg->host); dbBindText(stmt, ":target", target); dbBindText(stmt, ":message", message); dbRun(stmt); } static void handleNick(struct Message *msg) { require(msg, true, 1); if (!strcmp(msg->nick, self)) set(&self, msg->params[0]); insertName(msg); insertEvents(msg, Nick, msg->params[0], NULL); static sqlite3_stmt *stmt; const char *sql = SQL( UPDATE joins SET nick = :new WHERE nick = :old; ); dbPersist(&stmt, sql); dbBindText(stmt, ":old", msg->nick); dbBindText(stmt, ":new", msg->params[0]); dbRun(stmt); } static void handleQuit(struct Message *msg) { require(msg, true, 0); insertName(msg); insertEvents(msg, Quit, NULL, msg->params[0]); clearJoins(msg->nick, NULL); } static void handleTopic(struct Message *msg) { require(msg, true, 1); insertContext(msg->params[0], false); insertTopic(msg->time, msg->params[0], msg->params[1]); insertName(msg); insertEvent(msg, Topic, msg->params[0], NULL, msg->params[1]); } static void handleMode(struct Message *msg) { require(msg, true, 2); if (!strchr(chanTypes, msg->params[0][0])) return; insertContext(msg->params[0], false); insertName(msg); bool set = true; size_t param = 2; for (char *ch = msg->params[1]; *ch; ++ch) { if (*ch == '+') { set = true; } else if (*ch == '-') { set = false; } else if (*ch == 'b') { if (param >= ParamCap || !msg->params[param]) { errx(EX_PROTOCOL, "MODE missing ban target"); } insertEvent( msg, (set ? Ban : Unban), msg->params[0], msg->params[param++], NULL ); } else if ( strchr(prefixModes, *ch) || strchr(listModes, *ch) || strchr(paramModes, *ch) || (set && strchr(setParamModes, *ch)) ) { param++; } } } static void handlePing(struct Message *msg) { require(msg, false, 1); format("PONG :%s\r\n", msg->params[0]); } static void handleError(struct Message *msg) { require(msg, false, 1); errx(EX_UNAVAILABLE, "%s", msg->params[0]); } static const struct Handler { const char *cmd; bool transaction; Handler *fn; } Handlers[] = { { "001", false, handleReplyWelcome }, { "005", false, handleReplyISupport }, { "331", true, handleReplyTopic }, { "332", true, handleReplyTopic }, { "353", true, handleReplyNames }, { "372", false, handleReplyMOTD }, { "375", false, handleReplyMOTDStart }, { "376", true, handleReplyEndOfMOTD }, { "900", false, handleReplyLoggedIn }, { "904", false, handleErrorSASLFail }, { "905", false, handleErrorSASLFail }, { "906", false, handleErrorSASLFail }, { "AUTHENTICATE", false, handleAuthenticate }, { "CAP", false, handleCap }, { "ERROR", false, handleError }, { "JOIN", true, handleJoin }, { "KICK", true, handleKick }, { "MODE", true, handleMode }, { "NICK", true, handleNick }, { "NOTICE", true, handlePrivmsg }, { "PART", true, handlePart }, { "PING", false, handlePing }, { "PRIVMSG", true, handlePrivmsg }, { "QUIT", true, handleQuit }, { "TOPIC", true, handleTopic }, }; static int compar(const void *cmd, const void *_handler) { const struct Handler *handler = _handler; return strcmp(cmd, handler->cmd); } static void updateConsumer(size_t pos) { static sqlite3_stmt *stmt; const char *sql = SQL( INSERT INTO consumers (host, port, pos) VALUES (:host, :port, :pos) ON CONFLICT (host, port) DO UPDATE SET pos = :pos WHERE host = :host AND port = :port; ); dbPersist(&stmt, sql); dbBindText(stmt, ":host", host); dbBindText(stmt, ":port", port); dbBindInt(stmt, ":pos", pos); dbRun(stmt); } static 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) return; if (handler->transaction) { dbExec(SQL(BEGIN TRANSACTION;)); handler->fn(&msg); if (msg.pos) updateConsumer(msg.pos); dbExec(SQL(COMMIT TRANSACTION;)); } else { handler->fn(&msg); } } static void atExit(void) { if (client) tls_close(client); dbClose(); } static void quit(int sig) { (void)sig; format("QUIT\r\n"); atExit(); _exit(EX_OK); } int main(int argc, char *argv[]) { char *path = NULL; bool init = false; bool migrate = false; const char *backup = NULL; bool insecure = false; const char *cert = NULL; const char *priv = NULL; const char *defaultNetwork = NULL; const char *nick = "litterbox"; const char *user = NULL; const char *pass = NULL; struct option options[] = { { .val = '!', .name = "insecure", no_argument }, { .val = 'N', .name = "network", required_argument }, { .val = 'Q', .name = "public-query", no_argument }, { .val = 'b', .name = "backup", required_argument }, { .val = 'c', .name = "cert", required_argument }, { .val = 'd', .name = "database", required_argument }, { .val = 'h', .name = "host", required_argument }, { .val = 'i', .name = "init", no_argument }, { .val = 'j', .name = "join", required_argument }, { .val = 'k', .name = "priv", required_argument }, { .val = 'l', .name = "limit", required_argument }, { .val = 'm', .name = "migrate", no_argument }, { .val = 'n', .name = "nick", required_argument }, { .val = 'p', .name = "port", required_argument }, { .val = 'q', .name = "private-query", no_argument }, { .val = 'u', .name = "user", required_argument }, { .val = 'v', .name = "verbose", no_argument }, { .val = 'w', .name = "pass", required_argument }, {0}, }; char opts[2 * ARRAY_LEN(options)]; for (size_t i = 0, j = 0; i < ARRAY_LEN(options); ++i) { opts[j++] = options[i].val; if (options[i].has_arg) opts[j++] = ':'; } for (int opt; 0 < (opt = getopt_config(argc, argv, opts, options, NULL));) { switch (opt) { break; case '!': insecure = true; break; case 'N': defaultNetwork = optarg; break; case 'Q': searchQuery = Public; break; case 'b': backup = optarg; break; case 'c': cert = optarg; break; case 'd': path = optarg; break; case 'h': host = optarg; break; case 'i': init = true; break; case 'j': join = optarg; break; case 'k': priv = optarg; break; case 'l': searchLimit = strtol(optarg, NULL, 0); break; case 'm': migrate = true; break; case 'n': nick = optarg; break; case 'p': port = optarg; break; case 'q': searchQuery = Private; 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; dbFind(path, flags); atexit(atExit); if (init) { dbInit(); return EX_OK; } if (migrate) { dbMigrate(); return EX_OK; } if (backup) { dbBackup(backup); return EX_OK; } if (dbVersion() != DatabaseVersion) { errx(EX_CONFIG, "database out of date; migrate with -m"); } createJoins(); if (!host) errx(EX_USAGE, "host required"); set(&self, "*"); set(&network, (defaultNetwork ? defaultNetwork : host)); set(&chanTypes, "#&"); set(&prefixes, "@+"); set(&prefixModes, "ov"); set(&listModes, "b"); set(¶mModes, "k"); set(&setParamModes, "l"); 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); } 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) ); } } 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)); size_t consumerPos = 0; sqlite3_stmt *stmt = dbPrepare( SQL(SELECT pos FROM consumers WHERE host = :host AND port = :port;) ); dbBindText(stmt, ":host", host); dbBindText(stmt, ":port", port); if (dbStep(stmt) == SQLITE_ROW) { consumerPos = sqlite3_column_int64(stmt, 0); } sqlite3_finalize(stmt); if (pass) format("PASS :%s\r\n", pass); if (cert) format("CAP REQ :sasl\r\n"); format("CAP REQ :server-time\r\n"); format("CAP REQ :causal.agency/passive\r\n"); if (consumerPos) { format("CAP REQ :causal.agency/consumer=%zu\r\n", consumerPos); } else { format("CAP REQ :causal.agency/consumer\r\n"); } if (!cert) format("CAP END\r\n"); format("NICK :%s\r\nUSER %s 0 * :Litterbox\r\n", nick, user); signal(SIGINT, quit); signal(SIGTERM, quit); 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)); if (!ret) errx(EX_PROTOCOL, "server closed connection"); 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); } }