diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 14 | ||||
-rw-r--r-- | imsgev.c | 170 | ||||
-rw-r--r-- | imsgev.h | 49 | ||||
-rw-r--r-- | iobuf.c | 467 | ||||
-rw-r--r-- | iobuf.h | 71 | ||||
-rw-r--r-- | ioev.c | 910 | ||||
-rw-r--r-- | ioev.h | 69 | ||||
-rw-r--r-- | maildir.c | 229 | ||||
-rw-r--r-- | maildrop.c | 412 | ||||
-rw-r--r-- | mbox.c | 206 | ||||
-rw-r--r-- | pop3d.8 | 86 | ||||
-rw-r--r-- | pop3d.c | 230 | ||||
-rw-r--r-- | pop3d.h | 177 | ||||
-rw-r--r-- | pop3e.c | 255 | ||||
-rw-r--r-- | session.c | 754 | ||||
-rw-r--r-- | ssl.c | 168 | ||||
-rw-r--r-- | ssl.h | 11 | ||||
-rw-r--r-- | ssl_privsep.c | 253 | ||||
-rw-r--r-- | util.c | 183 |
20 files changed, 4717 insertions, 0 deletions
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 <bsd.prog.mk> 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 <eric@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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 <eric@openbsd.org> + * + * 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 <event.h> +#include <imsg.h> + +#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__ */ @@ -0,0 +1,467 @@ +/* $OpenBSD: iobuf.c,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#ifdef IO_SSL +#include <openssl/err.h> +#include <openssl/ssl.h> +#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 */ @@ -0,0 +1,71 @@ +/* $OpenBSD: iobuf.h,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> + +#include <stdarg.h> + +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 *); @@ -0,0 +1,910 @@ +/* $OpenBSD: ioev.c,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> + +#include "ioev.h" +#include "iobuf.h" + +#ifdef IO_SSL +#include <openssl/err.h> +#include <openssl/ssl.h> +#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:%p fd=%i to=%i fl=%s%s>", + io, io->sock, io->timeout, io_strflags(io->flags), ssl); + else + snprintf(buf, sizeof buf, + "<io:%p fd=%i to=%i fl=%s%s ib=%zu ob=%zu>", + 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, "<NONE>", 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 */ @@ -0,0 +1,69 @@ +/* $OpenBSD: ioev.h,v 1.1 2014/01/27 15:49:52 sunil Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * + * 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 <event.h> + +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 <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/tree.h> +#include <sys/stat.h> +#include <sys/socket.h> + +#include <fcntl.h> +#include <dirent.h> +#include <stdio.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#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 <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#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); +} + @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#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); +} + @@ -0,0 +1,86 @@ +.\" Copyright (c) Sunil Nimmagadda <sunil@nimmagadda.net> +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.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. @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/wait.h> + +#include <bsd_auth.h> +#include <err.h> +#include <event.h> +#include <login_cap.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#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); +} + @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/tree.h> + +#include <sha1.h> + +#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); @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/tree.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#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 <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <syslog.h> +#include <unistd.h> + +#include "imsgev.h" +#include "pop3d.h" +#include "ssl.h" + +#define MAXLINESIZE 2048 +#define TIMEOUT 600000 + +enum pop_command { + CMD_STLS = 0, + CMD_CAPA, + CMD_USER, + CMD_PASS, + CMD_QUIT, + CMD_STAT, + CMD_RETR, + CMD_LIST, + CMD_DELE, + CMD_RSET, + CMD_TOP, + CMD_UIDL, + CMD_NOOP +}; + +enum arg_constraint { + OPTIONAL = 1, + PROHIBITED, + REQUIRED +}; + +static struct {int code; enum arg_constraint c; const char *cmd;} commands[] = { + {CMD_STLS, PROHIBITED, "STLS"}, + {CMD_CAPA, PROHIBITED, "CAPA"}, + {CMD_USER, REQUIRED, "USER"}, + {CMD_PASS, REQUIRED, "PASS"}, + {CMD_QUIT, PROHIBITED, "QUIT"}, + {CMD_STAT, PROHIBITED, "STAT"}, + {CMD_RETR, REQUIRED, "RETR"}, + {CMD_LIST, OPTIONAL, "LIST"}, + {CMD_DELE, REQUIRED, "DELE"}, + {CMD_RSET, PROHIBITED, "RSET"}, + {CMD_TOP, REQUIRED, "TOP"}, + {CMD_UIDL, OPTIONAL, "UIDL"}, + {CMD_NOOP, PROHIBITED, "NOOP"}, + {-1, OPTIONAL, NULL} +}; + +static void auth_request(struct session *); +static void capa(struct session *); +static void command(struct session *, int, char *); +static void session_io(struct io *, int); +static void parse(struct session *, char *); +static void auth_command(struct session *, int, char *); +static void trans_command(struct session *, int, char *); +static void get_list_all(struct session *, int); +static void get_list(struct session *, unsigned int, int); +static void maildrop_imsgev(struct imsgev *, int, struct imsg *); +static void handle_init(struct session *, struct imsg *); +static void handle_retr(struct session *, struct imsg *); +static void handle_dele(struct session *, struct imsg *); +static void handle_list(struct session *, struct imsg *); +static void handle_list_all(struct session *, struct imsg *, int); +static void handle_update(struct session *, struct imsg *); +static void needfd(struct imsgev *); +static void pop3_debug(char *, ...); +static void session_write(struct session *, const char *, size_t); +static const char *strstate(enum state); + +struct session_tree sessions; +static int _pop3_debug = 1; + +void +session_init(struct listener *l, int fd) +{ + struct session *s; + void *ssl; + extern void *ssl_ctx; + + s = xcalloc(1, sizeof(*s), "session_init"); + s->l = l; + if (iobuf_init(&s->iobuf, 0, 0) == -1) + fatal("iobuf_init"); + + io_init(&s->io, fd, s, session_io, &s->iobuf); + io_set_timeout(&s->io, TIMEOUT); + s->id = arc4random(); + s->sock = fd; + s->state = AUTH; + if (s->l->flags & POP3S) { + s->flags |= POP3S; + ssl = pop3s_init(ssl_ctx, s->sock); + io_set_read(&s->io); + io_start_tls(&s->io, ssl); + return; + } + + log_connect(s->id, &l->ss, l->ss.ss_len); + SPLAY_INSERT(session_tree, &sessions, s); + session_reply(s, "%s", "+OK pop3d ready"); + io_set_write(&s->io); +} + +void +session_close(struct session *s, int flush) +{ + struct session *entry; + + entry = SPLAY_REMOVE(session_tree, &sessions, s); + if (entry == NULL) { + /* STARTTLS session was in progress and got interrupted */ + logit(LOG_DEBUG, "%u: not in tree", s->id); + entry = s; + } + + if (flush) { + if (entry->flags & POP3S) + iobuf_flush_ssl(&entry->iobuf, entry->io.ssl); + else + iobuf_flush(&entry->iobuf, entry->io.sock); + } + + iobuf_clear(&entry->iobuf); + io_clear(&entry->io); + imsgev_clear(&entry->iev_maildrop); + imsgev_close(&entry->iev_maildrop); + logit(LOG_INFO, "%u: session closed", entry->id); + free(entry); +} + +static void +session_io(struct io *io, int evt) +{ + struct session *s = io->arg; + char *line; + size_t len; + + pop3_debug("%u: %s", s->id, io_strevent(evt)); + switch (evt) { + case IO_DATAIN: + line = iobuf_getline(&s->iobuf, &len); + if (line == NULL) { + iobuf_normalize(&s->iobuf); + break; + } + if (strncasecmp(line, "PASS", 4) == 0) + pop3_debug(">>> PASS"); + else + pop3_debug(">>> %s", line); + parse(s, line); + break; + case IO_LOWAT: + if (iobuf_queued(&s->iobuf) == 0) + io_set_read(io); + break; + case IO_TLSREADY: + /* greet only for pop3s, STLS already greeted */ + if (s->flags & POP3S) { + log_connect(s->id, &s->l->ss, s->l->ss.ss_len); + session_reply(s, "%s", "+OK pop3 ready"); + io_set_write(&s->io); + } + SPLAY_INSERT(session_tree, &sessions, s); + /* mark STLS session as secure */ + s->flags |= POP3S; + logit(LOG_INFO, "%u: TLS ready", s->id); + break; + case IO_DISCONNECTED: + case IO_TIMEOUT: + case IO_ERROR: + session_close(s, 0); + break; + default: + logit(LOG_DEBUG, "unknown event %s", io_strevent(evt)); + break; + } +} + +static void +parse(struct session *s, char *line) +{ + enum arg_constraint c = OPTIONAL; + int i, cmd = -1; + char *args; + + /* trim newline */ + line[strcspn(line, "\n")] = '\0'; + + args = strchr(line, ' '); + if (args) { + *args++ = '\0'; + while (isspace((unsigned char)*args)) + args++; + } + + for (i = 0; commands[i].code != -1; i++) { + if (strcasecmp(line, commands[i].cmd) == 0) { + cmd = commands[i].code; + c = commands[i].c; + break; + } + } + + if (cmd == -1) { + logit(LOG_INFO, "%u: invalid command %s", s->id, line); + session_reply(s, "%s", "-ERR invalid command"); + io_set_write(&s->io); + return; + } + + if (c == PROHIBITED && args) { + session_reply(s, "%s", "-ERR no arguments allowed"); + io_set_write(&s->io); + return; + } else if ((c == REQUIRED) && + (args == NULL || strlen(args) >= ARGLEN)) { + session_reply(s, "%s", "-ERR args required or too long"); + io_set_write(&s->io); + return; + } + + command(s, cmd, args); +} + +static void +command(struct session *s, int cmd, char *args) +{ + switch (s->state) { + case AUTH: + auth_command(s, cmd, args); + break; + case TRANSACTION: + trans_command(s, cmd, args); + break; + case UPDATE: + session_reply(s, "%s", "-ERR commands not allowed"); + io_set_write(&s->io); + break; + default: + fatalx("Invalid state"); + } +} + +static void +auth_command(struct session *s, int cmd, char *args) +{ + extern void *ssl_ctx; + void *ssl; + + switch (cmd) { + case CMD_STLS: + if (s->flags & POP3S) { + session_reply(s, "%s", "-ERR already secured"); + break; + } + session_reply(s, "%s", "+OK"); + io_set_write(&s->io); + iobuf_flush(&s->iobuf, s->io.sock); + /* add back when IO_TLSREADY. */ + SPLAY_REMOVE(session_tree, &sessions, s); + ssl = pop3s_init(ssl_ctx, s->sock); + io_set_read(&s->io); + io_start_tls(&s->io, ssl); + return; + case CMD_CAPA: + capa(s); + break; + case CMD_USER: + strlcpy(s->user, args, sizeof(s->user)); + session_reply(s, "%s", "+OK"); + break; + case CMD_PASS: + if (s->user[0] == '\0') { + session_reply(s, "%s", "-ERR no USER specified"); + break; + } + strlcpy(s->pass, args, sizeof(s->pass)); + auth_request(s); + return; + case CMD_QUIT: + session_reply(s, "%s", "+OK"); + io_set_write(&s->io); + session_close(s, 1); + return; + default: + session_reply(s, "%s", "-ERR invalid command"); + break; + } + + io_set_write(&s->io); +} + +static void +auth_request(struct session *s) +{ + extern struct imsgev iev_pop3d; + struct auth_req req; + + memset(&req, 0, sizeof(req)); + strlcpy(req.user, s->user, sizeof(req.user)); + strlcpy(req.pass, s->pass, sizeof(req.pass)); + imsgev_xcompose(&iev_pop3d, IMSG_AUTH, s->id, 0, -1, + &req, sizeof(req), "auth_request"); +} + +static void +capa(struct session *s) +{ + session_reply(s, "%s", "+OK"); + session_reply(s, "%s", "STLS"); + session_reply(s, "%s", "USER"); + session_reply(s, "%s", "TOP"); + session_reply(s, "%s", "UIDL"); + session_reply(s, "%s", "IMPLEMENTATION pop3d"); + session_reply(s, "%s", "."); +} + +static void +trans_command(struct session *s, int cmd, char *args) +{ + struct retr_req retr_req; + unsigned int idx, n; + char *c; + const char *errstr; + int uidl = 0; + + memset(&retr_req, 0, sizeof(retr_req)); + switch (cmd) { + case CMD_CAPA: + capa(s); + break; + case CMD_STAT: + session_reply(s, "%s %zu %zu", "+OK", s->nmsgs, s->m_sz); + break; + case CMD_TOP: + if ((c = strchr(args, ' ')) == NULL) { + session_reply(s, "%s", "-ERR invalid arguments"); + break; + } + *c++ = '\0'; + n = strtonum(c, 0, UINT_MAX, &errstr); + if (errstr) { + session_reply(s, "%s", "-ERR invalid n"); + break; + } + retr_req.top = 1; + retr_req.ntop = n; + /* FALLTRHROUGH */ + case CMD_RETR: + if (!get_index(s, args, &retr_req.idx)) + break; + imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_RETR, + s->id, 0, -1, &retr_req, sizeof(retr_req), "trans_command"); + return; + case CMD_NOOP: + session_reply(s, "%s", "+OK"); + break; + case CMD_DELE: + if (!get_index(s, args, &idx)) + break; + imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_DELE, + s->id, 0, -1, &idx, sizeof(idx), "trans_command"); + return; + case CMD_RSET: + imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_RSET, + s->id, 0, -1, NULL, 0, "trans_command"); + return; + case CMD_UIDL: + uidl = 1; + /* FALLTHROUGH */ + case CMD_LIST: + if (args) { + if (!get_index(s, args, &idx)) + break; + get_list(s, idx, uidl); + } else + get_list_all(s, uidl); + return; + case CMD_QUIT: + imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_UPDATE, + s->id, 0, -1, NULL, 0, "trans_command"); + session_set_state(s, UPDATE); + return; + default: + session_reply(s, "%s", "-ERR invalid command"); + break; + } + + io_set_write(&s->io); +} + +static void +get_list_all(struct session *s, int uidl) +{ + imsgev_xcompose(&s->iev_maildrop, + (uidl) ? IMSG_MAILDROP_UIDLALL : IMSG_MAILDROP_LISTALL, + s->id, 0, -1, NULL, 0, "list_all"); +} + +static void +get_list(struct session *s, unsigned int i, int uidl) +{ + struct list_req req; + + req.idx = i; + req.uidl = uidl; + imsgev_xcompose(&s->iev_maildrop, IMSG_MAILDROP_LIST, + s->id, 0, -1, &req, sizeof(req), "list"); +} + +void +session_imsgev_init(struct session *s, int fd) +{ + imsgev_init(&s->iev_maildrop, fd, s, maildrop_imsgev, needfd); +} + +static void +maildrop_imsgev(struct imsgev *iev, int code, struct imsg *imsg) +{ + struct session key, *r; + int uidl = 0; + + switch (code) { + case IMSGEV_IMSG: + key.id = imsg->hdr.peerid; + r = SPLAY_FIND(session_tree, &sessions, &key); + if (r == NULL) { + logit(LOG_INFO, "%u: session not found", key.id); + fatalx("session: session lost"); + } + switch (imsg->hdr.type) { + case IMSG_MAILDROP_INIT: + handle_init(r, imsg); + break; + case IMSG_MAILDROP_RETR: + handle_retr(r, imsg); + break; + case IMSG_MAILDROP_DELE: + handle_dele(r, imsg); + break; + case IMSG_MAILDROP_RSET: + session_reply(r, "%s", "+OK reset"); + io_set_write(&r->io); + break; + case IMSG_MAILDROP_LIST: + handle_list(r, imsg); + break; + case IMSG_MAILDROP_UIDLALL: + uidl = 1; + /* FALLTHROUGH */ + case IMSG_MAILDROP_LISTALL: + handle_list_all(r, imsg, uidl); + break; + case IMSG_MAILDROP_UPDATE: + handle_update(r, imsg); + break; + default: + logit(LOG_DEBUG, "%s: unexpected imsg %u", + __func__, imsg->hdr.type); + break; + } + break; + case IMSGEV_EREAD: + case IMSGEV_EWRITE: + case IMSGEV_EIMSG: + fatal("session: imsgev read/write error"); + break; + } +} + +static void +handle_init(struct session *s, struct imsg *imsg) +{ + size_t datalen; + struct stats *stats; + + datalen = imsg->hdr.len - sizeof(imsg->hdr); + if (datalen) { + stats = imsg->data; + s->m_sz = stats->sz; + s->nmsgs = stats->nmsgs; + session_reply(s, "%s", "+OK maildrop ready"); + io_set_write(&s->io); + session_set_state(s, TRANSACTION); + } else { + session_reply(s, "%s", "-ERR maildrop init failed"); + io_set_write(&s->io); + session_close(s, 1); + } +} + +static void +handle_retr(struct session *s, struct imsg *imsg) +{ + struct retr_res *r = imsg->data; + FILE *fp; + char *line; + size_t len; + + if (imsg->fd == -1) { + session_reply(s, "%s", "-ERR marked for delete"); + io_set_write(&s->io); + return; + } + + if ((fp = fdopen(imsg->fd, "r")) == NULL) { + logit(LOG_INFO, "%zu: retr failed", s->id); + session_reply(s, "%s", "-ERR RETR failed"); + io_set_write(&s->io); + session_close(s, 1); + return; + } + + if (fseek(fp, r->offset, SEEK_SET) == -1) + fatal("fseek"); + + session_reply(s, "%s", "+OK"); + /* Ignore "From " line when type is mbox; maildir doesn't have it */ + if ((line = fgetln(fp, &len)) && strncmp(line, "From ", 5)) + session_write(s, line, len); + + if (r->top) { + /* print headers regardless of ntop */ + while ((line = fgetln(fp, &len))) { + session_write(s, line, len); + r->nlines -= 1; + if (strncmp(line , "\n", 1) == 0) + break; + } + + /* print ntop lines of body */ + while ((r->ntop-- > 0) && r->nlines-- && + (line = fgetln(fp, &len))) + session_write(s, line, len); + } else + while (r->nlines-- && (line = fgetln(fp, &len))) + session_write(s, line, len); + + session_reply(s, "%s", "."); + io_set_write(&s->io); + fclose(fp); + close(imsg->fd); +} + +static void +handle_dele(struct session *s, struct imsg *imsg) +{ + int *res = imsg->data; + + if (*res == 0) + session_reply(s, "%s", "+OK marked for delete"); + else + session_reply(s, "%s", "+ERR msg already marked delete"); + + io_set_write(&s->io); +} + +/* DELEted msg's hash and sz will be zero, ignore them */ +static void +handle_list(struct session *s, struct imsg *imsg) +{ + struct list_res *res = imsg->data; + + res->idx += 1; /* POP3 index is 1 based */ + if (res->uidl) { + if (strlen(res->u.hash)) + session_reply(s, "+OK %zu %s", res->idx, res->u.hash); + else + session_reply(s, "-ERR marked for delete"); + } else { + if (res->u.sz) + session_reply(s, "+OK %zu %zu", res->idx, res->u.sz); + else + session_reply(s, "-ERR marked for delete"); + } + + io_set_write(&s->io); +} + +/* DELEted msg's hash and sz will be zero, ignore them */ +static void +handle_list_all(struct session *s, struct imsg *imsg, int uidl) +{ + char *nhash = NULL; + size_t datalen, i, item_sz, j, nitems, *nsz = NULL; + + datalen = imsg->hdr.len - sizeof(imsg->hdr); + item_sz = (uidl) ? SHA1_DIGEST_STRING_LENGTH : sizeof(size_t); + nitems = datalen / item_sz; + if (uidl) + nhash = imsg->data; + else + nsz = imsg->data; + + session_reply(s, "+OK"); + for (i = 0; i < nitems; i++) { + if (uidl) { + j = i * SHA1_DIGEST_STRING_LENGTH; + if (nhash[j]) + session_reply(s, "%zu %s", i + 1, nhash + j); + } else { + if (nsz[i]) + session_reply(s, "%zu %zu", i + 1, nsz[i]); + } + } + + session_reply(s, "."); + io_set_write(&s->io); + +} + +static void +handle_update(struct session *s, struct imsg *imsg) +{ + int *res = imsg->data; + + if (*res == 0) + session_reply(s, "%s", "+OK maildrop updated"); + else + session_reply(s, "%s", "-ERR maildrop update failed"); + + io_set_write(&s->io); + session_close(s, 1); +} + +static void +needfd(struct imsgev *iev) +{ + /* XXX */ + fatalx("session needs an fd"); +} + +int +session_cmp(struct session *a, struct session *b) +{ + if (a->id < b->id) + return (-1); + + if (a->id > b->id) + return (1); + + return (0); +} + +void +session_set_state(struct session *s, enum state newstate) +{ + pop3_debug("%u: %s -> %s", s->id, strstate(s->state), + strstate(newstate)); + s->state = newstate; +} + +#define CASE(x) case x : return #x +static const char * +strstate(enum state state) +{ + static char buf[32]; + + switch (state) { + CASE(AUTH); + CASE(TRANSACTION); + CASE(UPDATE); + default: + snprintf(buf, sizeof(buf), "%d ???", state); + return (buf); + } +} + +void +session_reply(struct session *s, char *fmt, ...) +{ + va_list ap; + int n; + char buf[MAXLINESIZE]; + + va_start(ap, fmt); + n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if (n == -1 || n > MAXLINESIZE) + fatalx("session_reply: response too long"); + + if (buf[0] == '+') + pop3_debug("<<< +OK"); + else if (buf[0] == '-') + pop3_debug("<<< -ERR"); + + iobuf_xfqueue(&s->iobuf, "session_reply", "%s\r\n", buf); +} + +static void +session_write(struct session *s, const char *data, size_t len) +{ + /* remove terminating \n or \r\n if any */ + if (data[len - 1] == '\n') + len -= 1; + + if (data[len - 1] == '\r') + len -= 1; + + /* byte stuff "." if at beginning of line */ + if (data[0] == '.') + iobuf_xfqueue(&s->iobuf, "session_write", "."); + + iobuf_xqueue(&s->iobuf, "session_write", data, len); + /* explicitly terminate with CRLF */ + iobuf_xfqueue(&s->iobuf, "session_write", "\r\n"); +} + +static void +pop3_debug(char *fmt, ...) +{ + va_list ap; + char buf[MAXLINESIZE]; + int n; + + if (!_pop3_debug) + return; + + va_start(ap, fmt); + n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if (n == -1 || n > MAXLINESIZE) + fatalx("pop3_debug: response too long"); + + logit(LOG_DEBUG, "%s", buf); +} + +SPLAY_GENERATE(session_tree, session, entry, session_cmp); + @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2013 Sunil Nimmagadda <sunil@nimmagadda.net> + * Copyright (c) 2006 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/uio.h> +#include <sys/stat.h> + +#include <fcntl.h> +#include <syslog.h> +#include <unistd.h> + +#include <openssl/ssl.h> +#include <openssl/engine.h> +#include <openssl/err.h> + +#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); + } +} + @@ -0,0 +1,11 @@ +#include <openssl/ssl.h> + +/* 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 <sys/types.h> +#include <sys/uio.h> + +#include <unistd.h> +#include <stdio.h> + +#include <openssl/err.h> +#include <openssl/bio.h> +#include <openssl/objects.h> +#include <openssl/evp.h> +#include <openssl/x509.h> +#include <openssl/pem.h> +#include <openssl/ssl.h> + +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); +} @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2014 Sunil Nimmagadda <sunil@nimmagadda.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> + +#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); +} |