--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/session.c Thu Mar 27 09:53:52 2014 +0500
@@ -0,0 +1,754 @@
+/*
+ * Copyright (c) 2014 Sunil Nimmagadda <sunil@nimmagadda.net>
+ *
+ * Permission to use, copy, modify, and 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 <ctype.h>
+#include <err.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "imsgev.h"
+#include "pop3d.h"
+#include "ssl.h"
+
+#define MAXLINESIZE 2048
+#define TIMEOUT 600000
+
+enum pop_command {
+ CMD_STLS = 0,
+ CMD_CAPA,
+ CMD_USER,
+ CMD_PASS,
+ CMD_QUIT,
+ CMD_STAT,
+ CMD_RETR,
+ CMD_LIST,
+ CMD_DELE,
+ CMD_RSET,
+ CMD_TOP,
+ CMD_UIDL,
+ CMD_NOOP
+};
+
+enum arg_constraint {
+ OPTIONAL = 1,
+ PROHIBITED,
+ REQUIRED
+};
+
+static struct {int code; enum arg_constraint c; const char *cmd;} commands[] = {
+ {CMD_STLS, PROHIBITED, "STLS"},
+ {CMD_CAPA, PROHIBITED, "CAPA"},
+ {CMD_USER, REQUIRED, "USER"},
+ {CMD_PASS, REQUIRED, "PASS"},
+ {CMD_QUIT, PROHIBITED, "QUIT"},
+ {CMD_STAT, PROHIBITED, "STAT"},
+ {CMD_RETR, REQUIRED, "RETR"},
+ {CMD_LIST, OPTIONAL, "LIST"},
+ {CMD_DELE, REQUIRED, "DELE"},
+ {CMD_RSET, PROHIBITED, "RSET"},
+ {CMD_TOP, REQUIRED, "TOP"},
+ {CMD_UIDL, OPTIONAL, "UIDL"},
+ {CMD_NOOP, PROHIBITED, "NOOP"},
+ {-1, OPTIONAL, NULL}
+};
+
+static void auth_request(struct session *);
+static void capa(struct session *);
+static void command(struct session *, int, char *);
+static void session_io(struct io *, int);
+static void parse(struct session *, char *);
+static void auth_command(struct session *, int, char *);
+static void trans_command(struct session *, int, char *);
+static void get_list_all(struct session *, int);
+static void get_list(struct session *, unsigned int, int);
+static void maildrop_imsgev(struct imsgev *, int, struct imsg *);
+static void handle_init(struct session *, struct imsg *);
+static void handle_retr(struct session *, struct imsg *);
+static void handle_dele(struct session *, struct imsg *);
+static void handle_list(struct session *, struct imsg *);
+static void handle_list_all(struct session *, struct imsg *, int);
+static void handle_update(struct session *, struct imsg *);
+static void needfd(struct imsgev *);
+static void pop3_debug(char *, ...);
+static void session_write(struct session *, const char *, size_t);
+static const char *strstate(enum state);
+
+struct session_tree sessions;
+static int _pop3_debug = 1;
+
+void
+session_init(struct listener *l, int fd)
+{
+ struct session *s;
+ void *ssl;
+ extern void *ssl_ctx;
+
+ s = xcalloc(1, sizeof(*s), "session_init");
+ s->l = l;
+ if (iobuf_init(&s->iobuf, 0, 0) == -1)
+ fatal("iobuf_init");
+
+ io_init(&s->io, fd, s, session_io, &s->iobuf);
+ io_set_timeout(&s->io, TIMEOUT);
+ s->id = arc4random();
+ s->sock = fd;
+ s->state = AUTH;
+ if (s->l->flags & POP3S) {
+ s->flags |= POP3S;
+ ssl = pop3s_init(ssl_ctx, s->sock);
+ io_set_read(&s->io);
+ io_start_tls(&s->io, ssl);
+ return;
+ }
+
+ log_connect(s->id, &l->ss, l->ss.ss_len);
+ SPLAY_INSERT(session_tree, &sessions, s);
+ session_reply(s, "%s", "+OK pop3d ready");
+ io_set_write(&s->io);
+}
+
+void
+session_close(struct session *s, int flush)
+{
+ struct session *entry;
+
+ entry = SPLAY_REMOVE(session_tree, &sessions, s);
+ if (entry == NULL) {
+ /* STARTTLS session was in progress and got interrupted */
+ logit(LOG_DEBUG, "%u: not in tree", s->id);
+ entry = s;
+ }
+
+ if (flush) {
+ if (entry->flags & POP3S)
+ iobuf_flush_ssl(&entry->iobuf, entry->io.ssl);
+ else
+ iobuf_flush(&entry->iobuf, entry->io.sock);
+ }
+
+ iobuf_clear(&entry->iobuf);
+ io_clear(&entry->io);
+ imsgev_clear(&entry->iev_maildrop);
+ imsgev_close(&entry->iev_maildrop);
+ logit(LOG_INFO, "%u: session closed", entry->id);
+ free(entry);
+}
+
+static void
+session_io(struct io *io, int evt)
+{
+ struct session *s = io->arg;
+ char *line;
+ size_t len;
+
+ pop3_debug("%u: %s", s->id, io_strevent(evt));
+ switch (evt) {
+ case IO_DATAIN:
+ line = iobuf_getline(&s->iobuf, &len);
+ if (line == NULL) {
+ iobuf_normalize(&s->iobuf);
+ break;
+ }
+ if (strncasecmp(line, "PASS", 4) == 0)
+ pop3_debug(">>> PASS");
+ else
+ pop3_debug(">>> %s", line);
+ parse(s, line);
+ break;
+ case IO_LOWAT:
+ if (iobuf_queued(&s->iobuf) == 0)
+ io_set_read(io);
+ break;
+ case IO_TLSREADY:
+ /* greet only for pop3s, STLS already greeted */
+ if (s->flags & POP3S) {
+ log_connect(s->id, &s->l->ss, s->l->ss.ss_len);
+ session_reply(s, "%s", "+OK pop3 ready");
+ io_set_write(&s->io);
+ }
+ SPLAY_INSERT(session_tree, &sessions, s);
+ /* mark STLS session as secure */
+ s->flags |= POP3S;
+ logit(LOG_INFO, "%u: TLS ready", s->id);
+ break;
+ case IO_DISCONNECTED:
+ case IO_TIMEOUT:
+ case IO_ERROR:
+ session_close(s, 0);
+ break;
+ default:
+ logit(LOG_DEBUG, "unknown event %s", io_strevent(evt));
+ break;
+ }
+}
+
+static void
+parse(struct session *s, char *line)
+{
+ enum arg_constraint c = OPTIONAL;
+ int i, cmd = -1;
+ char *args;
+
+ /* trim newline */
+ line[strcspn(line, "\n")] = '\0';
+
+ args = strchr(line, ' ');
+ if (args) {
+ *args++ = '\0';
+ while (isspace((unsigned char)*args))
+ args++;
+ }
+
+ for (i = 0; commands[i].code != -1; i++) {
+ if (strcasecmp(line, commands[i].cmd) == 0) {
+ cmd = commands[i].code;
+ c = commands[i].c;
+ break;
+ }
+ }
+
+ if (cmd == -1) {
+ logit(LOG_INFO, "%u: invalid command %s", s->id, line);
+ session_reply(s, "%s", "-ERR invalid command");
+ io_set_write(&s->io);
+ return;
+ }
+
+ if (c == PROHIBITED && args) {
+ session_reply(s, "%s", "-ERR no arguments allowed");
+ io_set_write(&s->io);
+ return;
+ } else if ((c == REQUIRED) &&
+ (args == NULL || strlen(args) >= ARGLEN)) {
+ session_reply(s, "%s", "-ERR args required or too long");
+ io_set_write(&s->io);
+ return;
+ }
+
+ command(s, cmd, args);
+}
+
+static void
+command(struct session *s, int cmd, char *args)
+{
+ switch (s->state) {
+ case AUTH:
+ auth_command(s, cmd, args);
+ break;
+ case TRANSACTION:
+ trans_command(s, cmd, args);
+ break;
+ case UPDATE:
+ session_reply(s, "%s", "-ERR commands not allowed");
+ io_set_write(&s->io);
+ break;
+ default:
+ fatalx("Invalid state");
+ }
+}
+
+static void
+auth_command(struct session *s, int cmd, char *args)
+{
+ extern void *ssl_ctx;
+ void *ssl;
+
+ switch (cmd) {
+ case CMD_STLS:
+ if (s->flags & POP3S) {
+ session_reply(s, "%s", "-ERR already secured");
+ break;
+ }
+ session_reply(s, "%s", "+OK");
+ io_set_write(&s->io);
+ iobuf_flush(&s->iobuf, s->io.sock);
+ /* add back when IO_TLSREADY. */
+ SPLAY_REMOVE(session_tree, &sessions, s);
+ ssl = pop3s_init(ssl_ctx, s->sock);
+ io_set_read(&s->io);
+ io_start_tls(&s->io, ssl);
+ return;
+ case CMD_CAPA:
+ capa(s);
+ break;
+ case CMD_USER:
+ strlcpy(s->user, args, sizeof(s->user));
+ session_reply(s, "%s", "+OK");
+ break;
+ case CMD_PASS:
+ if (s->user[0] == '\0') {
+ session_reply(s, "%s", "-ERR no USER specified");
+ break;
+ }
+ strlcpy(s->pass, args, sizeof(s->pass));
+ auth_request(s);
+ return;
+ case CMD_QUIT:
+ session_reply(s, "%s", "+OK");
+ io_set_write(&s->io);
+ session_close(s, 1);
+ return;
+ default:
+ session_reply(s, "%s", "-ERR invalid command");
+ break;
+ }
+
+ io_set_write(&s->io);
+}
+
+static void
+auth_request(struct session *s)
+{
+ extern struct imsgev iev_pop3d;
+ struct auth_req req;
+
+ memset(&req, 0, sizeof(req));
+ strlcpy(req.user, s->user, sizeof(req.user));
+ strlcpy(req.pass, s->pass, sizeof(req.pass));
+ imsgev_xcompose(&iev_pop3d, IMSG_AUTH, s->id, 0, -1,
+ &req, sizeof(req), "auth_request");
+}
+
+static void
+capa(struct session *s)
+{
+ session_reply(s, "%s", "+OK");
+ session_reply(s, "%s", "STLS");
+ session_reply(s, "%s", "USER");
+ session_reply(s, "%s", "TOP");
+ session_reply(s, "%s", "UIDL");
+ session_reply(s, "%s", "IMPLEMENTATION pop3d");
+ session_reply(s, "%s", ".");
+}
+
+static void
+trans_command(struct session *s, int cmd, char *args)
+{
+ struct retr_req retr_req;
+ unsigned int idx, n;
+ char *c;
+ const char *errstr;
+ int uidl = 0;
+
+ memset(&retr_req, 0, sizeof(retr_req));
+ switch (cmd) {
+ case CMD_CAPA:
+ capa(s);
+ break;
+ case CMD_STAT:
+ session_reply(s, "%s %zu %zu", "+OK", s->nmsgs, s->m_sz);
+ break;
+ case CMD_TOP:
+ if ((c = strchr(args, ' ')) == NULL) {
+ session_reply(s, "%s", "-ERR invalid arguments");
+ break;
+ }
+ *c++ = '\0';
+ n = strtonum(c, 0, UINT_MAX, &errstr);
+ if (errstr) {
+ session_reply(s, "%s", "-ERR invalid n");
+ break;
+ }
+ retr_req.top = 1;
+ retr_req.ntop = n;
+ /* FALLTRHROUGH */
+ case CMD_RETR:
+ if (!get_index(s, args, &retr_req.idx))
+ break;
+ imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_RETR,
+ s->id, 0, -1, &retr_req, sizeof(retr_req), "trans_command");
+ return;
+ case CMD_NOOP:
+ session_reply(s, "%s", "+OK");
+ break;
+ case CMD_DELE:
+ if (!get_index(s, args, &idx))
+ break;
+ imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_DELE,
+ s->id, 0, -1, &idx, sizeof(idx), "trans_command");
+ return;
+ case CMD_RSET:
+ imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_RSET,
+ s->id, 0, -1, NULL, 0, "trans_command");
+ return;
+ case CMD_UIDL:
+ uidl = 1;
+ /* FALLTHROUGH */
+ case CMD_LIST:
+ if (args) {
+ if (!get_index(s, args, &idx))
+ break;
+ get_list(s, idx, uidl);
+ } else
+ get_list_all(s, uidl);
+ return;
+ case CMD_QUIT:
+ imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_UPDATE,
+ s->id, 0, -1, NULL, 0, "trans_command");
+ session_set_state(s, UPDATE);
+ return;
+ default:
+ session_reply(s, "%s", "-ERR invalid command");
+ break;
+ }
+
+ io_set_write(&s->io);
+}
+
+static void
+get_list_all(struct session *s, int uidl)
+{
+ imsgev_xcompose(&s->iev_maildrop,
+ (uidl) ? IMSG_MAILDROP_UIDLALL : IMSG_MAILDROP_LISTALL,
+ s->id, 0, -1, NULL, 0, "list_all");
+}
+
+static void
+get_list(struct session *s, unsigned int i, int uidl)
+{
+ struct list_req req;
+
+ req.idx = i;
+ req.uidl = uidl;
+ imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_LIST,
+ s->id, 0, -1, &req, sizeof(req), "list");
+}
+
+void
+session_imsgev_init(struct session *s, int fd)
+{
+ imsgev_init(&s->iev_maildrop, fd, s, maildrop_imsgev, needfd);
+}
+
+static void
+maildrop_imsgev(struct imsgev *iev, int code, struct imsg *imsg)
+{
+ struct session key, *r;
+ int uidl = 0;
+
+ switch (code) {
+ case IMSGEV_IMSG:
+ key.id = imsg->hdr.peerid;
+ r = SPLAY_FIND(session_tree, &sessions, &key);
+ if (r == NULL) {
+ logit(LOG_INFO, "%u: session not found", key.id);
+ fatalx("session: session lost");
+ }
+ switch (imsg->hdr.type) {
+ case IMSG_MAILDROP_INIT:
+ handle_init(r, imsg);
+ break;
+ case IMSG_MAILDROP_RETR:
+ handle_retr(r, imsg);
+ break;
+ case IMSG_MAILDROP_DELE:
+ handle_dele(r, imsg);
+ break;
+ case IMSG_MAILDROP_RSET:
+ session_reply(r, "%s", "+OK reset");
+ io_set_write(&r->io);
+ break;
+ case IMSG_MAILDROP_LIST:
+ handle_list(r, imsg);
+ break;
+ case IMSG_MAILDROP_UIDLALL:
+ uidl = 1;
+ /* FALLTHROUGH */
+ case IMSG_MAILDROP_LISTALL:
+ handle_list_all(r, imsg, uidl);
+ break;
+ case IMSG_MAILDROP_UPDATE:
+ handle_update(r, imsg);
+ break;
+ default:
+ logit(LOG_DEBUG, "%s: unexpected imsg %u",
+ __func__, imsg->hdr.type);
+ break;
+ }
+ break;
+ case IMSGEV_EREAD:
+ case IMSGEV_EWRITE:
+ case IMSGEV_EIMSG:
+ fatal("session: imsgev read/write error");
+ break;
+ }
+}
+
+static void
+handle_init(struct session *s, struct imsg *imsg)
+{
+ size_t datalen;
+ struct stats *stats;
+
+ datalen = imsg->hdr.len - sizeof(imsg->hdr);
+ if (datalen) {
+ stats = imsg->data;
+ s->m_sz = stats->sz;
+ s->nmsgs = stats->nmsgs;
+ session_reply(s, "%s", "+OK maildrop ready");
+ io_set_write(&s->io);
+ session_set_state(s, TRANSACTION);
+ } else {
+ session_reply(s, "%s", "-ERR maildrop init failed");
+ io_set_write(&s->io);
+ session_close(s, 1);
+ }
+}
+
+static void
+handle_retr(struct session *s, struct imsg *imsg)
+{
+ struct retr_res *r = imsg->data;
+ FILE *fp;
+ char *line;
+ size_t len;
+
+ if (imsg->fd == -1) {
+ session_reply(s, "%s", "-ERR marked for delete");
+ io_set_write(&s->io);
+ return;
+ }
+
+ if ((fp = fdopen(imsg->fd, "r")) == NULL) {
+ logit(LOG_INFO, "%zu: retr failed", s->id);
+ session_reply(s, "%s", "-ERR RETR failed");
+ io_set_write(&s->io);
+ session_close(s, 1);
+ return;
+ }
+
+ if (fseek(fp, r->offset, SEEK_SET) == -1)
+ fatal("fseek");
+
+ session_reply(s, "%s", "+OK");
+ /* Ignore "From " line when type is mbox; maildir doesn't have it */
+ if ((line = fgetln(fp, &len)) && strncmp(line, "From ", 5))
+ session_write(s, line, len);
+
+ if (r->top) {
+ /* print headers regardless of ntop */
+ while ((line = fgetln(fp, &len))) {
+ session_write(s, line, len);
+ r->nlines -= 1;
+ if (strncmp(line , "\n", 1) == 0)
+ break;
+ }
+
+ /* print ntop lines of body */
+ while ((r->ntop-- > 0) && r->nlines-- &&
+ (line = fgetln(fp, &len)))
+ session_write(s, line, len);
+ } else
+ while (r->nlines-- && (line = fgetln(fp, &len)))
+ session_write(s, line, len);
+
+ session_reply(s, "%s", ".");
+ io_set_write(&s->io);
+ fclose(fp);
+ close(imsg->fd);
+}
+
+static void
+handle_dele(struct session *s, struct imsg *imsg)
+{
+ int *res = imsg->data;
+
+ if (*res == 0)
+ session_reply(s, "%s", "+OK marked for delete");
+ else
+ session_reply(s, "%s", "+ERR msg already marked delete");
+
+ io_set_write(&s->io);
+}
+
+/* DELEted msg's hash and sz will be zero, ignore them */
+static void
+handle_list(struct session *s, struct imsg *imsg)
+{
+ struct list_res *res = imsg->data;
+
+ res->idx += 1; /* POP3 index is 1 based */
+ if (res->uidl) {
+ if (strlen(res->u.hash))
+ session_reply(s, "+OK %zu %s", res->idx, res->u.hash);
+ else
+ session_reply(s, "-ERR marked for delete");
+ } else {
+ if (res->u.sz)
+ session_reply(s, "+OK %zu %zu", res->idx, res->u.sz);
+ else
+ session_reply(s, "-ERR marked for delete");
+ }
+
+ io_set_write(&s->io);
+}
+
+/* DELEted msg's hash and sz will be zero, ignore them */
+static void
+handle_list_all(struct session *s, struct imsg *imsg, int uidl)
+{
+ char *nhash = NULL;
+ size_t datalen, i, item_sz, j, nitems, *nsz = NULL;
+
+ datalen = imsg->hdr.len - sizeof(imsg->hdr);
+ item_sz = (uidl) ? SHA1_DIGEST_STRING_LENGTH : sizeof(size_t);
+ nitems = datalen / item_sz;
+ if (uidl)
+ nhash = imsg->data;
+ else
+ nsz = imsg->data;
+
+ session_reply(s, "+OK");
+ for (i = 0; i < nitems; i++) {
+ if (uidl) {
+ j = i * SHA1_DIGEST_STRING_LENGTH;
+ if (nhash[j])
+ session_reply(s, "%zu %s", i + 1, nhash + j);
+ } else {
+ if (nsz[i])
+ session_reply(s, "%zu %zu", i + 1, nsz[i]);
+ }
+ }
+
+ session_reply(s, ".");
+ io_set_write(&s->io);
+
+}
+
+static void
+handle_update(struct session *s, struct imsg *imsg)
+{
+ int *res = imsg->data;
+
+ if (*res == 0)
+ session_reply(s, "%s", "+OK maildrop updated");
+ else
+ session_reply(s, "%s", "-ERR maildrop update failed");
+
+ io_set_write(&s->io);
+ session_close(s, 1);
+}
+
+static void
+needfd(struct imsgev *iev)
+{
+ /* XXX */
+ fatalx("session needs an fd");
+}
+
+int
+session_cmp(struct session *a, struct session *b)
+{
+ if (a->id < b->id)
+ return (-1);
+
+ if (a->id > b->id)
+ return (1);
+
+ return (0);
+}
+
+void
+session_set_state(struct session *s, enum state newstate)
+{
+ pop3_debug("%u: %s -> %s", s->id, strstate(s->state),
+ strstate(newstate));
+ s->state = newstate;
+}
+
+#define CASE(x) case x : return #x
+static const char *
+strstate(enum state state)
+{
+ static char buf[32];
+
+ switch (state) {
+ CASE(AUTH);
+ CASE(TRANSACTION);
+ CASE(UPDATE);
+ default:
+ snprintf(buf, sizeof(buf), "%d ???", state);
+ return (buf);
+ }
+}
+
+void
+session_reply(struct session *s, char *fmt, ...)
+{
+ va_list ap;
+ int n;
+ char buf[MAXLINESIZE];
+
+ va_start(ap, fmt);
+ n = vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+ if (n == -1 || n > MAXLINESIZE)
+ fatalx("session_reply: response too long");
+
+ if (buf[0] == '+')
+ pop3_debug("<<< +OK");
+ else if (buf[0] == '-')
+ pop3_debug("<<< -ERR");
+
+ iobuf_xfqueue(&s->iobuf, "session_reply", "%s\r\n", buf);
+}
+
+static void
+session_write(struct session *s, const char *data, size_t len)
+{
+ /* remove terminating \n or \r\n if any */
+ if (data[len - 1] == '\n')
+ len -= 1;
+
+ if (data[len - 1] == '\r')
+ len -= 1;
+
+ /* byte stuff "." if at beginning of line */
+ if (data[0] == '.')
+ iobuf_xfqueue(&s->iobuf, "session_write", ".");
+
+ iobuf_xqueue(&s->iobuf, "session_write", data, len);
+ /* explicitly terminate with CRLF */
+ iobuf_xfqueue(&s->iobuf, "session_write", "\r\n");
+}
+
+static void
+pop3_debug(char *fmt, ...)
+{
+ va_list ap;
+ char buf[MAXLINESIZE];
+ int n;
+
+ if (!_pop3_debug)
+ return;
+
+ va_start(ap, fmt);
+ n = vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+ if (n == -1 || n > MAXLINESIZE)
+ fatalx("pop3_debug: response too long");
+
+ logit(LOG_DEBUG, "%s", buf);
+}
+
+SPLAY_GENERATE(session_tree, session, entry, session_cmp);
+