From 305e2fd5530ace4fc7c3e9665a4645a94efdfbd7 Mon Sep 17 00:00:00 2001 From: Sunil Nimmagadda Date: Thu, 27 Mar 2014 09:53:22 +0500 Subject: Import pop3d. --- .gitignore | 3 + Makefile | 14 + imsgev.c | 170 +++++++++++ imsgev.h | 49 ++++ iobuf.c | 467 ++++++++++++++++++++++++++++++ iobuf.h | 71 +++++ ioev.c | 910 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ioev.h | 69 +++++ maildir.c | 229 +++++++++++++++ maildrop.c | 412 ++++++++++++++++++++++++++ mbox.c | 206 +++++++++++++ pop3d.8 | 86 ++++++ pop3d.c | 230 +++++++++++++++ pop3d.h | 177 ++++++++++++ pop3e.c | 255 ++++++++++++++++ session.c | 754 ++++++++++++++++++++++++++++++++++++++++++++++++ ssl.c | 168 +++++++++++ ssl.h | 11 + ssl_privsep.c | 253 ++++++++++++++++ util.c | 183 ++++++++++++ 20 files changed, 4717 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 imsgev.c create mode 100644 imsgev.h create mode 100644 iobuf.c create mode 100644 iobuf.h create mode 100644 ioev.c create mode 100644 ioev.h create mode 100644 maildir.c create mode 100644 maildrop.c create mode 100644 mbox.c create mode 100644 pop3d.8 create mode 100644 pop3d.c create mode 100644 pop3d.h create mode 100644 pop3e.c create mode 100644 session.c create mode 100644 ssl.c create mode 100644 ssl.h create mode 100644 ssl_privsep.c create mode 100644 util.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7495889 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +CVS/ +obj/ +tags diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..58e4f80 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +PROG= pop3d +MAN= pop3d.8 +CFLAGS+= -Wall -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations -Wshadow -Wpointer-arith +CFLAGS+= -Wcast-qual -Wsign-compare +CFLAGS+= -DIO_SSL +DEBUG= -g +SRCS= pop3d.c pop3e.c session.c maildrop.c maildir.c mbox.c util.c +SRCS+= imsgev.c iobuf.c ioev.c +SRCS+= ssl.c ssl_privsep.c +LDADD+= -levent -lssl -lcrypto -lutil +DPADD= ${LIBEVENT} ${LIBSSL} ${LIBCRYPTO} ${LIBUTIL} + +.include diff --git a/imsgev.c b/imsgev.c new file mode 100644 index 0000000..6b92d79 --- /dev/null +++ b/imsgev.c @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2009 Eric Faurot + * + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "imsgev.h" + +void imsgev_add(struct imsgev *); +void imsgev_dispatch(int, short, void *); +void imsgev_disconnect(struct imsgev *, int); + +void +imsgev_init(struct imsgev *iev, int fd, void *data, + void (*callback)(struct imsgev *, int, struct imsg *), + void (*needfd)(struct imsgev *)) +{ + imsg_init(&iev->ibuf, fd); + iev->terminate = 0; + + iev->data = data; + iev->handler = imsgev_dispatch; + iev->callback = callback; + iev->needfd = needfd; + + iev->events = EV_READ; + event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev); + event_add(&iev->ev, NULL); +} + +int +imsgev_compose(struct imsgev *iev, u_int16_t type, u_int32_t peerid, + uint32_t pid, int fd, void *data, u_int16_t datalen) +{ + int r; + + r = imsg_compose(&iev->ibuf, type, peerid, pid, fd, data, datalen); + if (r != -1) + imsgev_add(iev); + + return (r); +} + +void +imsgev_close(struct imsgev *iev) +{ + iev->terminate = 1; + imsgev_add(iev); +} + +void +imsgev_clear(struct imsgev *iev) +{ + event_del(&iev->ev); + msgbuf_clear(&iev->ibuf.w); + close(iev->ibuf.fd); +} + +void +imsgev_add(struct imsgev *iev) +{ + short events = 0; + + if (!iev->terminate) + events = EV_READ; + if (iev->ibuf.w.queued || iev->terminate) + events |= EV_WRITE; + + /* optimization: skip event_{del/set/add} if already set */ + if (events == iev->events) + return; + + iev->events = events; + event_del(&iev->ev); + event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev); + event_add(&iev->ev, NULL); +} + +void +imsgev_dispatch(int fd, short ev, void *humppa) +{ + struct imsgev *iev = humppa; + struct imsgbuf *ibuf = &iev->ibuf; + struct imsg imsg; + ssize_t n; + + iev->events = 0; + + if (ev & EV_READ) { + if ((n = imsg_read(ibuf)) == -1) { + /* if we don't have enough fds, free one up and retry */ + if (errno == EAGAIN) { + iev->needfd(iev); + n = imsg_read(ibuf); + } + + if (n == -1) { + imsgev_disconnect(iev, IMSGEV_EREAD); + return; + } + } + if (n == 0) { + /* + * Connection is closed for reading, and we assume + * it is also closed for writing, so we error out + * if write data is pending. + */ + imsgev_disconnect(iev, + (iev->ibuf.w.queued) ? IMSGEV_EWRITE : IMSGEV_DONE); + return; + } + } + + if (ev & EV_WRITE) { + /* + * We wanted to write data out but the connection is either + * closed, or some error occured. Both case are not recoverable + * from the imsg perspective, so we treat it as a WRITE error. + */ + if ((n = msgbuf_write(&ibuf->w)) != 1) { + imsgev_disconnect(iev, IMSGEV_EWRITE); + return; + } + } + + while (iev->terminate == 0) { + if ((n = imsg_get(ibuf, &imsg)) == -1) { + imsgev_disconnect(iev, IMSGEV_EIMSG); + return; + } + if (n == 0) + break; + iev->callback(iev, IMSGEV_IMSG, &imsg); + imsg_free(&imsg); + } + + if (iev->terminate && iev->ibuf.w.queued == 0) { + imsgev_disconnect(iev, IMSGEV_DONE); + return; + } + + imsgev_add(iev); +} + +void +imsgev_disconnect(struct imsgev *iev, int code) +{ + iev->callback(iev, code, NULL); +} diff --git a/imsgev.h b/imsgev.h new file mode 100644 index 0000000..d429c50 --- /dev/null +++ b/imsgev.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2009 Eric Faurot + * + * 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. + */ + +#ifndef __IMSGEV_H__ +#define __IMSGEV_H__ + +#include +#include + +#define IMSG_LEN(m) ((m)->hdr.len - IMSG_HEADER_SIZE) + +struct imsgev { + struct imsgbuf ibuf; + void (*handler)(int, short, void *); + struct event ev; + void *data; + short events; + int terminate; + void (*callback)(struct imsgev *, int, struct imsg *); + void (*needfd)(struct imsgev *); +}; + +#define IMSGEV_IMSG 0 +#define IMSGEV_DONE 1 +#define IMSGEV_EREAD 2 +#define IMSGEV_EWRITE 3 +#define IMSGEV_EIMSG 4 + +void imsgev_init(struct imsgev *, int, void *, void (*)(struct imsgev *, + int, struct imsg *), void (*)(struct imsgev *)); +int imsgev_compose(struct imsgev *, u_int16_t, u_int32_t, u_int32_t, int, + void *, u_int16_t); +void imsgev_close(struct imsgev *); +void imsgev_clear(struct imsgev *); + +#endif /* __IMSGEV_H__ */ diff --git a/iobuf.c b/iobuf.c new file mode 100644 index 0000000..58f452d --- /dev/null +++ b/iobuf.c @@ -0,0 +1,467 @@ +/* $OpenBSD: iobuf.c,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef IO_SSL +#include +#include +#endif + +#include "iobuf.h" + +#define IOBUF_MAX 65536 +#define IOBUFQ_MIN 4096 + +struct ioqbuf *ioqbuf_alloc(struct iobuf *, size_t); +void iobuf_drain(struct iobuf *, size_t); + +int +iobuf_init(struct iobuf *io, size_t size, size_t max) +{ + memset(io, 0, sizeof *io); + + if (max == 0) + max = IOBUF_MAX; + + if (size == 0) + size = max; + + if (size > max) + return (-1); + + if ((io->buf = malloc(size)) == NULL) + return (-1); + + io->size = size; + io->max = max; + + return (0); +} + +void +iobuf_clear(struct iobuf *io) +{ + struct ioqbuf *q; + + if (io->buf) + free(io->buf); + + while ((q = io->outq)) { + io->outq = q->next; + free(q); + } + + memset(io, 0, sizeof (*io)); +} + +void +iobuf_drain(struct iobuf *io, size_t n) +{ + struct ioqbuf *q; + size_t left = n; + + while ((q = io->outq) && left) { + if ((q->wpos - q->rpos) > left) { + q->rpos += left; + left = 0; + } else { + left -= q->wpos - q->rpos; + io->outq = q->next; + free(q); + } + } + + io->queued -= (n - left); + if (io->outq == NULL) + io->outqlast = NULL; +} + +int +iobuf_extend(struct iobuf *io, size_t n) +{ + char *t; + + if (n > io->max) + return (-1); + + if (io->max - io->size < n) + return (-1); + + t = realloc(io->buf, io->size + n); + if (t == NULL) + return (-1); + + io->size += n; + io->buf = t; + + return (0); +} + +size_t +iobuf_left(struct iobuf *io) +{ + return io->size - io->wpos; +} + +size_t +iobuf_space(struct iobuf *io) +{ + return io->size - (io->wpos - io->rpos); +} + +size_t +iobuf_len(struct iobuf *io) +{ + return io->wpos - io->rpos; +} + +char * +iobuf_data(struct iobuf *io) +{ + return io->buf + io->rpos; +} + +void +iobuf_drop(struct iobuf *io, size_t n) +{ + if (n >= iobuf_len(io)) { + io->rpos = io->wpos = 0; + return; + } + + io->rpos += n; +} + +char * +iobuf_getline(struct iobuf *iobuf, size_t *rlen) +{ + char *buf; + size_t len, i; + + buf = iobuf_data(iobuf); + len = iobuf_len(iobuf); + + for (i = 0; i + 1 <= len; i++) + if (buf[i] == '\n') { + /* Note: the returned address points into the iobuf + * buffer. We NUL-end it for convenience, and discard + * the data from the iobuf, so that the caller doesn't + * have to do it. The data remains "valid" as long + * as the iobuf does not overwrite it, that is until + * the next call to iobuf_normalize() or iobuf_extend(). + */ + iobuf_drop(iobuf, i + 1); + len = (i && buf[i - 1] == '\r') ? i - 1 : i; + buf[len] = '\0'; + if (rlen) + *rlen = len; + return (buf); + } + + return (NULL); +} + +void +iobuf_normalize(struct iobuf *io) +{ + if (io->rpos == 0) + return; + + if (io->rpos == io->wpos) { + io->rpos = io->wpos = 0; + return; + } + + memmove(io->buf, io->buf + io->rpos, io->wpos - io->rpos); + io->wpos -= io->rpos; + io->rpos = 0; +} + +ssize_t +iobuf_read(struct iobuf *io, int fd) +{ + ssize_t n; + + n = read(fd, io->buf + io->wpos, iobuf_left(io)); + if (n == -1) { + /* XXX is this really what we want? */ + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_READ); + return (IOBUF_ERROR); + } + if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +struct ioqbuf * +ioqbuf_alloc(struct iobuf *io, size_t len) +{ + struct ioqbuf *q; + + if (len < IOBUFQ_MIN) + len = IOBUFQ_MIN; + + if ((q = malloc(sizeof(*q) + len)) == NULL) + return (NULL); + + q->rpos = 0; + q->wpos = 0; + q->size = len; + q->next = NULL; + q->buf = (char *)(q) + sizeof(*q); + + if (io->outqlast == NULL) + io->outq = q; + else + io->outqlast->next = q; + io->outqlast = q; + + return (q); +} + +size_t +iobuf_queued(struct iobuf *io) +{ + return io->queued; +} + +void * +iobuf_reserve(struct iobuf *io, size_t len) +{ + struct ioqbuf *q; + void *r; + + if (len == 0) + return (NULL); + + if (((q = io->outqlast) == NULL) || q->size - q->wpos <= len) { + if ((q = ioqbuf_alloc(io, len)) == NULL) + return (NULL); + } + + r = q->buf + q->wpos; + q->wpos += len; + io->queued += len; + + return (r); +} + +int +iobuf_queue(struct iobuf *io, const void *data, size_t len) +{ + void *buf; + + if (len == 0) + return (0); + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + memmove(buf, data, len); + + return (0); +} + +int +iobuf_queuev(struct iobuf *io, const struct iovec *iov, int iovcnt) +{ + int i; + size_t len = 0; + char *buf; + + for (i = 0; i < iovcnt; i++) + len += iov[i].iov_len; + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len == 0) + continue; + memmove(buf, iov[i].iov_base, iov[i].iov_len); + buf += iov[i].iov_len; + } + + return (0); + +} + +int +iobuf_fqueue(struct iobuf *io, const char *fmt, ...) +{ + va_list ap; + int len; + + va_start(ap, fmt); + len = iobuf_vfqueue(io, fmt, ap); + va_end(ap); + + return (len); +} + +int +iobuf_vfqueue(struct iobuf *io, const char *fmt, va_list ap) +{ + char *buf; + int len; + + len = vasprintf(&buf, fmt, ap); + + if (len == -1) + return (-1); + + len = iobuf_queue(io, buf, len); + free(buf); + + return (len); +} + +ssize_t +iobuf_write(struct iobuf *io, int fd) +{ + struct iovec iov[IOV_MAX]; + struct ioqbuf *q; + int i; + ssize_t n; + + i = 0; + for (q = io->outq; q ; q = q->next) { + if (i >= IOV_MAX) + break; + iov[i].iov_base = q->buf + q->rpos; + iov[i].iov_len = q->wpos - q->rpos; + i++; + } + + n = writev(fd, iov, i); + if (n == -1) { + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_WRITE); + if (errno == EPIPE) + return (IOBUF_CLOSED); + return (IOBUF_ERROR); + } + + iobuf_drain(io, n); + + return (n); +} + +int +iobuf_flush(struct iobuf *io, int fd) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write(io, fd)) < 0) + return (s); + + return (0); +} + +#ifdef IO_SSL + +int +iobuf_flush_ssl(struct iobuf *io, void *ssl) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write_ssl(io, ssl) < 0)) + return (s); + + return (0); +} + +ssize_t +iobuf_write_ssl(struct iobuf *io, void *ssl) +{ + struct ioqbuf *q; + int r; + ssize_t n; + + q = io->outq; + n = SSL_write(ssl, q->buf + q->rpos, q->wpos - q->rpos); + if (n <= 0) { + switch ((r = SSL_get_error(ssl, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_ZERO_RETURN: /* connection closed */ + return (IOBUF_CLOSED); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_SSLERROR); + if (r == 0) + errno = EPIPE; + return (IOBUF_ERROR); + default: + return (IOBUF_SSLERROR); + } + } + iobuf_drain(io, n); + + return (n); +} + +ssize_t +iobuf_read_ssl(struct iobuf *io, void *ssl) +{ + ssize_t n; + int r; + + n = SSL_read(ssl, io->buf + io->wpos, iobuf_left(io)); + if (n < 0) { + switch ((r = SSL_get_error(ssl, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_SSLERROR); + if (r == 0) + errno = EPIPE; + return (IOBUF_ERROR); + default: + return (IOBUF_SSLERROR); + } + } else if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +#endif /* IO_SSL */ diff --git a/iobuf.h b/iobuf.h new file mode 100644 index 0000000..7c5c85c --- /dev/null +++ b/iobuf.h @@ -0,0 +1,71 @@ +/* $OpenBSD: iobuf.h,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot + * + * 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 + +#include + +struct ioqbuf { + struct ioqbuf *next; + char *buf; + size_t size; + size_t wpos; + size_t rpos; +}; + +struct iobuf { + char *buf; + size_t max; + size_t size; + size_t wpos; + size_t rpos; + + size_t queued; + struct ioqbuf *outq; + struct ioqbuf *outqlast; +}; + +#define IOBUF_WANT_READ -1 +#define IOBUF_WANT_WRITE -2 +#define IOBUF_CLOSED -3 +#define IOBUF_ERROR -4 +#define IOBUF_SSLERROR -5 + +int iobuf_init(struct iobuf *, size_t, size_t); +void iobuf_clear(struct iobuf *); + +int iobuf_extend(struct iobuf *, size_t); +void iobuf_normalize(struct iobuf *); +void iobuf_drop(struct iobuf *, size_t); +size_t iobuf_space(struct iobuf *); +size_t iobuf_len(struct iobuf *); +size_t iobuf_left(struct iobuf *); +char *iobuf_data(struct iobuf *); +char *iobuf_getline(struct iobuf *, size_t *); +ssize_t iobuf_read(struct iobuf *, int); +ssize_t iobuf_read_ssl(struct iobuf *, void *); + +size_t iobuf_queued(struct iobuf *); +void* iobuf_reserve(struct iobuf *, size_t); +int iobuf_queue(struct iobuf *, const void*, size_t); +int iobuf_queuev(struct iobuf *, const struct iovec *, int); +int iobuf_fqueue(struct iobuf *, const char *, ...); +int iobuf_vfqueue(struct iobuf *, const char *, va_list); +int iobuf_flush(struct iobuf *, int); +int iobuf_flush_ssl(struct iobuf *, void *); +ssize_t iobuf_write(struct iobuf *, int); +ssize_t iobuf_write_ssl(struct iobuf *, void *); diff --git a/ioev.c b/ioev.c new file mode 100644 index 0000000..5ed5148 --- /dev/null +++ b/ioev.c @@ -0,0 +1,910 @@ +/* $OpenBSD: ioev.c,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ioev.h" +#include "iobuf.h" + +#ifdef IO_SSL +#include +#include +#endif + +enum { + IO_STATE_NONE, + IO_STATE_CONNECT, + IO_STATE_CONNECT_SSL, + IO_STATE_ACCEPT_SSL, + IO_STATE_UP, + + IO_STATE_MAX, +}; + +const char* io_strflags(int); +const char* io_evstr(short); + +void _io_init(void); +void io_hold(struct io *); +void io_release(struct io *); +void io_callback(struct io*, int); +void io_dispatch(int, short, void *); +void io_dispatch_connect(int, short, void *); +size_t io_pending(struct io *); +size_t io_queued(struct io*); +void io_reset(struct io *, short, void (*)(int, short, void*)); +void io_frame_enter(const char *, struct io *, int); +void io_frame_leave(struct io *); + +#ifdef IO_SSL +void ssl_error(const char *); /* XXX external */ + +static const char* io_ssl_error(void); +void io_dispatch_accept_ssl(int, short, void *); +void io_dispatch_connect_ssl(int, short, void *); +void io_dispatch_read_ssl(int, short, void *); +void io_dispatch_write_ssl(int, short, void *); +void io_reload_ssl(struct io *io); +#endif + +static struct io *current = NULL; +static uint64_t frame = 0; +static int _io_debug = 0; + +#define io_debug(args...) do { if (_io_debug) printf(args); } while(0) + + +const char* +io_strio(struct io *io) +{ + static char buf[128]; + char ssl[128]; + + ssl[0] = '\0'; +#ifdef IO_SSL + if (io->ssl) { + snprintf(ssl, sizeof ssl, " ssl=%s:%s:%i", + SSL_get_cipher_version(io->ssl), + SSL_get_cipher_name(io->ssl), + SSL_get_cipher_bits(io->ssl, NULL)); + } +#endif + + if (io->iobuf == NULL) + snprintf(buf, sizeof buf, + "", + io, io->sock, io->timeout, io_strflags(io->flags), ssl); + else + snprintf(buf, sizeof buf, + "", + io, io->sock, io->timeout, io_strflags(io->flags), ssl, + io_pending(io), io_queued(io)); + + return (buf); +} + +#define CASE(x) case x : return #x + +const char* +io_strevent(int evt) +{ + static char buf[32]; + + switch (evt) { + CASE(IO_CONNECTED); + CASE(IO_TLSREADY); + CASE(IO_TLSVERIFIED); + CASE(IO_DATAIN); + CASE(IO_LOWAT); + CASE(IO_DISCONNECTED); + CASE(IO_TIMEOUT); + CASE(IO_ERROR); + default: + snprintf(buf, sizeof(buf), "IO_? %i", evt); + return buf; + } +} + +void +io_set_blocking(int fd, int blocking) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL, 0)) == -1) + err(1, "io_set_blocking:fcntl(F_GETFL)"); + + if (blocking) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + + if ((flags = fcntl(fd, F_SETFL, flags)) == -1) + err(1, "io_set_blocking:fcntl(F_SETFL)"); +} + +void +io_set_linger(int fd, int linger) +{ + struct linger l; + + bzero(&l, sizeof(l)); + l.l_onoff = linger ? 1 : 0; + l.l_linger = linger; + if (setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) + err(1, "io_set_linger:setsockopt()"); +} + +/* + * Event framing must not rely on an io pointer to refer to the "same" io + * throughout the frame, beacuse this is not always the case: + * + * 1) enter(addr0) -> free(addr0) -> leave(addr0) = SEGV + * 2) enter(addr0) -> free(addr0) -> malloc == addr0 -> leave(addr0) = BAD! + * + * In both case, the problem is that the io is freed in the callback, so + * the pointer becomes invalid. If that happens, the user is required to + * call io_clear, so we can adapt the frame state there. + */ +void +io_frame_enter(const char *where, struct io *io, int ev) +{ + io_debug("\n=== %" PRIu64 " ===\n" + "io_frame_enter(%s, %s, %s)\n", + frame, where, io_evstr(ev), io_strio(io)); + + if (current) + errx(1, "io_frame_enter: interleaved frames"); + + current = io; + + io_hold(io); +} + +void +io_frame_leave(struct io *io) +{ + io_debug("io_frame_leave(%" PRIu64 ")\n", frame); + + if (current && current != io) + errx(1, "io_frame_leave: io mismatch"); + + /* io has been cleared */ + if (current == NULL) + goto done; + + /* TODO: There is a possible optimization there: + * In a typical half-duplex request/response scenario, + * the io is waiting to read a request, and when done, it queues + * the response in the output buffer and goes to write mode. + * There, the write event is set and will be triggered in the next + * event frame. In most case, the write call could be done + * immediatly as part of the last read frame, thus avoiding to go + * through the event loop machinery. So, as an optimisation, we + * could detect that case here and force an event dispatching. + */ + + /* Reload the io if it has not been reset already. */ + io_release(io); + current = NULL; + done: + io_debug("=== /%" PRIu64 "\n", frame); + + frame += 1; +} + +void +_io_init() +{ + static int init = 0; + + if (init) + return; + + init = 1; + _io_debug = getenv("IO_DEBUG") != NULL; +} + +void +io_init(struct io *io, int sock, void *arg, + void(*cb)(struct io*, int), struct iobuf *iobuf) +{ + _io_init(); + + memset(io, 0, sizeof *io); + + io->sock = sock; + io->timeout = -1; + io->arg = arg; + io->iobuf = iobuf; + io->cb = cb; + + if (sock != -1) + io_reload(io); +} + +void +io_clear(struct io *io) +{ + io_debug("io_clear(%p)\n", io); + + /* the current io is virtually dead */ + if (io == current) + current = NULL; + +#ifdef IO_SSL + if (io->ssl) { + SSL_shutdown(io->ssl); + SSL_free(io->ssl); + io->ssl = NULL; + } +#endif + + event_del(&io->ev); + if (io->sock != -1) { + close(io->sock); + io->sock = -1; + } +} + +void +io_hold(struct io *io) +{ + io_debug("io_enter(%p)\n", io); + + if (io->flags & IO_HELD) + errx(1, "io_hold: io is already held"); + + io->flags &= ~IO_RESET; + io->flags |= IO_HELD; +} + +void +io_release(struct io *io) +{ + if (!(io->flags & IO_HELD)) + errx(1, "io_release: io is not held"); + + io->flags &= ~IO_HELD; + if (!(io->flags & IO_RESET)) + io_reload(io); +} + +void +io_set_timeout(struct io *io, int msec) +{ + io_debug("io_set_timeout(%p, %i)\n", io, msec); + + io->timeout = msec; +} + +void +io_set_lowat(struct io *io, size_t lowat) +{ + io_debug("io_set_lowat(%p, %zu)\n", io, lowat); + + io->lowat = lowat; +} + +void +io_pause(struct io *io, int dir) +{ + io_debug("io_pause(%p, %x)\n", io, dir); + + io->flags |= dir & (IO_PAUSE_IN | IO_PAUSE_OUT); + io_reload(io); +} + +void +io_resume(struct io *io, int dir) +{ + io_debug("io_resume(%p, %x)\n", io, dir); + + io->flags &= ~(dir & (IO_PAUSE_IN | IO_PAUSE_OUT)); + io_reload(io); +} + +void +io_set_read(struct io *io) +{ + int mode; + + io_debug("io_set_read(%p)\n", io); + + mode = io->flags & IO_RW; + if (!(mode == 0 || mode == IO_WRITE)) + errx(1, "io_set_read(): full-duplex or reading"); + + io->flags &= ~IO_RW; + io->flags |= IO_READ; + io_reload(io); +} + +void +io_set_write(struct io *io) +{ + int mode; + + io_debug("io_set_write(%p)\n", io); + + mode = io->flags & IO_RW; + if (!(mode == 0 || mode == IO_READ)) + errx(1, "io_set_write(): full-duplex or writing"); + + io->flags &= ~IO_RW; + io->flags |= IO_WRITE; + io_reload(io); +} + +#define IO_READING(io) (((io)->flags & IO_RW) != IO_WRITE) +#define IO_WRITING(io) (((io)->flags & IO_RW) != IO_READ) + +/* + * Setup the necessary events as required by the current io state, + * honouring duplex mode and i/o pauses. + */ +void +io_reload(struct io *io) +{ + short events; + + /* io will be reloaded at release time */ + if (io->flags & IO_HELD) + return; + +#ifdef IO_SSL + if (io->ssl) { + io_reload_ssl(io); + return; + } +#endif + + io_debug("io_reload(%p)\n", io); + + events = 0; + if (IO_READING(io) && !(io->flags & IO_PAUSE_IN)) + events = EV_READ; + if (IO_WRITING(io) && !(io->flags & IO_PAUSE_OUT) && io_queued(io)) + events |= EV_WRITE; + + io_reset(io, events, io_dispatch); +} + +/* Set the requested event. */ +void +io_reset(struct io *io, short events, void (*dispatch)(int, short, void*)) +{ + struct timeval tv, *ptv; + + io_debug("io_reset(%p, %s, %p) -> %s\n", + io, io_evstr(events), dispatch, io_strio(io)); + + /* + * Indicate that the event has already been reset so that reload + * is not called on frame_leave. + */ + io->flags |= IO_RESET; + + event_del(&io->ev); + + /* + * The io is paused by the user, so we don't want the timeout to be + * effective. + */ + if (events == 0) + return; + + event_set(&io->ev, io->sock, events, dispatch, io); + if (io->timeout >= 0) { + tv.tv_sec = io->timeout / 1000; + tv.tv_usec = (io->timeout % 1000) * 1000; + ptv = &tv; + } else + ptv = NULL; + + event_add(&io->ev, ptv); +} + +size_t +io_pending(struct io *io) +{ + return iobuf_len(io->iobuf); +} + +size_t +io_queued(struct io *io) +{ + return iobuf_queued(io->iobuf); +} + +const char* +io_strflags(int flags) +{ + static char buf[64]; + + buf[0] = '\0'; + + switch (flags & IO_RW) { + case 0: + strlcat(buf, "rw", sizeof buf); + break; + case IO_READ: + strlcat(buf, "R", sizeof buf); + break; + case IO_WRITE: + strlcat(buf, "W", sizeof buf); + break; + case IO_RW: + strlcat(buf, "RW", sizeof buf); + break; + } + + if (flags & IO_PAUSE_IN) + strlcat(buf, ",F_PI", sizeof buf); + if (flags & IO_PAUSE_OUT) + strlcat(buf, ",F_PO", sizeof buf); + + return buf; +} + +const char* +io_evstr(short ev) +{ + static char buf[64]; + char buf2[16]; + int n; + + n = 0; + buf[0] = '\0'; + + if (ev == 0) { + strlcat(buf, "", sizeof(buf)); + return buf; + } + + if (ev & EV_TIMEOUT) { + strlcat(buf, "EV_TIMEOUT", sizeof(buf)); + ev &= ~EV_TIMEOUT; + n++; + } + + if (ev & EV_READ) { + if (n) + strlcat(buf, "|", sizeof(buf)); + strlcat(buf, "EV_READ", sizeof(buf)); + ev &= ~EV_READ; + n++; + } + + if (ev & EV_WRITE) { + if (n) + strlcat(buf, "|", sizeof(buf)); + strlcat(buf, "EV_WRITE", sizeof(buf)); + ev &= ~EV_WRITE; + n++; + } + + if (ev & EV_SIGNAL) { + if (n) + strlcat(buf, "|", sizeof(buf)); + strlcat(buf, "EV_SIGNAL", sizeof(buf)); + ev &= ~EV_SIGNAL; + n++; + } + + if (ev) { + if (n) + strlcat(buf, "|", sizeof(buf)); + strlcat(buf, "EV_?=0x", sizeof(buf)); + snprintf(buf2, sizeof(buf2), "%hx", ev); + strlcat(buf, buf2, sizeof(buf)); + } + + return buf; +} + +void +io_dispatch(int fd, short ev, void *humppa) +{ + struct io *io = humppa; + size_t w; + ssize_t n; + int saved_errno; + + io_frame_enter("io_dispatch", io, ev); + + if (ev == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if (ev & EV_WRITE && (w = io_queued(io))) { + if ((n = iobuf_write(io->iobuf, io->sock)) < 0) { + if (n == IOBUF_WANT_WRITE) /* kqueue bug? */ + goto read; + if (n == IOBUF_CLOSED) + io_callback(io, IO_DISCONNECTED); + else { + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + } + goto leave; + } + if (w > io->lowat && w - n <= io->lowat) + io_callback(io, IO_LOWAT); + } + read: + + if (ev & EV_READ) { + if ((n = iobuf_read(io->iobuf, io->sock)) < 0) { + if (n == IOBUF_CLOSED) + io_callback(io, IO_DISCONNECTED); + else { + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + } + goto leave; + } + if (n) + io_callback(io, IO_DATAIN); + } + +leave: + io_frame_leave(io); +} + +void +io_callback(struct io *io, int evt) +{ + io->cb(io, evt); +} + +int +io_connect(struct io *io, const struct sockaddr *sa, const struct sockaddr *bsa) +{ + int sock, errno_save; + + if ((sock = socket(sa->sa_family, SOCK_STREAM, 0)) == -1) + goto fail; + + io_set_blocking(sock, 0); + io_set_linger(sock, 0); + + if (bsa && bind(sock, bsa, bsa->sa_len) == -1) + goto fail; + + if (connect(sock, sa, sa->sa_len) == -1) + if (errno != EINPROGRESS) + goto fail; + + io->sock = sock; + io_reset(io, EV_WRITE, io_dispatch_connect); + + return (sock); + + fail: + if (sock != -1) { + errno_save = errno; + close(sock); + errno = errno_save; + io->error = strerror(errno); + } + return (-1); +} + +void +io_dispatch_connect(int fd, short ev, void *humppa) +{ + struct io *io = humppa; + int r, e; + socklen_t sl; + + io_frame_enter("io_dispatch_connect", io, ev); + + if (ev == EV_TIMEOUT) { + close(fd); + io->sock = -1; + io_callback(io, IO_TIMEOUT); + } else { + sl = sizeof(e); + r = getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &sl); + if (r == -1) { + warn("io_dispatch_connect: getsockopt"); + e = errno; + } + if (e) { + close(fd); + io->sock = -1; + io->error = strerror(e); + io_callback(io, e == ETIMEDOUT ? IO_TIMEOUT : IO_ERROR); + } + else { + io->state = IO_STATE_UP; + io_callback(io, IO_CONNECTED); + } + } + + io_frame_leave(io); +} + +#ifdef IO_SSL + +static const char* +io_ssl_error(void) +{ + static char buf[128]; + unsigned long e; + + e = ERR_peek_last_error(); + if (e) { + ERR_error_string(e, buf); + return (buf); + } + + return ("No SSL error"); +} + +int +io_start_tls(struct io *io, void *ssl) +{ + int mode; + + mode = io->flags & IO_RW; + if (mode == 0 || mode == IO_RW) + errx(1, "io_start_tls(): full-duplex or unset"); + + if (io->ssl) + errx(1, "io_start_tls(): SSL already started"); + io->ssl = ssl; + + if (SSL_set_fd(io->ssl, io->sock) == 0) { + ssl_error("io_start_ssl:SSL_set_fd"); + return (-1); + } + + if (mode == IO_WRITE) { + io->state = IO_STATE_CONNECT_SSL; + SSL_set_connect_state(io->ssl); + io_reset(io, EV_WRITE, io_dispatch_connect_ssl); + } else { + io->state = IO_STATE_ACCEPT_SSL; + SSL_set_accept_state(io->ssl); + io_reset(io, EV_READ, io_dispatch_accept_ssl); + } + + return (0); +} + +void +io_dispatch_accept_ssl(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int e, ret; + + io_frame_enter("io_dispatch_accept_ssl", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if ((ret = SSL_accept(io->ssl)) > 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_TLSREADY); + goto leave; + } + + switch ((e = SSL_get_error(io->ssl, ret))) { + case SSL_ERROR_WANT_READ: + io_reset(io, EV_READ, io_dispatch_accept_ssl); + break; + case SSL_ERROR_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_accept_ssl); + break; + default: + io->error = io_ssl_error(); + ssl_error("io_dispatch_accept_ssl:SSL_accept"); + io_callback(io, IO_ERROR); + break; + } + + leave: + io_frame_leave(io); +} + +void +io_dispatch_connect_ssl(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int e, ret; + + io_frame_enter("io_dispatch_connect_ssl", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if ((ret = SSL_connect(io->ssl)) > 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_TLSREADY); + goto leave; + } + + switch ((e = SSL_get_error(io->ssl, ret))) { + case SSL_ERROR_WANT_READ: + io_reset(io, EV_READ, io_dispatch_connect_ssl); + break; + case SSL_ERROR_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_connect_ssl); + break; + default: + io->error = io_ssl_error(); + ssl_error("io_dispatch_connect_ssl:SSL_connect"); + io_callback(io, IO_ERROR); + break; + } + + leave: + io_frame_leave(io); +} + +void +io_dispatch_read_ssl(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int n, saved_errno; + + io_frame_enter("io_dispatch_read_ssl", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + +again: + switch ((n = iobuf_read_ssl(io->iobuf, (SSL*)io->ssl))) { + case IOBUF_WANT_READ: + io_reset(io, EV_READ, io_dispatch_read_ssl); + break; + case IOBUF_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_read_ssl); + break; + case IOBUF_CLOSED: + io_callback(io, IO_DISCONNECTED); + break; + case IOBUF_ERROR: + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + break; + case IOBUF_SSLERROR: + io->error = io_ssl_error(); + ssl_error("io_dispatch_read_ssl:SSL_read"); + io_callback(io, IO_ERROR); + break; + default: + io_debug("io_dispatch_read_ssl(...) -> r=%i\n", n); + io_callback(io, IO_DATAIN); + if (current == io && IO_READING(io) && SSL_pending(io->ssl)) + goto again; + } + + leave: + io_frame_leave(io); +} + +void +io_dispatch_write_ssl(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int n, saved_errno; + size_t w2, w; + + io_frame_enter("io_dispatch_write_ssl", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + w = io_queued(io); + switch ((n = iobuf_write_ssl(io->iobuf, (SSL*)io->ssl))) { + case IOBUF_WANT_READ: + io_reset(io, EV_READ, io_dispatch_write_ssl); + break; + case IOBUF_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_write_ssl); + break; + case IOBUF_CLOSED: + io_callback(io, IO_DISCONNECTED); + break; + case IOBUF_ERROR: + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + break; + case IOBUF_SSLERROR: + io->error = io_ssl_error(); + ssl_error("io_dispatch_write_ssl:SSL_write"); + io_callback(io, IO_ERROR); + break; + default: + io_debug("io_dispatch_write_ssl(...) -> w=%i\n", n); + w2 = io_queued(io); + if (w > io->lowat && w2 <= io->lowat) + io_callback(io, IO_LOWAT); + break; + } + + leave: + io_frame_leave(io); +} + +void +io_reload_ssl(struct io *io) +{ + short ev = 0; + void (*dispatch)(int, short, void*) = NULL; + + switch (io->state) { + case IO_STATE_CONNECT_SSL: + ev = EV_WRITE; + dispatch = io_dispatch_connect_ssl; + break; + case IO_STATE_ACCEPT_SSL: + ev = EV_READ; + dispatch = io_dispatch_accept_ssl; + break; + case IO_STATE_UP: + ev = 0; + if (IO_READING(io) && !(io->flags & IO_PAUSE_IN)) { + ev = EV_READ; + dispatch = io_dispatch_read_ssl; + } + else if (IO_WRITING(io) && !(io->flags & IO_PAUSE_OUT) && io_queued(io)) { + ev = EV_WRITE; + dispatch = io_dispatch_write_ssl; + } + if (! ev) + return; /* paused */ + break; + default: + errx(1, "io_reload_ssl(): bad state"); + } + + io_reset(io, ev, dispatch); +} + +#endif /* IO_SSL */ diff --git a/ioev.h b/ioev.h new file mode 100644 index 0000000..3353013 --- /dev/null +++ b/ioev.h @@ -0,0 +1,69 @@ +/* $OpenBSD: ioev.h,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot + * + * 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 + +enum { + IO_CONNECTED = 0, /* connection successful */ + IO_TLSREADY, /* TLS started succesfully */ + IO_TLSVERIFIED, /* XXX - needs more work */ + IO_DATAIN, /* new data in input buffer */ + IO_LOWAT, /* output queue running low */ + IO_DISCONNECTED, /* error? */ + IO_TIMEOUT, /* error? */ + IO_ERROR, /* details? */ +}; + +#define IO_READ 0x01 +#define IO_WRITE 0x02 +#define IO_RW (IO_READ | IO_WRITE) +#define IO_PAUSE_IN 0x04 +#define IO_PAUSE_OUT 0x08 +#define IO_RESET 0x10 /* internal */ +#define IO_HELD 0x20 /* internal */ + +struct iobuf; +struct io { + int sock; + void *arg; + void (*cb)(struct io*, int); + struct iobuf *iobuf; + size_t lowat; + int timeout; + int flags; + int state; + struct event ev; + void *ssl; + const char *error; /* only valid immediatly on callback */ +}; + +void io_set_blocking(int, int); +void io_set_linger(int, int); + +void io_init(struct io*, int, void*, void(*)(struct io*, int), struct iobuf*); +void io_clear(struct io*); +void io_set_read(struct io *); +void io_set_write(struct io *); +void io_set_timeout(struct io *, int); +void io_set_lowat(struct io *, size_t); +void io_pause(struct io *, int); +void io_resume(struct io *, int); +void io_reload(struct io *); +int io_connect(struct io *, const struct sockaddr *, const struct sockaddr *); +int io_start_tls(struct io *, void *); +const char* io_strio(struct io *); +const char* io_strevent(int); diff --git a/maildir.c b/maildir.c new file mode 100644 index 0000000..f3d7178 --- /dev/null +++ b/maildir.c @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "pop3d.h" + +static int init(struct mdrop *, size_t *, size_t *); +static int retr(struct mdrop *, unsigned int, size_t *, size_t *); +static int update(struct mdrop *); +static int new_to_cur(struct mdrop *); +static int msgcmp(struct msg *, struct msg *); +RB_PROTOTYPE(msgtree, msg, e.t_entry, msgcmp); + +struct m_backend m_backend_maildir = { + init, + retr, + update +}; + +/* + * No resource management on error path as the process is + * killed if an error occurs. + */ +static int +init(struct mdrop *m, size_t *nmsgs, size_t *sz) +{ + SHA1_CTX ctx; + struct stat sb; + char buf[MAXBSIZE]; + DIR *dirp; + struct dirent *dp; + struct msg *msg; + u_char *C; + size_t i; + ssize_t len; + int cur_fd, msg_fd; + + *nmsgs = 0; + *sz = 0; + if (new_to_cur(m) == -1) { + logit(LOG_WARNING, "maildir: move msgs from new to cur failed"); + return (-1); + } + + if ((cur_fd = openat(m->fd, "cur", O_RDONLY)) == -1) { + logit(LOG_CRIT, "maildir: unable to open \"cur\" dir"); + return (-1); + } + + if ((dirp = fdopendir(cur_fd)) == NULL) + return (-1); + + while ((dp = readdir(dirp))) { + if (dp->d_type != DT_REG) + continue; + + if (strcmp(dp->d_name, ".") == 0 || + strcmp(dp->d_name, "..") == 0) + continue; + + msg = xcalloc(1, sizeof(*msg), "init"); + if ((msg->u.fname = strdup(dp->d_name)) == NULL) + fatalx("init: strdup"); + + if (fstatat(cur_fd, dp->d_name, &sb, 0) == -1) { + logit(LOG_CRIT, "%s fstatat failed", dp->d_name); + return (-1); + } + + msg->sz = sb.st_size; + if ((msg_fd = openat(cur_fd, dp->d_name, O_RDONLY)) == -1) { + logit(LOG_CRIT, "%s openat failed", dp->d_name); + return (-1); + } + + SHA1Init(&ctx); + while (( len = read(msg_fd, buf, sizeof(buf))) > 0) { + SHA1Update(&ctx, (u_int8_t *)buf, len); + for (C = buf;len--; ++C) + if (*C == '\n') + msg->nlines += 1; + } + + SHA1End(&ctx, msg->hash); + close(msg_fd); + RB_INSERT(msgtree, &m->e.t_msgs, msg); + m->nmsgs += 1; + } + + /* allocate space for nmsgs of struct msg pointers */ + m->msgs_index = xcalloc(m->nmsgs, sizeof(msg), "init"); + *nmsgs = m->nmsgs; + i = 0; + *sz = 0; + RB_FOREACH(msg, msgtree, &m->e.t_msgs) { + m->msgs_index[i++] = msg; + /* calculate maildir size by counting newline as 2 (CRLF) */ + *sz += msg->sz + msg->nlines; + } + + closedir(dirp); + close(cur_fd); + return (0); +} + +static int +new_to_cur(struct mdrop *m) +{ + DIR *dirp; + struct dirent *dp; + int cur_fd, new_fd; + + + if ((cur_fd = openat(m->fd, "cur", O_RDONLY)) == -1) { + logit(LOG_CRIT, "maildir: unable to open \"cur\" dir"); + return (-1); + } + + if ((new_fd = openat(m->fd, "new", O_RDONLY)) == -1) { + logit(LOG_CRIT, "maildir: unable to open \"new\" dir"); + return (-1); + } + + if ((dirp = fdopendir(new_fd)) == NULL) + return (-1); + + while ((dp = readdir(dirp))) { + if (dp->d_type != DT_REG) + continue; + + if (strcmp(dp->d_name, ".") == 0 || + strcmp(dp->d_name, "..") == 0) + continue; + + if(renameat(new_fd, dp->d_name, cur_fd, dp->d_name) == -1) { + logit(LOG_CRIT, "maildir: renameat failed"); + return (-1); + } + } + + closedir(dirp); + close(cur_fd); + close(new_fd); + return (0); +} + +static int +retr(struct mdrop *m, unsigned int idx, size_t *nlines, size_t *offset) +{ + char buf[MAXPATHLEN]; + int fd, r; + + *offset = 0; + r = snprintf(buf, sizeof(buf), "cur/%s", m->msgs_index[idx]->u.fname); + if ((u_int)r >= sizeof(buf)) { + logit(LOG_WARNING, "path too long"); + return (-1); + } + + fd = openat(m->fd, buf, O_RDONLY); + return (fd); +} + +static int +update(struct mdrop *m) +{ + char buf[MAXPATHLEN]; + size_t i, j = 0; + int r; + + for (i = 0; i < m->nmsgs; i++) + if (m->msgs_index[i]->flags & F_DELE) + j += 1; + + if (j == 0) /* nothing to update */ + return (0); + + for (i = 0; i < m->nmsgs; i++) { + if (!(m->msgs_index[i]->flags & F_DELE)) + continue; + + r = snprintf(buf, sizeof(buf), "cur/%s", + m->msgs_index[i]->u.fname); + if ((u_int)r >= sizeof(buf)) { + logit(LOG_WARNING, "path too long"); + return (1); + } + + if (unlinkat(m->fd, buf, 0) == -1) { + logit(LOG_CRIT, "%s unlink failed", buf); + return (1); + } + } + + return (0); +} + +static int +msgcmp(struct msg *m1, struct msg *m2) +{ + return strcmp(m1->u.fname, m2->u.fname); +} + +RB_GENERATE(msgtree, msg, e.t_entry, msgcmp); diff --git a/maildrop.c b/maildrop.c new file mode 100644 index 0000000..634fa3f --- /dev/null +++ b/maildrop.c @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imsgev.h" +#include "pop3d.h" + +static void session_imsgev(struct imsgev *, int, struct imsg *); +static void update(struct imsgev *, struct imsg *, struct m_backend *); +static void retr(struct imsgev *, struct imsg *, struct m_backend *); +static void dele(struct imsgev *, struct imsg *, struct m_backend *); +static void rset(struct imsgev *, struct imsg *, struct m_backend *); +static void list(struct imsgev *, struct imsg *, struct m_backend *); +static void list_all(struct imsgev *, struct imsg *, struct m_backend *, int); +static void do_list(unsigned int, size_t *, char *, size_t); +static void *do_list_all(int, size_t *); +static struct m_backend *m_backend_lookup(enum m_type); +static void sig_handler(int, short, void *); +static void needfd(struct imsgev *); +static size_t expand(char *, const char *, size_t, struct passwd *); + +static struct mdrop m; + +void +maildrop_init(uint32_t session_id, int pair[2], struct passwd *pw, + int type, const char *path) +{ + struct imsgev iev_session; + struct event ev_sigint, ev_sigterm; + struct stats stats; + struct m_backend *mb; + char buf[MAXPATHLEN]; + pid_t pid; + mode_t old_mask; + int fd, flags, res = -1; + + if (seteuid(pw->pw_uid) < 0) + fatal("cannot lower privileges"); + + pid = fork(); + if (seteuid(0) < 0) + fatal("cannot restore privileges"); + + if (pid < 0) + fatal("maildrop: fork"); + + if (pid > 0) + return; + + if (seteuid(pw->pw_uid) < 0) + fatal("cannot lower privileges"); + + close(pair[0]); + setproctitle("maildrop"); + if ((mb = m_backend_lookup(type)) == NULL) + fatalx("maildrop: invalid backend"); + + if (expand(buf, path, sizeof(buf), pw) >= sizeof(buf)) + fatalx("maildrop: path truncation"); + + flags = O_CREAT; + if (type == M_MBOX) + flags |= O_RDWR; + else + flags |= O_RDONLY; + + old_mask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + if ((fd = open(buf, flags)) == -1) + logit(LOG_CRIT, "%zu: failed to open %s", session_id , buf); + + if (fd != -1) { + m.fd = fd; + res = mb->init(&m, &stats.nmsgs, &stats.sz); + } + + umask(old_mask); + if (seteuid(0) < 0) + fatal("cannot restore privileges"); + + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + fatal("cannot drop privileges"); + + event_init(); + signal_set(&ev_sigint, SIGINT, sig_handler, NULL); + signal_set(&ev_sigterm, SIGTERM, sig_handler, NULL); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + imsgev_init(&iev_session, pair[1], mb, session_imsgev, needfd); + + if (res == 0) { + imsgev_xcompose(&iev_session, IMSG_MAILDROP_INIT, session_id, + 0, -1, &stats, sizeof(struct stats), "maildrop_init"); + } else { + logit(LOG_CRIT, "%zu: maildrop init failed %s", + session_id, buf); + imsgev_xcompose(&iev_session, IMSG_MAILDROP_INIT, session_id, + 0, -1, NULL, 0, "maildrop_init"); + } + + if (event_dispatch() < 0) + fatal("event_dispatch"); + + logit(LOG_INFO, "maildrop process exiting"); + _exit(0); +} + +/* + * Build dst by substituting '~' with user's home dir and '%u' with user name + * in src. Return the length of string built. If return value >= dst_sz then + * dst is truncated. + */ +static size_t +expand(char *dst, const char *src, size_t dst_sz, struct passwd *pw) +{ + size_t i = 0, r; + int c; + + while ((c = *src++)) { + if (i >= dst_sz) + break; + + switch (c) { + case '~': + if ((r = strlcpy(&dst[i], pw->pw_dir, + (dst_sz - i))) >= (dst_sz - i)) { + i += r; + goto end; + } + i += r; + break; + case '%': + if (*src == 'u') { + if ((r = strlcpy(&dst[i], pw->pw_name, + (dst_sz - i))) >= (dst_sz - i)) { + i += r; + goto end; + } + i += r; + src++; + } else + dst[i++] = c; + break; + default: + dst[i++] = c; + break; + } + } + +end: + if (c) + while ((c = *src++)) + i++; + + dst[dst_sz - 1] = '\0'; + return (i); +} + +static void +session_imsgev(struct imsgev *iev, int code, struct imsg *imsg) +{ + struct m_backend *mb = iev->data; + int uidl = 0; + + switch (code) { + case IMSGEV_IMSG: + switch (imsg->hdr.type) { + case IMSG_MAILDROP_UPDATE: + update(iev, imsg, mb); + break; + case IMSG_MAILDROP_RETR: + retr(iev, imsg, mb); + break; + case IMSG_MAILDROP_DELE: + dele(iev, imsg, mb); + break; + case IMSG_MAILDROP_RSET: + rset(iev, imsg, mb); + break; + case IMSG_MAILDROP_LIST: + list(iev, imsg, mb); + break; + case IMSG_MAILDROP_UIDLALL: + uidl = 1; + /* FALLTHROUGH */ + case IMSG_MAILDROP_LISTALL: + list_all(iev, imsg, mb, uidl); + 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("maildrop: imsgev read/write error"); + break; + case IMSGEV_DONE: + event_loopexit(NULL); + break; + } +} + +static void +update(struct imsgev *iev, struct imsg *imsg, struct m_backend *mb) +{ + int res; + uint32_t session_id = imsg->hdr.peerid; + + if ((res = mb->update(&m)) == 0) + logit(LOG_INFO, "%zu: maildrop updated", session_id); + else + logit(LOG_CRIT, "%zu: maildrop updated failed", session_id); + + imsgev_xcompose(iev, IMSG_MAILDROP_UPDATE, session_id, 0, + -1, &res, sizeof(res), "maildrop_update"); +} + +static void +retr(struct imsgev *iev, struct imsg *imsg, struct m_backend *mb) +{ + struct retr_res res; + struct retr_req *req = imsg->data; + int fd; + + fd = mb->retr(&m, req->idx, &res.nlines, &res.offset); + /* pass on top arguments */ + res.top = req->top; + res.ntop = req->ntop; + imsgev_xcompose(iev, IMSG_MAILDROP_RETR, imsg->hdr.peerid, 0, + fd, &res, sizeof(res), "maildrop_retr"); +} + +static void +dele(struct imsgev *iev, struct imsg *imsg, struct m_backend *mb) +{ + unsigned int *idx = imsg->data; + int res = 0; + + if (m.msgs_index[*idx]->flags & F_DELE) { + res = -1; + goto end; + } + + m.msgs_index[*idx]->flags |= F_DELE; +end: + imsgev_xcompose(iev, IMSG_MAILDROP_DELE, imsg->hdr.peerid, 0, + -1, &res, sizeof(res), "maildrop_dele"); +} + +static void +rset(struct imsgev *iev, struct imsg *imsg, struct m_backend *mb) +{ + size_t i; + + for (i = 0; i < m.nmsgs; i++) + m.msgs_index[i]->flags = 0; + + imsgev_xcompose(iev, IMSG_MAILDROP_RSET, imsg->hdr.peerid, 0, + -1, NULL, 0, "maildrop_rset"); +} + +static void +list(struct imsgev *iev, struct imsg *imsg, struct m_backend *mb) +{ + struct list_req *req = imsg->data; + struct list_res res; + char hash[SHA1_DIGEST_STRING_LENGTH]; + size_t sz; + + res.idx = req->idx; + do_list(req->idx, &sz, hash, sizeof(hash)); + res.uidl = req->uidl; + if (res.uidl) + strlcpy(res.u.hash, hash, sizeof(res.u.hash)); + else + res.u.sz = sz; + + imsgev_xcompose(iev, IMSG_MAILDROP_LIST, imsg->hdr.peerid, 0, + -1, &res, sizeof(res), "maildrop_list"); + +} + +static void +list_all(struct imsgev *iev, struct imsg *imsg, struct m_backend *mb, int uidl) +{ + void *res; + size_t sz; + + res = do_list_all(uidl, &sz); + /* XXX watchout for sz > MAX_IMSGSIZE */ + imsgev_xcompose(iev, + (uidl) ? IMSG_MAILDROP_UIDLALL : IMSG_MAILDROP_LISTALL, + imsg->hdr.peerid, 0, -1, res, sz, "maildrop_list"); +} + +static void +do_list(unsigned int idx, size_t *sz, char *hash, size_t hash_sz) +{ + if (m.msgs_index[idx]->flags & F_DELE) { + *sz = 0; + strlcpy(hash, "", hash_sz); + return; + } + + *sz = m.msgs_index[idx]->sz; + strlcpy(hash, m.msgs_index[idx]->hash, hash_sz); +} + +static void * +do_list_all(int uidl, size_t *sz) +{ + size_t i, j, *nsz = NULL; + char *nhash = NULL; + + if (uidl) { + nhash = xcalloc(m.nmsgs, SHA1_DIGEST_STRING_LENGTH, "list_all"); + } else + nsz = xcalloc(m.nmsgs, sizeof(size_t), "list_all"); + + for (i = 0; i < m.nmsgs; i++) { + + if (uidl) { + j = i * SHA1_DIGEST_STRING_LENGTH; + if (m.msgs_index[i]->flags & F_DELE) + nhash[j] = '\0'; + else + strlcpy(nhash + j, m.msgs_index[i]->hash, + SHA1_DIGEST_STRING_LENGTH); + } else { + if (m.msgs_index[i]->flags & F_DELE) + nsz[i] = 0; + else + nsz[i] = m.msgs_index[i]->sz; + } + } + + if (uidl) { + *sz = m.nmsgs * SHA1_DIGEST_STRING_LENGTH; + return (nhash); + } else { + *sz = m.nmsgs * sizeof(size_t); + return (nsz); + } +} + +static void +needfd(struct imsgev *iev) +{ + fatalx("maildrop should never need an fd"); +} + +static void +sig_handler(int sig, short event, void *arg) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + event_loopexit(NULL); + } +} + +extern struct m_backend m_backend_mbox; +extern struct m_backend m_backend_maildir; + +static struct m_backend * +m_backend_lookup(enum m_type type) +{ + switch (type) { + case M_MBOX: + return &m_backend_mbox; + break; + case M_MAILDIR: + return &m_backend_maildir; + break; + default: + fatalx("m_backend_lookup: invalid m_type"); + }; + + return (NULL); +} + diff --git a/mbox.c b/mbox.c new file mode 100644 index 0000000..83b3d80 --- /dev/null +++ b/mbox.c @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "pop3d.h" + +static int init(struct mdrop *, size_t *, size_t *); +static int retr(struct mdrop *, unsigned int, size_t *, size_t *); +static int update(struct mdrop *); + +struct m_backend m_backend_mbox = { + init, + retr, + update +}; + +/* + * Parse mbox calculating each message's offset, size and hash. + * A message is identified by "From " at the start of a line. + * Returns 0 on success with nmsgs and sz populated. + */ +int +init(struct mdrop *m, size_t *nmsgs, size_t *sz) +{ + SHA1_CTX ctx; + FILE *fp; + struct msg *msg = NULL; + size_t i, len; + long offset; + char *line; + + *nmsgs = 0; + *sz = 0; + if (flock(m->fd, LOCK_EX|LOCK_NB) == -1) { + switch (errno) { + case EWOULDBLOCK: + logit(LOG_INFO, "mbox: locked by other process"); + return (-1); + default: + fatal("flock(LOCK_EX)"); + } + } + + if ((fp = fdopen(dup(m->fd), "r+")) == NULL) { + logit(LOG_INFO, "mbox: fdopen failed"); + return (-1); + } + + SIMPLEQ_INIT(&m->e.q_msgs); + offset = ftell(fp); + while ((line = fgetln(fp, &len))) { + if ((len > 5) && strncmp("From ", line, 5) == 0) { + if (msg) + SHA1End(&ctx, msg->hash); + + msg = xcalloc(1, sizeof(*msg), "init"); + SHA1Init(&ctx); + msg->u.offset = offset; + m->nmsgs += 1; + SIMPLEQ_INSERT_TAIL(&m->e.q_msgs, msg, e.q_entry); + } else { + if (msg == NULL) + fatalx("mbox corrupted: no \"From \" line"); + + msg->sz += len; + msg->nlines += 1; + SHA1Update(&ctx, (u_int8_t *)line, len); + offset = ftell(fp); + } + } + + if (msg) + SHA1End(&ctx, msg->hash); + + /* allocate space for nmsgs of struct msg pointers */ + m->msgs_index = xcalloc(m->nmsgs, sizeof(msg), "make_index"); + i = 0; + SIMPLEQ_FOREACH(msg, &m->e.q_msgs, e.q_entry) { + m->msgs_index[i++] = msg; + /* calculate mbox size by counting newline as 2 (CRLF) */ + m->sz += msg->sz + msg->nlines; + } + + *nmsgs = m->nmsgs; + *sz = m->sz; + fclose(fp); + return (0); +} + +static int +retr(struct mdrop *m, unsigned int idx, size_t *nlines, size_t *offset) +{ + if (m->msgs_index[idx]->flags & F_DELE) + return (-1); + + *offset = m->msgs_index[idx]->u.offset; + *nlines = m->msgs_index[idx]->nlines; + return (dup(m->fd)); /* imsg closes sender's fd */ +} + +/* + * No resource management as this process is blown away + * upon success or error. + */ +static int +update(struct mdrop *m) +{ + struct msg *cur; + size_t i, j = 0, len, nlines; + char buf[MAXBSIZE], fn[22], *line; + FILE *tmp_fp, *m_fp; + mode_t old_mask; + int tmp_fd; + + for (i = 0; i < m->nmsgs; i++) + if (m->msgs_index[i]->flags & F_DELE) + j += 1; + + if ((m_fp = fdopen(dup(m->fd), "r+")) == NULL) { + logit(LOG_INFO, "mbox: fdopen failed"); + return (-1); + } + + if (j == 0) + return (0); + else if (j == m->nmsgs) { + if (ftruncate(fileno(m_fp), 0) == -1) { + logit(LOG_CRIT, "update: ftruncate failed"); + return (1); + } + + return (0); + } + + strlcpy(fn, "/tmp/pop3d.XXXXXXXXXX", sizeof(fn)); + old_mask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + if ((tmp_fd = mkstemp(fn)) == -1 || + (tmp_fp = fdopen(tmp_fd, "r+")) == NULL) { + logit(LOG_CRIT, "mbox: mkstemp failed"); + return (1); + } + + umask(old_mask); + for (i = 0; i < m->nmsgs; i++) { + cur = m->msgs_index[i]; + if (cur->flags & F_DELE) + continue; + + if (fseek(m_fp, cur->u.offset, SEEK_SET) == -1) { + logit(LOG_CRIT, "update: fseek failed"); + return (1); + } + /* + * "From " line isn't counted in nlines but offset starts + * there, adjust nlines here + */ + nlines = m->msgs_index[i]->nlines + 1; + while (nlines--) { + if ((line = fgetln(m_fp, &len))) + if (fwrite(line, len, 1, tmp_fp) != 1) + fatalx("update: short write"); + } + } + + fflush(tmp_fp); + rewind(m_fp); + rewind(tmp_fp); + while (!feof(tmp_fp)) { + fread(buf, sizeof(buf), 1, tmp_fp); + if (fwrite(buf, sizeof(buf), 1, m_fp) != 1) + fatalx("update: short write"); + } + + fflush(m_fp); + if (ftruncate(fileno(m_fp), ftello(tmp_fp)) == -1) + fatal("update: failed to truncate"); + + fclose(m_fp); + return (0); +} + diff --git a/pop3d.8 b/pop3d.8 new file mode 100644 index 0000000..344316c --- /dev/null +++ b/pop3d.8 @@ -0,0 +1,86 @@ +.\" Copyright (c) Sunil Nimmagadda +.\" +.\" 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. +.\" +.Dd $Mdocdate: March 26 2014 $ +.Dt POP3D 8 +.Os +.Sh NAME +.Nm pop3d +.Nd Post Office Protocol (POP3) daemon. +.Sh SYNOPSIS +.Nm +.Op Fl d +.Op Fl p Ar path +.Op Fl t Ar type +.Sh DESCRIPTION +.Nm +daemon implements Post Office Protocol (Version 3) as specified in +RFC 1939 as well as POP3S and STARTTLS extensions. +.Pp +.Nm +binds to 110(POP3), 995(POP3S) ports and operates on local mailboxes on +behalf of its remote users. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl d +Do not daemonize. If this option is specified, +.Nm +will run in foreground and log to +.Em stderr . +.It Fl p +Path to the maildrop. Defaults to /var/mail/%u in case of mbox and +~/Maildir in case of maildir. +.Nm +expands '~' to user's home dir +and '%u' to user's name if specified in the path. +.It Fl t +Specify maildrop type. Options are mbox and maildir. Defaults to mbox. +.El +.Sh FILES +.Bl -tag -width "/etc/ssl/private/server.key" -compact +.It Pa ~/maildir +.It Pa /var/mail/%u +User maildrops +.Pp +.It /etc/ssl/server.crt +.It /etc/ssl/private/server.key +Location of SSL certificate and key +.Sh SEE ALSO +.Xr smtpd 8 , +.Xr ssl 8 +.Sh STANDARDS +.Rs +.%A J. Myers +.%A M. Rose +.%D May 1996 +.%R RFC 1939 +.%T Post Office Protocol \(en Version 3 +.Re +.Pp +.Rs +.%A C. Newman +.%D June 1999 +.%R RFC 2595 +.%T Using TLS with IMAP, POP3 and ACAP +.Re +.Pp +.Rs +.%A A. Melnikov +.%A C. Newman +.%A M. Yevstifeyev +.%D August 2011 +.%R draft-melnikov-pop3-over-tls-02 +.Sh CAVEATS +POP3 authenticates using cleartext passwords on 110(POP3) port. diff --git a/pop3d.c b/pop3d.c new file mode 100644 index 0000000..56c8047 --- /dev/null +++ b/pop3d.c @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imsgev.h" +#include "pop3d.h" + +#define MBOX_PATH "/var/mail/%u" +#define MAILDIR_PATH "~/Maildir" +#define POP3D_USER "_pop3d" + +static void authenticate(struct imsgev *, struct imsg *); +static void pop3e_imsgev(struct imsgev *, int , struct imsg *); +static void needfd(struct imsgev *); +static void sig_handler(int, short, void *); +static enum m_type m_type(const char *); +static void usage(void); + +static struct imsgev iev_pop3e; +static pid_t pop3e_pid; +static const char *mpath = MBOX_PATH; +static int mtype = M_MBOX; + +int +main(int argc, char *argv[]) +{ + struct passwd *pw; + struct event ev_sigint, ev_sigterm, ev_sighup, ev_sigchld; + const char *mtype_str = "mbox"; + int ch, d = 0, pair[2]; + + while ((ch = getopt(argc, argv, "dp:t:")) != -1) { + switch (ch) { + case 'd': + d = 1; + break; + case 'p': + mpath = optarg; + break; + case 't': + if ((mtype = m_type(optarg)) == -1) + errx(1, "%s invalid argument", optarg); + if (mtype == M_MAILDIR) + mpath = MAILDIR_PATH; + mtype_str = optarg; + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + if (argc > 0 || *argv) + usage(); + + log_init(d); + if (geteuid()) + fatalx("need root privileges"); + + if (!d && daemon(1, 0) == -1) + fatal("failed to daemonize"); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) == -1) + fatal("socketpair"); + + set_nonblocking(pair[0]); + set_nonblocking(pair[1]); + if ((pw = getpwnam(POP3D_USER)) == NULL) + fatalx("main: getpwnam " POP3D_USER); + + pop3e_pid = pop3_main(pair, pw); + close(pair[1]); + setproctitle("[priv]"); + logit(LOG_INFO, "pop3d ready; type:%s, path:%s", mtype_str, mpath); + event_init(); + signal_set(&ev_sigint, SIGINT, sig_handler, NULL); + signal_set(&ev_sighup, SIGHUP, sig_handler, NULL); + signal_set(&ev_sigterm, SIGTERM, sig_handler, NULL); + signal_set(&ev_sigchld, SIGCHLD, sig_handler, NULL); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sighup, NULL); + signal_add(&ev_sigterm, NULL); + signal_add(&ev_sigchld, NULL); + imsgev_init(&iev_pop3e, pair[0], NULL, pop3e_imsgev, needfd); + if (event_dispatch() < 0) + fatal("event_dispatch"); + + logit(LOG_INFO, "pop3d exiting"); + return (0); +} + +static void +pop3e_imsgev(struct imsgev *iev, int code, struct imsg *imsg) +{ + switch (code) { + case IMSGEV_IMSG: + switch (imsg->hdr.type) { + case IMSG_AUTH: + authenticate(iev, 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("pop3d: imsgev read/write error"); + break; + case IMSGEV_DONE: + event_loopexit(NULL); + break; + } +} + +static void +authenticate(struct imsgev *iev, struct imsg *imsg) +{ + struct auth_req *req = imsg->data; + struct passwd *pw; + int pair[2]; + + if (auth_userokay(req->user, NULL, "auth-pop3", req->pass) == 0) { + logit(LOG_INFO, "%u: auth [%s] failed", + imsg->hdr.peerid, req->user); + pair[0] = -1; + goto end; + } + + logit(LOG_INFO, "%u: auth [%s] passed", imsg->hdr.peerid, + req->user); + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) == -1) + fatal("socketpair"); + + set_nonblocking(pair[0]); + set_nonblocking(pair[1]); + if ((pw = getpwnam(req->user)) == NULL) + fatalx("authenticate: getpwnam"); + + maildrop_init(imsg->hdr.peerid, pair, pw, mtype, mpath); + close(pair[1]); + +end: + imsgev_xcompose(iev, IMSG_AUTH, imsg->hdr.peerid, 0, + pair[0], NULL, 0, "authenticate"); +} + +static void +needfd(struct imsgev *iev) +{ + fatalx("pop3d should never need an fd"); +} + +static void +sig_handler(int sig, short event, void *arg) +{ + int status; + + switch (sig) { + case SIGINT: + case SIGHUP: + case SIGTERM: + imsgev_clear(&iev_pop3e); + imsgev_close(&iev_pop3e); + event_loopexit(NULL); + break; + case SIGCHLD: + if (waitpid(pop3e_pid, &status, WNOHANG) > 0) + if (WIFEXITED(status) || WIFSIGNALED(status)) { + logit(LOG_ERR, "Lost pop3 engine"); + event_loopexit(NULL); + } + break; + } +} + +static enum m_type +m_type(const char *str) +{ + if (strcasecmp(str, "mbox") == 0) + return M_MBOX; + + if (strcasecmp(str, "maildir") == 0) + return M_MAILDIR; + + return (-1); +} + +static void +usage(void) +{ + extern char *__progname; + + fprintf(stderr, "usage: %s [-d] [-p path] [-t type]\n", __progname); + exit(EXIT_FAILURE); +} + diff --git a/pop3d.h b/pop3d.h new file mode 100644 index 0000000..3035795 --- /dev/null +++ b/pop3d.h @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 + +#include + +#include "imsgev.h" +#include "iobuf.h" +#include "ioev.h" + +#define ARGLEN 40 +#define POP3S 0x01 +#define F_DELE 0x01 + +struct passwd; + +enum imsg_type { + IMSG_AUTH, + IMSG_MAILDROP_INIT, + IMSG_MAILDROP_RETR, + IMSG_MAILDROP_DELE, + IMSG_MAILDROP_RSET, + IMSG_MAILDROP_LIST, + IMSG_MAILDROP_LISTALL, + IMSG_MAILDROP_UIDLALL, + IMSG_MAILDROP_UPDATE +}; + +enum m_type { + M_MBOX, + M_MAILDIR +}; + +struct msg { + union { + SIMPLEQ_ENTRY(msg) q_entry; + RB_ENTRY(msg) t_entry; + } e; + char hash[SHA1_DIGEST_STRING_LENGTH]; + size_t sz; + size_t nlines; + union { + long offset; + const char *fname; + } u; + int flags; +}; + +struct mdrop { + union { + SIMPLEQ_HEAD(, msg) q_msgs; + RB_HEAD(msgtree, msg) t_msgs; + } e; + size_t nmsgs; + size_t sz; + struct msg **msgs_index; /* random access to msgs */ + int fd; +}; + +struct stats { + size_t nmsgs; + size_t sz; +}; + +struct retr_req { + unsigned int idx; + unsigned int ntop; + int top; +}; + +struct retr_res { + size_t nlines; + long offset; + unsigned int ntop; + int top; +}; + +struct list_req { + unsigned int idx; + int uidl; +}; + +struct list_res { + unsigned int idx; + union { + size_t sz; + char hash[SHA1_DIGEST_STRING_LENGTH]; + } u; + int uidl; +}; + +struct m_backend { + int (*init)(struct mdrop *, size_t *, size_t *); + int (*retr)(struct mdrop *, unsigned int, size_t *, size_t *); + int (*update)(struct mdrop *); +}; + +struct auth_req { + char user[ARGLEN]; + char pass[ARGLEN]; +}; + +struct listener { + struct sockaddr_storage ss; + struct event ev; + struct event pause; + int flags; + int sock; +}; + +enum state { + AUTH, + TRANSACTION, + UPDATE +}; + +struct session { + SPLAY_ENTRY(session) entry; + struct imsgev iev_maildrop; + struct iobuf iobuf; + struct io io; + char user[ARGLEN]; + char pass[ARGLEN]; + size_t m_sz; + size_t nmsgs; + struct listener *l; + uint32_t id; + int sock; + int flags; + enum state state; +}; + +/* pop3e.c */ +pid_t pop3_main(int [2], struct passwd *); + +/* session.c */ +void session_init(struct listener *, int); +void session_close(struct session *, int); +void session_reply(struct session *, char *, ...); +void session_set_state(struct session *, enum state); +void session_imsgev_init(struct session *, int); +SPLAY_HEAD(session_tree, session); +int session_cmp(struct session *, struct session *); +SPLAY_PROTOTYPE(session_tree, session, entry, session_cmp); + +/* maildrop.c */ +void maildrop_init(uint32_t, int [2], struct passwd *, + int, const char *); + +/* util.c */ +void set_nonblocking(int); +void log_init(int); +void logit(int, const char *, ...); +void vlog(int, const char *, va_list); +void fatal(const char *); +void fatalx(const char *); +void *xcalloc(size_t, size_t, const char *); +void iobuf_xfqueue(struct iobuf *, const char *, const char *, ...); +void iobuf_xqueue(struct iobuf *, const char *, const void *, size_t); +int imsgev_xcompose(struct imsgev *, u_int16_t, u_int32_t, + uint32_t, int, void *, u_int16_t, const char *); +int get_index(struct session *, const char *, unsigned int *); +void log_connect(uint32_t, struct sockaddr_storage *, socklen_t); diff --git a/pop3e.c b/pop3e.c new file mode 100644 index 0000000..cb6f017 --- /dev/null +++ b/pop3e.c @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imsgev.h" +#include "pop3d.h" +#include "ssl.h" + +#define BACKLOG 5 + +static void auth_response(struct session *, int); +static void pop3_accept(int, short, void *); +static void pop3_listen(const char *); +static void pop3_pause(int, short, void *); +static void pop3d_imsgev(struct imsgev *, int, struct imsg *); +static void needfd(struct imsgev *); +static void sig_handler(int, short, void *); + +struct imsgev iev_pop3d; +void *ssl_ctx; + +pid_t +pop3_main(int pair[2], struct passwd *pw) +{ + extern struct session_tree sessions; + struct event ev_sigint, ev_sigterm; + pid_t pid; + + pid = fork(); + if (pid < 0) + fatal("pop3e: fork"); + + if (pid > 0) + return (pid); + + close(pair[0]); + setproctitle("pop3 engine"); + SPLAY_INIT(&sessions); + event_init(); + signal_set(&ev_sigint, SIGINT, sig_handler, NULL); + signal_set(&ev_sigterm, SIGTERM, sig_handler, NULL); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + imsgev_init(&iev_pop3d, pair[1], NULL, pop3d_imsgev, needfd); + pop3_listen("pop3"); + + ssl_init(); + if ((ssl_ctx = ssl_setup()) == NULL) + fatal("ssl_setup failed"); + pop3_listen("pop3s"); + + if (chroot(pw->pw_dir) == -1 || chdir("/") == -1) + fatal("chroot"); + + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + fatal("cannot drop privileges"); + + if (event_dispatch() < 0) + fatal("event_dispatch"); + + logit(LOG_INFO, "pop3 engine exiting"); + _exit(0); +} + +static void +pop3_listen(const char *port) +{ + struct listener *l = NULL; + struct addrinfo hints, *res, *res0; + int error, opt, serrno, s; + const char *cause = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + error = getaddrinfo(NULL, port, &hints, &res0); + if (error) + errx(1, "%s", gai_strerror(error)); + + for (res = res0; res != NULL; res = res->ai_next) { + s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (s == -1) { + cause = "socket"; + continue; + } + + opt = 1; + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + &opt, sizeof(opt)) == -1) + fatal("listener setsockopt(SO_REUSEADDR)"); + + if (bind(s, res->ai_addr, res->ai_addrlen) == -1) { + serrno = errno; + cause = "bind"; + close(s); + errno = serrno; + continue; + } + + set_nonblocking(s); + if (listen(s, BACKLOG) == -1) + fatal("listen"); + + l = xcalloc(1, sizeof(*l), "pop3_listen"); + l->sock = s; + if (strcmp(port, "pop3s") == 0) + l->flags |= POP3S; + + event_set(&l->ev, s, EV_READ|EV_PERSIST, pop3_accept, l); + event_add(&l->ev, NULL); + evtimer_set(&l->pause, pop3_pause, l); + } + + if (l == NULL) + errx(1, "%s", cause); + + freeaddrinfo(res0); +} + +static void +pop3_accept(int fd, short events, void *arg) +{ + struct sockaddr_storage ss; + struct listener *l = arg; + struct timeval timeout = {1, 0}; + socklen_t len; + int s; + + len = sizeof(ss); + s = accept(fd, (struct sockaddr *)&ss, &len); + if (s == -1) { + switch (errno) { + case EINTR: + case EWOULDBLOCK: + case ECONNABORTED: + return; + case EMFILE: + case ENFILE: + event_del(&l->ev); + evtimer_add(&l->pause, &timeout); + return; + default: + fatalx("accept"); + } + } + + set_nonblocking(s); + l->ss = ss; + session_init(l, s); +} + +static void +pop3_pause(int fd, short events, void *arg) +{ + struct listener *l = arg; + + event_add(&l->ev, NULL); +} + +static void +pop3d_imsgev(struct imsgev *iev, int code, struct imsg *imsg) +{ + extern struct session_tree sessions; + struct session key, *r; + + 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("pop3e: session lost"); + } + switch (imsg->hdr.type) { + case IMSG_AUTH: + auth_response(r, imsg->fd); + break; + default: + logit(LOG_DEBUG, "%s: unexpected imsg %d", + __func__, imsg->hdr.type); + break; + } + break; + case IMSGEV_EREAD: + case IMSGEV_EWRITE: + case IMSGEV_EIMSG: + fatal("pop3e: imsgev read/write error"); + break; + case IMSGEV_DONE: + event_loopexit(NULL); + break; + } +} + +static void +auth_response(struct session *s, int fd) +{ + if (fd == -1) { + session_reply(s, "%s", "-ERR auth failed"); + io_set_write(&s->io); + session_close(s, 1); + return; + } + + session_imsgev_init(s, fd); +} + +static void +needfd(struct imsgev *iev) +{ + /* XXX can anything be done to handle fd exhaustion? */ + fatalx("pop3e needs an fd"); +} + +static void +sig_handler(int sig, short event, void *arg) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + event_loopexit(NULL); + } +} + diff --git a/session.c b/session.c new file mode 100644 index 0000000..77402a3 --- /dev/null +++ b/session.c @@ -0,0 +1,754 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#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); + diff --git a/ssl.c b/ssl.c new file mode 100644 index 0000000..fce300f --- /dev/null +++ b/ssl.c @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2013 Sunil Nimmagadda + * Copyright (c) 2006 Pierre-Yves Ritschard + * + * 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 +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "pop3d.h" +#include "ssl.h" + +#define SSL_CIPHERS "HIGH" +#define SSL_SESSION_TIMEOUT 300 +#define CERTFILE "/etc/ssl/server.crt" +#define KEYFILE "/etc/ssl/private/server.key" + +static char *ssl_load_file(const char *, off_t *); + +void +ssl_init(void) +{ + /* SSL init */ + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + + /* Init hardware cryto engines. */ + ENGINE_load_builtin_engines(); + ENGINE_register_all_complete(); +} + +void * +ssl_setup(void) +{ + SSL_CTX *ctx = NULL; + char *cert, *key; + off_t cert_len, key_len; + + /* SSL context creation */ + ctx = SSL_CTX_new(SSLv23_server_method()); + if (ctx == NULL) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not create SSL context"); + } + + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + SSL_CTX_set_timeout(ctx, SSL_SESSION_TIMEOUT); + SSL_CTX_set_options(ctx, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_TICKET); + SSL_CTX_set_options(ctx, + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + /* SSL certificate, key loading */ + cert = ssl_load_file(CERTFILE, &cert_len); + if (cert == NULL) + fatal("ssl_load_file: Unable to load " CERTFILE); + + key = ssl_load_file(KEYFILE, &key_len); + if (key == NULL) + fatal("ssl_load_file: Unable to load " KEYFILE); + + if (!SSL_CTX_set_cipher_list(ctx, SSL_CIPHERS)) + goto err; + + if (!ssl_ctx_use_certificate_chain(ctx, cert, cert_len)) + goto err; + + else if (!ssl_ctx_use_private_key(ctx, key, key_len)) + goto err; + + else if (!SSL_CTX_check_private_key(ctx)) + goto err; + + return (ctx); + +err: + if (ctx != NULL) + SSL_CTX_free(ctx); + ssl_error("ssl_setup"); + fatal("ssl_setup: cannot set SSL up"); + return (NULL); +} + +void * +pop3s_init(SSL_CTX *ctx, int fd) +{ + SSL *ssl; + + if ((ssl = SSL_new(ctx)) == NULL) + fatal("SSL_new"); + + if (SSL_set_fd(ssl, fd) == 0) + fatal("SSL_set_fd"); + + return (ssl); +} + +static char * +ssl_load_file(const char *name, off_t *len) +{ + struct stat st; + off_t size; + char *buf = NULL; + int fd; + + if ((fd = open(name, O_RDONLY)) == -1) + return (NULL); + + if (fstat(fd, &st) != 0) + goto fail; + + size = st.st_size; + if ((buf = calloc(1, size + 1)) == NULL) + goto fail; + if (read(fd, buf, size) != size) + goto fail; + + close(fd); + + *len = size; + return (buf); + +fail: + if (buf != NULL) + free(buf); + + close(fd); + return (NULL); +} + +void +ssl_error(const char *where) +{ + unsigned long code; + char errbuf[128]; + extern int debug; + + if (!debug) + return; + + for (; (code = ERR_get_error()) != 0 ;) { + ERR_error_string_n(code, errbuf, sizeof(errbuf)); + logit(LOG_DEBUG, "SSL library error: %s: %s", where, errbuf); + } +} + diff --git a/ssl.h b/ssl.h new file mode 100644 index 0000000..b52d626 --- /dev/null +++ b/ssl.h @@ -0,0 +1,11 @@ +#include + +/* ssl.c */ +void ssl_init(void); +void *ssl_setup(void); +void *pop3s_init(SSL_CTX *, int); +void ssl_error(const char *); + +/* ssl_privsep.c */ +int ssl_ctx_use_private_key(SSL_CTX *, char *, off_t); +int ssl_ctx_use_certificate_chain(SSL_CTX *, char *, off_t); diff --git a/ssl_privsep.c b/ssl_privsep.c new file mode 100644 index 0000000..e996769 --- /dev/null +++ b/ssl_privsep.c @@ -0,0 +1,253 @@ +/* $OpenBSD: ssl_privsep.c,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +/* + * SSL operations needed when running in a privilege separated environment. + * Adapted from openssl's ssl_rsa.c by Pierre-Yves Ritschard . + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +int ssl_ctx_use_private_key(SSL_CTX *, char *, off_t); +int ssl_ctx_use_certificate_chain(SSL_CTX *, char *, off_t); +int ssl_ctx_load_verify_memory(SSL_CTX *, char *, off_t); +int ssl_by_mem_ctrl(X509_LOOKUP *, int, const char *, long, char **); + +X509_LOOKUP_METHOD x509_mem_lookup = { + "Load cert from memory", + NULL, /* new */ + NULL, /* free */ + NULL, /* init */ + NULL, /* shutdown */ + ssl_by_mem_ctrl, /* ctrl */ + NULL, /* get_by_subject */ + NULL, /* get_by_issuer_serial */ + NULL, /* get_by_fingerprint */ + NULL, /* get_by_alias */ +}; + +#define X509_L_ADD_MEM 3 + +int +ssl_ctx_use_private_key(SSL_CTX *ctx, char *buf, off_t len) +{ + int ret; + BIO *in; + EVP_PKEY *pkey; + + ret = 0; + + if ((in = BIO_new_mem_buf(buf, len)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB); + return 0; + } + + pkey = PEM_read_bio_PrivateKey(in, NULL, + ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata); + + if (pkey == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_PEM_LIB); + goto end; + } + ret = SSL_CTX_use_PrivateKey(ctx, pkey); + EVP_PKEY_free(pkey); +end: + if (in != NULL) + BIO_free(in); + return ret; +} + + +int +ssl_ctx_use_certificate_chain(SSL_CTX *ctx, char *buf, off_t len) +{ + int ret; + BIO *in; + X509 *x; + X509 *ca; + unsigned long err; + + ret = 0; + x = ca = NULL; + + if ((in = BIO_new_mem_buf(buf, len)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_BUF_LIB); + goto end; + } + + if ((x = PEM_read_bio_X509(in, NULL, + ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_PEM_LIB); + goto end; + } + + if (!SSL_CTX_use_certificate(ctx, x) || ERR_peek_error() != 0) + goto end; + + /* If we could set up our certificate, now proceed to + * the CA certificates. + */ + + if (ctx->extra_certs != NULL) { + sk_X509_pop_free(ctx->extra_certs, X509_free); + ctx->extra_certs = NULL; + } + + while ((ca = PEM_read_bio_X509(in, NULL, + ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata)) != NULL) { + + if (!SSL_CTX_add_extra_chain_cert(ctx, ca)) + goto end; + } + + err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_PEM && + ERR_GET_REASON(err) == PEM_R_NO_START_LINE) + ERR_clear_error(); + else + goto end; + + ret = 1; +end: + if (ca != NULL) + X509_free(ca); + if (x != NULL) + X509_free(x); + if (in != NULL) + BIO_free(in); + return (ret); +} + +int +ssl_ctx_load_verify_memory(SSL_CTX *ctx, char *buf, off_t len) +{ + X509_LOOKUP *lu; + struct iovec iov; + + if ((lu = X509_STORE_add_lookup(ctx->cert_store, + &x509_mem_lookup)) == NULL) + return (0); + + iov.iov_base = buf; + iov.iov_len = len; + + if (!ssl_by_mem_ctrl(lu, X509_L_ADD_MEM, + (const char *)&iov, X509_FILETYPE_PEM, NULL)) + return (0); + + return (1); +} + +int +ssl_by_mem_ctrl(X509_LOOKUP *lu, int cmd, const char *buf, + long type, char **ret) +{ + STACK_OF(X509_INFO) *inf; + const struct iovec *iov; + X509_INFO *itmp; + BIO *in = NULL; + int i, count = 0; + + iov = (const struct iovec *)buf; + + if (type != X509_FILETYPE_PEM) + goto done; + + if ((in = BIO_new_mem_buf(iov->iov_base, iov->iov_len)) == NULL) + goto done; + + if ((inf = PEM_X509_INFO_read_bio(in, NULL, NULL, NULL)) == NULL) + goto done; + + for (i = 0; i < sk_X509_INFO_num(inf); i++) { + itmp = sk_X509_INFO_value(inf, i); + if (itmp->x509) { + X509_STORE_add_cert(lu->store_ctx, itmp->x509); + count++; + } + if (itmp->crl) { + X509_STORE_add_crl(lu->store_ctx, itmp->crl); + count++; + } + } + sk_X509_INFO_pop_free(inf, X509_INFO_free); + + done: + if (!count) + X509err(X509_F_X509_LOAD_CERT_CRL_FILE,ERR_R_PEM_LIB); + + if (in != NULL) + BIO_free(in); + return (count); +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..4a95a1d --- /dev/null +++ b/util.c @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pop3d.h" + +int debug = 0; + +void +set_nonblocking(int fd) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL, 0)) == -1) + fatal("fcntl F_GETFL"); + + flags |= O_NONBLOCK; + if ((flags = fcntl(fd, F_SETFL, flags)) == -1) + fatal("fcntl F_SETFL"); +} + +void +log_init(int n_debug) +{ + extern char *__progname; + + debug = n_debug; + if (!debug) + openlog(__progname, LOG_PID | LOG_NDELAY, LOG_DAEMON); + + tzset(); +} + +void +fatal(const char *emsg) +{ + if (errno) + logit(LOG_CRIT, "fatal: %s: %s\n", emsg, strerror(errno)); + else + logit(LOG_CRIT, "fatal: %s\n", emsg); + + exit(EXIT_FAILURE); +} + +void +fatalx(const char *emsg) +{ + errno = 0; + fatal(emsg); +} + +void +logit(int pri, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vlog(pri, fmt, ap); + va_end(ap); +} + +void +vlog(int pri, const char *fmt, va_list ap) +{ + char *nfmt; + + if (debug) { + /* best effort in out of mem situations */ + if (asprintf(&nfmt, "%s\n", fmt) == -1) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } else { + vfprintf(stderr, nfmt, ap); + free(nfmt); + } + fflush(stderr); + } else + vsyslog(pri, fmt, ap); +} + +void * +xcalloc(size_t nmemb, size_t size, const char *where) +{ + void *r; + + if ((r = calloc(nmemb, size)) == NULL) { + logit(LOG_CRIT, "%s: calloc(%zu, %zu)", where, nmemb, size); + err(1, "exiting"); + } + + return (r); +} + +int +imsgev_xcompose(struct imsgev *iev, u_int16_t type, u_int32_t peerid, + uint32_t pid, int fd, void *data, u_int16_t datalen, const char *where) +{ + int r; + r = imsgev_compose(iev, type, peerid, pid, fd, data, datalen); + if (r == -1) { + logit(LOG_CRIT, "imsgev_xcompose: %s", where); + errx(1, "maildrop exiting"); + } + + return (r); +} + +void +iobuf_xfqueue(struct iobuf *io, const char *where, const char *fmt, ...) +{ + va_list ap; + int len; + + va_start(ap, fmt); + len = iobuf_vfqueue(io, fmt, ap); + va_end(ap); + + if (len == -1) + errx(1, "%s: iobuf_xfqueue(%p, %s, ...)", where, io, fmt); +} + +void +iobuf_xqueue(struct iobuf *io, const char *where, const void *data, size_t len) +{ + if (iobuf_queue(io, data, len) == -1) + errx(1, "%s: iobuf_xqueue(%p, data, %zu)", where, io, len); +} + +int +get_index(struct session *s, const char *args, unsigned int *idx) +{ + const char *errstr; + + *idx = strtonum(args, 1, UINT_MAX, &errstr); + if (errstr || *idx < 1 || *idx > s->nmsgs) { + logit(LOG_INFO, "%zu: Invalid index", s->id); + session_reply(s, "%s", "-ERR invalid index"); + return (0); + } + + *idx -= 1; /* make it zero based */ + return (1); +} + +void +log_connect(uint32_t id, struct sockaddr_storage *s, socklen_t s_len) +{ + char hbuf[NI_MAXHOST]; + int e; + + e = getnameinfo((struct sockaddr *)s, s_len, hbuf, sizeof(hbuf), + NULL, 0, NI_NUMERICHOST); + if (e) { + logit(LOG_DEBUG, "getnameinfo: %s", gai_strerror(e)); + logit(LOG_INFO, "new session with id %u", id); + } else + logit(LOG_INFO, "new session with id %u from %s", id, hbuf); +} -- cgit v1.2.3