diff --git a/extras/filters/smtpfd/Makefile b/extras/filters/smtpfd/Makefile new file mode 100644 index 0000000..eb407d7 --- /dev/null +++ b/extras/filters/smtpfd/Makefile @@ -0,0 +1,29 @@ +# $OpenBSD$ + +PROG= smtpfd + +SRCS+= control.c +SRCS+= engine.c +SRCS+= frontend.c +SRCS+= frontend_smtpf.c +SRCS+= io.c +SRCS+= iobuf.c +SRCS+= log.c +SRCS+= logmsg.c +SRCS+= parse.y +SRCS+= proc.c +SRCS+= resolver.c +SRCS+= smtpfd.c + +MAN= smtpfd.8 smtpfd.conf.5 + +CFLAGS+= -Wall -I${.CURDIR} +CFLAGS+= -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations +CFLAGS+= -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+= -Wsign-compare +YFLAGS= +LDADD+= -levent -lutil +DPADD+= ${LIBEVENT} ${LIBUTIL} + +.include diff --git a/extras/filters/smtpfd/control.c b/extras/filters/smtpfd/control.c new file mode 100644 index 0000000..f322507 --- /dev/null +++ b/extras/filters/smtpfd/control.c @@ -0,0 +1,250 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "smtpfd.h" + +#include "log.h" +#include "proc.h" + +#define CONTROL_BACKLOG 5 + +static void control_init(const char *); +static void control_listen(void); +static void control_pause(void); +static void control_resume(void); +static void control_accept(int, short, void *); +static void control_close(struct imsgproc *); +static void control_dispatch_priv(struct imsgproc *, struct imsg *, void *); +static void control_dispatch_client(struct imsgproc *, struct imsg *, void *); + +static struct { + struct event evt; + int fd; + int pause; +} ctl; + +void +control(int debug, int verbose) +{ + struct passwd *pw; + + /* Early initialisation. */ + log_init(debug, LOG_DAEMON); + log_setverbose(verbose); + log_procinit("control"); + setproctitle("control"); + + control_init(SMTPFD_SOCKET); + + /* Drop priviledges. */ + if ((pw = getpwnam(SMTPFD_USER)) == NULL) + fatalx("unknown user " SMTPFD_USER); + + 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 (chroot(pw->pw_dir) == 1) + fatal("%s: chroot", __func__); + + if (pledge("stdio unix recvfd sendfd", NULL) == -1) + fatal("%s: pledge", __func__); + + event_init(); + + signal(SIGPIPE, SIG_IGN); + + /* Setup imsg socket with parent. */ + p_priv = proc_attach(PROC_PRIV, 3); + if (p_priv == NULL) + fatal("%s: proc_attach", __func__); + proc_setcallback(p_priv, control_dispatch_priv, NULL); + proc_enable(p_priv); + + event_dispatch(); + + exit(0); +} + +static void +control_init(const char *path) +{ + struct sockaddr_un sun; + mode_t old_umask; + int fd; + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd == -1) + fatal("%s: socket", __func__); + + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_UNIX; + strlcpy(sun.sun_path, SMTPFD_SOCKET, sizeof(sun.sun_path)); + + if ((unlink(path) == -1) && (errno != ENOENT)) + fatal("%s: unlink: %s", __func__, path); + + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) + fatal("%s: bind: %s", __func__, path); + umask(old_umask); + + if (chmod(path, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) == -1) + fatal("%s: chmod: %s", __func__, path); + + ctl.fd = fd; +} + +static void +control_listen(void) +{ + if (listen(ctl.fd, CONTROL_BACKLOG) == -1) + fatal("%s: listen", __func__); + + ctl.pause = 0; + control_resume(); +} + +static void +control_pause(void) +{ + struct timeval tv; + + event_del(&ctl.evt); + + tv.tv_sec = 1; + tv.tv_usec = 0; + + evtimer_set(&ctl.evt, control_accept, NULL); + evtimer_add(&ctl.evt, &tv); + ctl.pause = 1; +} + +static void +control_resume(void) +{ + if (ctl.pause) { + evtimer_del(&ctl.evt); + ctl.pause = 0; + } + event_set(&ctl.evt, ctl.fd, EV_READ | EV_PERSIST, control_accept, NULL); + event_add(&ctl.evt, NULL); +} + +static void +control_accept(int fd, short event, void *arg) +{ + struct imsgproc *proc; + int sock; + + if (ctl.pause) { + ctl.pause = 0; + control_resume(); + return; + } + + sock = accept4(ctl.fd, NULL, NULL, SOCK_CLOEXEC | SOCK_NONBLOCK); + if (sock == -1) { + if (errno == ENFILE || errno == EMFILE) + control_pause(); + else if (errno != EWOULDBLOCK && errno != EINTR && + errno != ECONNABORTED) + log_warn("%s: accept4", __func__); + return; + } + + proc = proc_attach(PROC_CLIENT, sock); + if (proc == NULL) { + log_warn("%s: proc_attach", __func__); + close(sock); + return; + } + proc_setcallback(proc, control_dispatch_client, NULL); + proc_enable(proc); +} + +static void +control_close(struct imsgproc *proc) +{ + proc_free(proc); + + if (ctl.pause) + control_resume(); +} + +static void +control_dispatch_priv(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) { + log_debug("%s: imsg connection lost", __func__); + event_loopexit(NULL); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + case IMSG_CONF_START: + m_end(proc); + break; + + case IMSG_CONF_END: + m_end(proc); + control_listen(); + break; + + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} + +static void +control_dispatch_client(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) { + control_close(proc); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + default: + log_debug("%s: error handling imsg %d", __func__, + imsg->hdr.type); + } +} diff --git a/extras/filters/smtpfd/engine.c b/extras/filters/smtpfd/engine.c new file mode 100644 index 0000000..9682ef3 --- /dev/null +++ b/extras/filters/smtpfd/engine.c @@ -0,0 +1,211 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include + +#include "smtpfd.h" + +#include "log.h" +#include "proc.h" + +struct filter_proc { + TAILQ_ENTRY(filter_proc) entry; + struct imsgproc *proc; +}; + +struct filter_node { + TAILQ_ENTRY(filter_node) entry; + struct filter_proc *proc; +}; + +struct filter { + char *name; + TAILQ_ENTRY(filter) entry; + TAILQ_HEAD(, filter_node) nodes; +}; + +struct engine_config { + TAILQ_HEAD(, filter_proc) procs; + TAILQ_HEAD(, filter) filters; +}; + +static void engine_shutdown(void); +static void engine_dispatch_priv(struct imsgproc *, struct imsg *, void *); +static void engine_dispatch_filter(struct imsgproc *, struct imsg *, void *); +static void engine_dispatch_frontend(struct imsgproc *, struct imsg *, void *); + +static struct engine_config *tmpconf, *conf; + +void +engine(int debug, int verbose) +{ + struct passwd *pw; + + /* Early initialisation. */ + log_init(debug, LOG_DAEMON); + log_setverbose(verbose); + log_procinit("engine"); + setproctitle("engine"); + + /* Drop priviledges. */ + if ((pw = getpwnam(SMTPFD_USER)) == NULL) + fatal("%s: getpwnam: %s", __func__, SMTPFD_USER); + + 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("%s: cannot drop privileges", __func__); + + if (pledge("stdio rpath wpath cpath dns sendfd recvfd", NULL) == -1) + fatal("%s: pledge", __func__); + + event_init(); + + signal(SIGPIPE, SIG_IGN); + + /* Setup imsg socket with parent. */ + p_priv = proc_attach(PROC_PRIV, 3); + if (p_priv == NULL) + fatal("%s: proc_attach", __func__); + proc_setcallback(p_priv, engine_dispatch_priv, NULL); + proc_enable(p_priv); + + event_dispatch(); + + engine_shutdown(); +} + +static void +engine_shutdown() +{ + log_debug("exiting"); + + exit(0); +} + +static void +engine_dispatch_priv(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + struct filter_proc *fproc; + + if (imsg == NULL) { + log_debug("%s: imsg connection lost", __func__); + event_loopexit(NULL); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + case IMSG_SOCK_FRONTEND: + m_end(proc); + + if (imsg->fd == -1) + fatalx("failed to receive frontend socket"); + p_frontend = proc_attach(PROC_FRONTEND, imsg->fd); + proc_setcallback(p_frontend, engine_dispatch_frontend, NULL); + proc_enable(p_frontend); + break; + + case IMSG_CONF_START: + m_end(proc); + tmpconf = calloc(1, sizeof(*tmpconf)); + if (tmpconf == NULL) + fatal("%s: calloc", __func__); + TAILQ_INIT(&tmpconf->procs); + TAILQ_INIT(&tmpconf->filters); + break; + + case IMSG_CONF_FILTER_PROC: + if (imsg->fd == -1) + fatalx("%s: filter process fd not received", __func__); + fproc = calloc(1, sizeof(*fproc)); + if (fproc == NULL) + fatal("%s: calloc", __func__); + fproc->proc = proc_attach(PROC_FILTER, imsg->fd); + if (fproc->proc == NULL) + fatal("%s: proc_attach", __func__); + proc_settitle(fproc->proc, imsg->data); + proc_setcallback(fproc->proc, engine_dispatch_filter, fproc); + proc_enable(fproc->proc); + TAILQ_INSERT_TAIL(&tmpconf->procs, fproc, entry); + log_info("new filter process: %s", (char *)imsg->data); + break; + + case IMSG_CONF_END: + m_end(proc); + conf = tmpconf; + break; + + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} + +static void +engine_dispatch_frontend(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) { + log_debug("%s: imsg connection lost", __func__); + event_loopexit(NULL); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + case IMSG_RES_GETADDRINFO: + case IMSG_RES_GETNAMEINFO: + resolver_dispatch_request(proc, imsg); + break; + + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} + +static void +engine_dispatch_filter(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) { + log_debug("%s: imsg connection lost", __func__); + event_loopexit(NULL); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + return; + + switch (imsg->hdr.type) { + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} diff --git a/extras/filters/smtpfd/frontend.c b/extras/filters/smtpfd/frontend.c new file mode 100644 index 0000000..d7b5e60 --- /dev/null +++ b/extras/filters/smtpfd/frontend.c @@ -0,0 +1,331 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include "smtpfd.h" + +#include "log.h" +#include "proc.h" + +static void frontend_shutdown(void); +static void frontend_listen(struct listener *); +static void frontend_pause(struct listener *); +static void frontend_resume(struct listener *); +static void frontend_accept(int, short, void *); +static void frontend_dispatch_priv(struct imsgproc *, struct imsg *, void *); +static void frontend_dispatch_engine(struct imsgproc *, struct imsg *, void *); + +struct conn { + SPLAY_ENTRY(conn) entry; + struct listener *listener; + uint32_t id; + struct sockaddr_storage ss; +}; + +static int conn_cmp(struct conn *, struct conn *); + +SPLAY_HEAD(conntree, conn); +SPLAY_PROTOTYPE(conntree, conn, entry, conn_cmp); + +static struct conntree conns; +static struct smtpfd_conf *tmpconf; + +static int +conn_cmp(struct conn *a, struct conn *b) +{ + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + return (0); +} + +SPLAY_GENERATE(conntree, conn, entry, conn_cmp); + +void +frontend(int debug, int verbose) +{ + struct passwd *pw; + + /* Early initialisation. */ + log_init(debug, LOG_DAEMON); + log_setverbose(verbose); + log_procinit("frontend"); + setproctitle("frontend"); + + SPLAY_INIT(&conns); + frontend_smtpf_init(); + + /* Drop priviledges. */ + if ((pw = getpwnam(SMTPFD_USER)) == NULL) + fatal("%s: getpwnam: %s", __func__, SMTPFD_USER); + + if (chroot(_PATH_VAREMPTY) == -1) + fatal("%s: chroot", __func__); + if (chdir("/") == -1) + fatal("%s: chdir", __func__); + + 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("%s: cannot drop privileges", __func__); + + if (pledge("stdio unix inet recvfd sendfd", NULL) == -1) + fatal("%s: pledge", __func__); + + event_init(); + + signal(SIGPIPE, SIG_IGN); + + /* Setup imsg socket with parent. */ + p_priv = proc_attach(PROC_PRIV, 3); + if (p_priv == NULL) + fatal("%s: proc_attach", __func__); + proc_setcallback(p_priv, frontend_dispatch_priv, NULL); + proc_enable(p_priv); + + event_dispatch(); + + frontend_shutdown(); +} + +void +frontend_conn_closed(uint32_t connid) +{ + struct listener *l; + struct conn key, *conn; + + key.id = connid; + conn = SPLAY_FIND(conntree, &conns, &key); + if (conn == NULL) + fatalx("%s: %08x unknown connid", __func__, connid); + + l = conn->listener; + + if (log_getverbose() > LOGLEVEL_CONN) + log_debug("%08x close %s %s", conn->id, + log_fmt_proto(l->proto), + log_fmt_sockaddr((struct sockaddr*)&conn->ss)); + + SPLAY_REMOVE(conntree, &conns, conn); + free(conn); + + if (l->pause) + frontend_resume(l); +} + +static void +frontend_shutdown() +{ + struct listener *l; + + TAILQ_FOREACH(l, &env->listeners, entry) + close(l->sock); + + log_debug("exiting"); + + exit(0); +} + +static void +frontend_listen(struct listener *l) +{ + if (log_getverbose() > LOGLEVEL_CONN) + log_debug("listen %s %s", log_fmt_proto(l->proto), + log_fmt_sockaddr((struct sockaddr*)&l->ss)); + + if (listen(l->sock, 5) == -1) + fatal("%s: listen", __func__); + + frontend_resume(l); +} + +static void +frontend_pause(struct listener *l) +{ + struct timeval tv; + + event_del(&l->ev); + + tv.tv_sec = 2; + tv.tv_usec = 0; + + evtimer_set(&l->ev, frontend_accept, l); + evtimer_add(&l->ev, &tv); + l->pause = 1; +} + +static void +frontend_resume(struct listener *l) +{ + if (l->pause) { + evtimer_del(&l->ev); + l->pause = 0; + } + event_set(&l->ev, l->sock, EV_READ | EV_PERSIST, frontend_accept, l); + event_add(&l->ev, NULL); +} + +static void +frontend_accept(int sock, short ev, void *arg) +{ + struct listener *l = arg; + struct sockaddr *sa; + struct conn *conn; + socklen_t len; + + if (l->pause) { + l->pause = 0; + frontend_resume(l); + return; + } + + conn = calloc(1, sizeof(*conn)); + if (conn == NULL) { + log_warn("%s: calloc", __func__); + sa = NULL; + len = 0; + } + else { + sa = (struct sockaddr *)&conn->ss; + len = sizeof(conn->ss); + } + + sock = accept4(sock, sa, &len, SOCK_NONBLOCK); + if (sock == -1) { + if (errno == ENFILE || errno == EMFILE) + frontend_pause(l); + else if (errno != EWOULDBLOCK && errno != EINTR && + errno != ECONNABORTED) + log_warn("%s: accept4", __func__); + free(conn); + return; + } + + if (conn == NULL) { + close(sock); + return; + } + + while (conn->id == 0 || SPLAY_FIND(conntree, &conns, conn)) + conn->id = arc4random(); + SPLAY_INSERT(conntree, &conns, conn); + conn->listener = l; + + if (log_getverbose() > LOGLEVEL_CONN) + log_debug("%08x accept %s %s", conn->id, + log_fmt_proto(conn->listener->proto), + log_fmt_sockaddr((struct sockaddr*)&conn->ss)); + + switch (l->proto) { + case PROTO_SMTPF: + frontend_smtpf_conn(conn->id, l, sock, sa); + break; + default: + fatalx("%s: unexpected protocol %d", __func__, l->proto); + } +} + +static void +frontend_dispatch_priv(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + struct listener *l; + + if (imsg == NULL) { + log_debug("%s: imsg connection lost", __func__); + event_loopexit(NULL); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + case IMSG_SOCK_ENGINE: + if (imsg->fd == -1) + fatalx("%s: engine socket not received", __func__); + m_end(proc); + p_engine = proc_attach(PROC_ENGINE, imsg->fd); + proc_setcallback(p_engine, frontend_dispatch_engine, NULL); + proc_enable(p_engine); + break; + + case IMSG_CONF_START: + m_end(proc); + if ((tmpconf = calloc(1, sizeof(*tmpconf))) == NULL) + fatal("%s: calloc", __func__); + TAILQ_INIT(&tmpconf->listeners); + break; + + case IMSG_CONF_LISTENER: + if (imsg->fd == -1) + fatalx("%s: listener socket not received", __func__); + if ((l = calloc(1, sizeof(*l))) == NULL) + fatal("%s: calloc", __func__); + m_get_int(proc, &l->proto); + m_get_sockaddr(proc, (struct sockaddr *)&l->ss); + m_end(proc); + l->sock = imsg->fd; + TAILQ_INSERT_TAIL(&tmpconf->listeners, l, entry); + break; + + case IMSG_CONF_END: + m_end(proc); + TAILQ_FOREACH(l, &tmpconf->listeners, entry) + frontend_listen(l); + env = tmpconf; + break; + + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} + +static void +frontend_dispatch_engine(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) { + log_debug("%s: imsg connection lost", __func__); + event_loopexit(NULL); + return; + } + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + case IMSG_RES_GETADDRINFO: + case IMSG_RES_GETADDRINFO_END: + case IMSG_RES_GETNAMEINFO: + resolver_dispatch_result(proc, imsg); + break; + + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} diff --git a/extras/filters/smtpfd/frontend_smtpf.c b/extras/filters/smtpfd/frontend_smtpf.c new file mode 100644 index 0000000..aeddf63 --- /dev/null +++ b/extras/filters/smtpfd/frontend_smtpf.c @@ -0,0 +1,307 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include "smtpfd.h" + +#include "io.h" +#include "log.h" + +#define SMTPF_LINEMAX 4096 +#define SMTPF_MAXSESSIONNAME 32 + +struct smtpf_session { + SPLAY_ENTRY(smtpf_session) entry; + char name[SMTPF_MAXSESSIONNAME]; +}; + +SPLAY_HEAD(sessiontree, smtpf_session); + +struct smtpf_client { + uint32_t id; + struct io *io; + struct sessiontree sessions; + struct smtpf_session *last; +}; + +static void smtpf_close(struct smtpf_client *); +static void smtpf_dispatch_io(struct io *, int, void *); +static void smtpf_process_line(struct smtpf_client *, char *); +static void smtpf_session_open(struct smtpf_client *, const char *, int, char **); +static void smtpf_session_close(struct smtpf_client *, const char *, int, char **); +static void smtpf_session_line(struct smtpf_client *, int, const char *, const char *); +static struct smtpf_session *smtpf_session_find(struct smtpf_client *, const char *); +static int smtpf_session_cmp(struct smtpf_session *, struct smtpf_session *); +SPLAY_PROTOTYPE(sessiontree, smtpf_session, entry, smtpf_session_cmp); + +void +frontend_smtpf_init(void) +{ +} + +void +frontend_smtpf_conn(uint32_t connid, struct listener *l, int sock, + const struct sockaddr *sa) +{ + struct smtpf_client *clt; + + if ((clt = calloc(1, sizeof(*clt))) == NULL) { + log_warn("%s: calloc", __func__); + close(sock); + frontend_conn_closed(connid); + return; + } + clt->id = connid; + clt->io = io_new(); + if (clt->io == NULL) { + close(sock); + free(clt); + frontend_conn_closed(connid); + return; + } + io_set_callback(clt->io, smtpf_dispatch_io, clt); + io_attach(clt->io, sock); + + log_debug("%08x:newconn", clt->id); +} + +static void +smtpf_close(struct smtpf_client *clt) +{ + struct smtpf_session *s; + uint32_t connid; + + log_debug("%08x:close", clt->id); + + while ((s = SPLAY_ROOT(&clt->sessions))) { + SPLAY_REMOVE(sessiontree, &clt->sessions, s); + free(s); + } + + connid = clt->id; + io_free(clt->io); + free(clt); + + frontend_conn_closed(connid); +} + +static void +smtpf_dispatch_io(struct io *io, int evt, void *arg) +{ + struct smtpf_client *clt = arg; + char *line; + + switch (evt) { + case IO_CONNECTED: + case IO_TLSREADY: + case IO_TLSERROR: + break; + + case IO_DATAIN: + while ((line = io_getline(clt->io, NULL))) + smtpf_process_line(clt, line); + + if (io_datalen(clt->io) > SMTPF_LINEMAX) { + log_warnx("%s: line too long", __func__); + break; + } + return; + + case IO_LOWAT: + return; + + case IO_DISCONNECTED: + log_debug("%08x disconnected", clt->id); + break; + + case IO_TIMEOUT: + log_debug("%08x timeout", clt->id); + break; + + case IO_ERROR: + log_warnx("%08x io error: %s", clt->id, io_error(io)); + break; + + default: + fatalx("%s: unexpected event %d", __func__, evt); + } + + smtpf_close(clt); +} + +static void +smtpf_process_line(struct smtpf_client *clt, char *line) +{ + #define MAXARGS 8 + char *cmd, *name, *data, *last, *args[MAXARGS], *p; + int i = 0; + + log_debug("%08x >>> %s", clt->id, line); + + if ((name = strchr(line, ':')) == NULL) { + log_warnx("%s: invalid line \"%s\"", __func__, line); + return; + } + if ((data = strchr(name + 1, ':')) == NULL) { + log_warnx("%s: invalid session name \"%s\"", __func__, name+1); + return; + } + cmd = line; + *name++ = '\0'; + *data++ = '\0'; + + if (!strcmp(cmd, "A")) { + smtpf_session_line(clt, 0, name, data); + } + else if (!strcmp(cmd, "B")) { + smtpf_session_line(clt, 1, name, data); + } + else if (!strcmp(cmd, "SMTPF")) { + for (p = strtok_r(data, " ", &last); p; p = strtok_r(NULL, " ", &last)) { + if (i >= MAXARGS) { + log_warnx("%s: too many args", __func__); + return; + } + args[i++] = p; + } + args[i] = NULL; + + if (!strcmp(args[0], "OPEN")) + smtpf_session_open(clt, name, i - 1, &args[1]); + else if (!strcmp(args[0], "CLOSE")) + smtpf_session_close(clt, name, i - 1, &args[1]); + } + else + log_warn("%s: invalid command \"%s\"", __func__, line); +} + +static struct smtpf_session * +smtpf_session_find(struct smtpf_client *clt, const char *name) +{ + struct smtpf_session key, *s; + + if (clt->last && !(strcmp(name, clt->last->name))) + return clt->last; + + if (strlcpy(key.name, name, sizeof(key.name)) >= sizeof(key.name)) { + log_warnx("%s: name too long", __func__); + return NULL; + } + + s = SPLAY_FIND(sessiontree, &clt->sessions, &key); + if (s == NULL) + return NULL; + + clt->last = s; + return clt->last; + +} + +static void +smtpf_session_open(struct smtpf_client *clt, const char *name, int argc, char **argv) +{ + struct smtpf_session *s; + + s = calloc(1, sizeof(*s)); + if (s == NULL) { + log_warn("%s: calloc", __func__); + goto fail; + } + + if (strlcpy(s->name, name, sizeof(s->name)) >= sizeof(s->name)) { + log_warnx("%s: name too long", __func__); + free(s); + goto fail; + } + + if (smtpf_session_find(clt, name)) { + log_warnx("%s: duplicate token", __func__); + free(s); + goto fail; + } + + SPLAY_INSERT(sessiontree, &clt->sessions, s); + clt->last = s; + io_printf(clt->io, "SMTPF:%s:OPEN OK\n", name); + log_debug("%08x <<< SMTPF:%s:OPEN OK", clt->id, name); + return; + fail: + io_printf(clt->io, "SMTPF:%s:OPEN FAILED\n", name); + log_debug("%08x <<< SMTPF:%s:OPEN FAILED", clt->id, name); +} + +static void +smtpf_session_close(struct smtpf_client *clt, const char *name, int argc, char **argv) +{ + struct smtpf_session *s; + + s = smtpf_session_find(clt, name); + if (s == NULL) { + log_warnx("%s: session not found", __func__); + io_printf(clt->io, "SMTPF:%s:CLOSE NOT_FOUND\n", name); + log_debug("%08x <<< SMTPF:%s:CLOSE NOT_FOUND", clt->id, name); + return; + } + + io_printf(clt->io, "SMTPF:%s:CLOSE OK\n", name); + log_debug("%08x <<< SMTPF:%s:CLOSE OK", clt->id, name); + + SPLAY_REMOVE(sessiontree, &clt->sessions, s); + clt->last = NULL; + free(s); +} + +static void +smtpf_session_line(struct smtpf_client *clt, int srv, const char *name, + const char *line) +{ + struct smtpf_session *s; + char *buf, *p; + + s = smtpf_session_find(clt, name); + if (s == NULL) { + log_warnx("%s: session not found", __func__); + return; + } + + if (!srv && !strcmp(line, "HAHA")) { + io_printf(clt->io, "A:%s:200 (from smtpfd) %s\n", name, line + 2); + log_debug("%08x <<< A:%s:200 (from smtpfd) %s", clt->id, name, line + 2); + } + else { + /* relay between the two ends of the session */ + buf = strdup(line); + for (p = buf; *p; p++) + if (*p == 'z') + *p = 'x'; + io_printf(clt->io, "%c:%s:%s\n", srv ? 'A' : 'B', name, buf); + log_debug("%08x <<< %c:%s:%s", clt->id, srv ? 'A' : 'B', name, buf); + free(buf); + } +} + +static int +smtpf_session_cmp(struct smtpf_session *a, struct smtpf_session *b) +{ + return strcmp(a->name, b->name); +} + +SPLAY_GENERATE(sessiontree, smtpf_session, entry, smtpf_session_cmp); diff --git a/extras/filters/smtpfd/io.c b/extras/filters/smtpfd/io.c new file mode 100644 index 0000000..1f4f16f --- /dev/null +++ b/extras/filters/smtpfd/io.c @@ -0,0 +1,1149 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2012 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "io.h" +#include "iobuf.h" +#include "log.h" + +#ifdef IO_SSL +#include +#include +#endif + +enum { + IO_STATE_DOWN, + IO_STATE_UP, + IO_STATE_CONNECT, + IO_STATE_CONNECT_TLS, + IO_STATE_ACCEPT_TLS +}; + +#define IO_PAUSE_IN IO_IN +#define IO_PAUSE_OUT IO_OUT + +#define IO_READ 0x0100 +#define IO_WRITE 0x0200 +#define IO_RW (IO_READ | IO_WRITE) +#define IO_RESET 0x1000 +#define IO_HELD 0x2000 + +struct io { + int sock; + void *arg; + void (*cb)(struct io*, int, void *); + struct iobuf iobuf; + size_t lowat; + int timeout; + int flags; + int state; + struct event ev; + void *tls; + const char *error; /* only valid immediately on callback */ + struct sockaddr *bind; + struct addrinfo *ai; /* for connecting */ +}; + +static const char* io_strflags(int); +static const char* io_strevents(short); + +static void io_reload(struct io *); +static void io_reset(struct io *, short, void (*)(int, short, void*)); +static void io_frame_enter(const char *, struct io *, int); +static void io_frame_leave(struct io *); +static void io_hold(struct io *); +static void io_release(struct io *); +static void io_callback(struct io*, int); +static void io_dispatch(int, short, void *); +static void io_dispatch_connect(int, short, void *); +static int io_connect_next(struct io *); + +#ifdef IO_SSL +void ssl_error(const char *); /* XXX external */ +static const char* io_ssl_error(void); +static void io_dispatch_accept_tls(int, short, void *); +static void io_dispatch_connect_tls(int, short, void *); +static void io_dispatch_read_tls(int, short, void *); +static void io_dispatch_write_tls(int, short, void *); +static void io_reload_tls(struct io *io); +#endif + +static struct io *current = NULL; +static long long unsigned frame = 0; +static int _io_trace = 0; + +static const char *states[] = { + "DOWN", + "UP", + "CONNECT", + "CONNECT_TLS", + "ACCEPT_TLS" +}; + +#define io_debug(args...) do { if (_io_trace) log_debug(args); } while(0) +#define IO_READING(io) (((io)->flags & IO_RW) != IO_WRITE) +#define IO_WRITING(io) (((io)->flags & IO_RW) != IO_READ) + +void +io_trace(int on) +{ + _io_trace = on; +} + +const char* +io_strio(struct io *io) +{ + static char buf[128]; + char ssl[128]; + + ssl[0] = '\0'; +#ifdef IO_SSL + if (io->tls) { + (void)snprintf(ssl, sizeof ssl, " ssl=%s:%s:%d", + SSL_get_version(io->tls), + SSL_get_cipher_name(io->tls), + SSL_get_cipher_bits(io->tls, NULL)); + } +#endif + (void)snprintf(buf, sizeof buf, + "", + io, states[io->state], io->sock, io->timeout, + io_strflags(io->flags), ssl, io_datalen(io), io_queued(io)); + + return buf; +} + +const char* +io_strevent(int evt) +{ + static char buf[32]; + + switch (evt) { + case IO_CONNECTED: + return "IO_CONNECTED"; + case IO_TLSREADY: + return "IO_TLSREADY"; + case IO_DATAIN: + return "IO_DATAIN"; + case IO_LOWAT: + return "IO_LOWAT"; + case IO_CLOSED: + return "IO_CLOSED"; + case IO_DISCONNECTED: + return "IO_DISCONNECTED"; + case IO_TIMEOUT: + return "IO_TIMEOUT"; + case IO_ERROR: + return "IO_ERROR"; + case IO_TLSERROR: + return "IO_TLSERROR"; + default: + (void)snprintf(buf, sizeof(buf), "IO_? %d", evt); + return buf; + } +} + +struct io * +io_new(void) +{ + struct io *io; + + io = calloc(1, sizeof(*io)); + if (io == NULL) + return NULL; + + iobuf_init(&io->iobuf, 0, 0); + io->sock = -1; + io->timeout = -1; + + return io; +} + +void +io_free(struct io *io) +{ + io_debug("%s(%p)", __func__, io); + + /* the current io is virtually dead */ + if (io == current) + current = NULL; + +#ifdef IO_SSL + if (io->tls) { + SSL_free(io->tls); + io->tls = NULL; + } +#endif + + if (io->ai) + freeaddrinfo(io->ai); + if (event_initialized(&io->ev)) + event_del(&io->ev); + if (io->sock != -1) { + (void)close(io->sock); + io->sock = -1; + } + + iobuf_clear(&io->iobuf); + free(io->bind); + free(io); +} + +int +io_set_callback(struct io *io, void(*cb)(struct io *, int, void *), void *arg) +{ + io->cb = cb; + io->arg = arg; + + return 0; +} + +int +io_set_bindaddr(struct io *io, const struct sockaddr *sa) +{ + struct sockaddr *t; + + if (io->state != IO_STATE_DOWN) { + errno = EISCONN; + return -1; + } + + t = malloc(sa->sa_len); + if (t == NULL) + return -1; + memmove(t, sa, sa->sa_len); + + free(io->bind); + io->bind = t; + + return 0; +} + +int +io_set_bufsize(struct io *io, size_t sz) +{ + errno = ENOSYS; + return -1; +} + +void +io_set_timeout(struct io *io, int msec) +{ + io_debug("%s(%p, %d)", __func__, io, msec); + + io->timeout = msec; +} + +void +io_set_lowat(struct io *io, size_t lowat) +{ + io_debug("%s(%p, %zu)", __func__, io, lowat); + + io->lowat = lowat; +} + +const char * +io_error(struct io *io) +{ + const char *e; + + e = io->error; + io->error = NULL; + return e; +} + +int +io_fileno(struct io *io) +{ + return io->sock; +} + +int +io_attach(struct io *io, int sock) +{ + if (io->state != IO_STATE_DOWN) { + errno = EISCONN; + return -1; + } + + io->state = IO_STATE_UP; + io->sock = sock; + io_reload(io); + return 0; +} + +int +io_detach(struct io *io) +{ + errno = ENOSYS; + return -1; +} + +int +io_close(struct io *io) +{ + errno = ENOSYS; + return -1; +} + +int +io_connect(struct io *io, struct addrinfo *ai) +{ + if (ai == NULL) { + errno = EINVAL; + fatal("%s", __func__); + return -1; + } + + if (io->state != IO_STATE_DOWN) { + freeaddrinfo(ai); + errno = EISCONN; + fatal("%s", __func__); + return -1; + } + + io->ai = ai; + return io_connect_next(io); +} + +int +io_disconnect(struct io *io) +{ + errno = ENOSYS; + fatal("%s", __func__); + return -1; +} + +int +io_starttls(struct io *io, void *ssl) +{ +#ifdef IO_SSL + int mode; + + mode = io->flags & IO_RW; + if (mode == 0 || mode == IO_RW) + fatalx("%s: full-duplex or unset", __func__); + + if (io->tls) + fatalx("%s: SSL already started", __func__); + io->tls = ssl; + + if (SSL_set_fd(io->tls, io->sock) == 0) { + ssl_error("io_start_tls:SSL_set_fd"); + return -1; + } + + if (mode == IO_WRITE) { + io->state = IO_STATE_CONNECT_TLS; + SSL_set_connect_state(io->tls); + io_reset(io, EV_WRITE, io_dispatch_connect_tls); + } else { + io->state = IO_STATE_ACCEPT_TLS; + SSL_set_accept_state(io->tls); + io_reset(io, EV_READ, io_dispatch_accept_tls); + } + + return 0; +#else + errno = ENOSYS; + return -1; +#endif +} + +void +io_pause(struct io *io, int dir) +{ + io_debug("%s(%p, %x)", __func__, io, dir); + + io->flags |= dir & (IO_IN | IO_OUT); + io_reload(io); +} + +void +io_resume(struct io *io, int dir) +{ + io_debug("%s(%p, %x)", __func__, io, dir); + + io->flags &= ~(dir & (IO_IN | IO_OUT)); + io_reload(io); +} + +void +io_set_read(struct io *io) +{ + int mode; + + io_debug("%s(%p)", __func__, io); + + mode = io->flags & IO_RW; + if (!(mode == 0 || mode == IO_WRITE)) + fatalx("%s: full-duplex or reading", __func__); + + io->flags &= ~IO_RW; + io->flags |= IO_READ; + io_reload(io); +} + +void +io_set_write(struct io *io) +{ + int mode; + + io_debug("%s(%p)", __func__, io); + + mode = io->flags & IO_RW; + if (!(mode == 0 || mode == IO_READ)) + fatalx("%s: full-duplex or writing", __func__); + + io->flags &= ~IO_RW; + io->flags |= IO_WRITE; + io_reload(io); +} + +int +io_write(struct io *io, const void *buf, size_t len) +{ + int r; + + r = iobuf_queue(&io->iobuf, buf, len); + + io_reload(io); + + return r; +} + +int +io_writev(struct io *io, const struct iovec *iov, int iovcount) +{ + int r; + + r = iobuf_queuev(&io->iobuf, iov, iovcount); + + io_reload(io); + + return r; +} + +int +io_print(struct io *io, const char *s) +{ + return io_write(io, s, strlen(s)); +} + +int +io_printf(struct io *io, const char *fmt, ...) +{ + va_list ap; + int r; + + va_start(ap, fmt); + r = io_vprintf(io, fmt, ap); + va_end(ap); + + return r; +} + +int +io_vprintf(struct io *io, const char *fmt, va_list ap) +{ + + char *buf; + int len; + + len = vasprintf(&buf, fmt, ap); + if (len == -1) + return -1; + + len = io_write(io, buf, len); + free(buf); + + return len; +} + +size_t +io_queued(struct io *io) +{ + return iobuf_queued(&io->iobuf); +} + +void * +io_data(struct io *io) +{ + return iobuf_data(&io->iobuf); +} + +size_t +io_datalen(struct io *io) +{ + return iobuf_len(&io->iobuf); +} + +char * +io_getline(struct io *io, size_t *sz) +{ + return iobuf_getline(&io->iobuf, sz); +} + +void +io_drop(struct io *io, size_t sz) +{ + return iobuf_drop(&io->iobuf, sz); +} + +const char* +io_strflags(int flags) +{ + static char buf[64]; + + buf[0] = '\0'; + + switch (flags & IO_RW) { + case 0: + (void)strlcat(buf, "rw", sizeof buf); + break; + case IO_READ: + (void)strlcat(buf, "R", sizeof buf); + break; + case IO_WRITE: + (void)strlcat(buf, "W", sizeof buf); + break; + case IO_RW: + (void)strlcat(buf, "RW", sizeof buf); + break; + } + + if (flags & IO_PAUSE_IN) + (void)strlcat(buf, ",F_PI", sizeof buf); + if (flags & IO_PAUSE_OUT) + (void)strlcat(buf, ",F_PO", sizeof buf); + + return buf; +} + +const char* +io_strevents(short ev) +{ + static char buf[64]; + char buf2[16]; + int n; + + n = 0; + buf[0] = '\0'; + + if (ev == 0) { + (void)strlcat(buf, "", sizeof(buf)); + return buf; + } + + if (ev & EV_TIMEOUT) { + (void)strlcat(buf, "EV_TIMEOUT", sizeof(buf)); + ev &= ~EV_TIMEOUT; + n++; + } + + if (ev & EV_READ) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_READ", sizeof(buf)); + ev &= ~EV_READ; + n++; + } + + if (ev & EV_WRITE) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_WRITE", sizeof(buf)); + ev &= ~EV_WRITE; + n++; + } + + if (ev & EV_SIGNAL) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_SIGNAL", sizeof(buf)); + ev &= ~EV_SIGNAL; + n++; + } + + if (ev) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_?=0x", sizeof(buf)); + (void)snprintf(buf2, sizeof(buf2), "%hx", ev); + (void)strlcat(buf, buf2, sizeof(buf)); + } + + return buf; +} + +/* + * Setup the necessary events as required by the current io state, + * honouring duplex mode and i/o pause. + */ +static void +io_reload(struct io *io) +{ + short events; + + /* The io will be reloaded at release time. */ + if (io->flags & IO_HELD) + return; + + /* Do nothing if no socket. */ + if (io->sock == -1) + return; + +#ifdef IO_SSL + if (io->tls) { + io_reload_tls(io); + return; + } +#endif + + io_debug("%s(%p)", __func__, 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); +} + +static void +io_reset(struct io *io, short events, void (*dispatch)(int, short, void*)) +{ + struct timeval tv, *ptv; + + io_debug("%s(%p, %s, %p) -> %s", __func__, io, + io_strevents(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; + + if (event_initialized(&io->ev)) + 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); +} + +static void +io_frame_enter(const char *where, struct io *io, int ev) +{ + io_debug("io: BEGIN %llu", frame); + io_debug("%s(%s, %s, %s)", __func__, where, io_strevents(ev), + io_strio(io)); + + if (current) + fatalx("%s: interleaved frames", __func__); + + current = io; + + io_hold(io); +} + +static void +io_frame_leave(struct io *io) +{ + io_debug("%s(%llu)", __func__, frame); + + if (current && current != io) + fatalx("%s: io mismatch", __func__); + + /* The 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 + * immediately 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("io: END %llu", frame); + + frame += 1; +} + +static void +io_hold(struct io *io) +{ + io_debug("%s(%p)", __func__, io); + + if (io->flags & IO_HELD) + fatalx("%s: already held", __func__); + + io->flags &= ~IO_RESET; + io->flags |= IO_HELD; +} + +static void +io_release(struct io *io) +{ + io_debug("%s(%p)", __func__, io); + + if (!(io->flags & IO_HELD)) + fatalx("%s: not held", __func__); + + io->flags &= ~IO_HELD; + if (!(io->flags & IO_RESET)) + io_reload(io); +} + +static void +io_callback(struct io *io, int evt) +{ + io_debug("%s(%s, %s)", __func__, io_strio(io), io_strevent(evt)); + + io->cb(io, evt, io->arg); +} + +static void +io_dispatch(int fd, short ev, void *arg) +{ + struct io *io = arg; + size_t w; + ssize_t n; + int saved_errno; + + io_frame_enter(__func__, 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 { + log_warn("%s: iobuf_write", __func__); + 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) { + iobuf_normalize(&io->iobuf); + if ((n = iobuf_read(&io->iobuf, io->sock)) < 0) { + if (n == IOBUF_CLOSED) + io_callback(io, IO_DISCONNECTED); + else { + log_warn("%s: iobuf_read", __func__); + 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); +} + +static void +io_dispatch_connect(int fd, short ev, void *arg) +{ + struct io *io = arg; + socklen_t sl; + int r, e; + + io_frame_enter(__func__, io, ev); + + if (ev == EV_TIMEOUT) + e = ETIMEDOUT; + else { + sl = sizeof(e); + r = getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &sl); + if (r == -1) { + log_warn("%s: getsockopt", __func__); + e = errno; + } + else if (e) { + errno = e; + log_warn("%s: (connect)", __func__); + } + } + + if (e == 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_CONNECTED); + goto done; + } + + while (io->ai) { + r = io_connect_next(io); + if (r == 0) + goto done; + e = errno; + } + + (void)close(fd); + io->sock = -1; + io->error = strerror(e); + io->state = IO_STATE_DOWN; + io_callback(io, e == ETIMEDOUT ? IO_TIMEOUT : IO_ERROR); + done: + io_frame_leave(io); +} + +static int +io_connect_next(struct io *io) +{ + struct addrinfo *ai; + struct linger l; + int saved_errno; + + while ((ai = io->ai)) { + io->ai = ai->ai_next; + ai->ai_next = NULL; + if (ai->ai_socktype == SOCK_STREAM) + break; + freeaddrinfo(ai); + } + + if (ai == NULL) { + errno = ESOCKTNOSUPPORT; + log_warn("%s", __func__); + return -1; + } + + if ((io->sock = socket(ai->ai_family, ai->ai_socktype | SOCK_NONBLOCK, + 0)) == -1) { + log_warn("%s: socket", __func__); + goto fail; + } + + memset(&l, 0, sizeof(l)); + if (setsockopt(io->sock, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) { + log_warn("%s: setsockopt", __func__); + goto fail; + } + + if (io->bind && bind(io->sock, io->bind, io->bind->sa_len) == -1) { + log_warn("%s: bind", __func__); + goto fail; + } + + if (connect(io->sock, ai->ai_addr, ai->ai_addr->sa_len) == -1) + if (errno != EINPROGRESS) { + log_warn("%s: connect", __func__); + goto fail; + } + + freeaddrinfo(ai); + io->state = IO_STATE_CONNECT; + io_reset(io, EV_WRITE, io_dispatch_connect); + return 0; + + fail: + if (io->sock != -1) { + saved_errno = errno; + close(io->sock); + errno = saved_errno; + io->error = strerror(errno); + io->sock = -1; + } + freeaddrinfo(ai); + if (io->ai) { + freeaddrinfo(io->ai); + io->ai = NULL; + } + return -1; +} + +#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"; +} + +static void +io_dispatch_accept_tls(int fd, short event, void *arg) +{ + struct io *io = arg; + int e, ret; + + io_frame_enter(__func__, io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if ((ret = SSL_accept(io->tls)) > 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_TLSREADY); + goto leave; + } + + switch ((e = SSL_get_error(io->tls, ret))) { + case SSL_ERROR_WANT_READ: + io_reset(io, EV_READ, io_dispatch_accept_tls); + break; + case SSL_ERROR_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_accept_tls); + break; + default: + io->error = io_ssl_error(); + ssl_error("io_dispatch_accept_tls:SSL_accept"); + io_callback(io, IO_TLSERROR); + break; + } + + leave: + io_frame_leave(io); +} + +static void +io_dispatch_connect_tls(int fd, short event, void *arg) +{ + struct io *io = arg; + int e, ret; + + io_frame_enter(__func__, io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if ((ret = SSL_connect(io->tls)) > 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_TLSREADY); + goto leave; + } + + switch ((e = SSL_get_error(io->tls, ret))) { + case SSL_ERROR_WANT_READ: + io_reset(io, EV_READ, io_dispatch_connect_tls); + break; + case SSL_ERROR_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_connect_tls); + break; + default: + io->error = io_ssl_error(); + ssl_error("io_dispatch_connect_tls:SSL_connect"); + io_callback(io, IO_TLSERROR); + break; + } + + leave: + io_frame_leave(io); +} + +static void +io_dispatch_read_tls(int fd, short event, void *arg) +{ + struct io *io = arg; + int n, saved_errno; + + io_frame_enter(__func__, io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + +again: + iobuf_normalize(&io->iobuf); + switch ((n = iobuf_read_ssl(&io->iobuf, (SSL*)io->tls))) { + case IOBUF_WANT_READ: + io_reset(io, EV_READ, io_dispatch_read_tls); + break; + case IOBUF_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_read_tls); + break; + case IOBUF_CLOSED: + io_callback(io, IO_DISCONNECTED); + break; + case IOBUF_ERROR: + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + log_warn("%s: iobuf_read_ssl", __func__); + io_callback(io, IO_ERROR); + break; + case IOBUF_SSLERROR: + io->error = io_ssl_error(); + ssl_error("io_dispatch_read_tls:SSL_read"); + io_callback(io, IO_TLSERROR); + break; + default: + io_debug("%s(...) -> r=%d", __func__, n); + io_callback(io, IO_DATAIN); + if (current == io && IO_READING(io) && SSL_pending(io->tls)) + goto again; + } + + leave: + io_frame_leave(io); +} + +static void +io_dispatch_write_tls(int fd, short event, void *arg) +{ + struct io *io = arg; + size_t w2, w; + int n, saved_errno; + + io_frame_enter(__func__, 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->tls))) { + case IOBUF_WANT_READ: + io_reset(io, EV_READ, io_dispatch_write_tls); + break; + case IOBUF_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_write_tls); + break; + case IOBUF_CLOSED: + io_callback(io, IO_DISCONNECTED); + break; + case IOBUF_ERROR: + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + log_warn("%s: iobuf_write_ssl", __func__); + io_callback(io, IO_ERROR); + break; + case IOBUF_SSLERROR: + io->error = io_ssl_error(); + ssl_error("io_dispatch_write_tls:SSL_write"); + io_callback(io, IO_TLSERROR); + break; + default: + io_debug("%s(...) -> w=%d", __func__, n); + w2 = io_queued(io); + if (w > io->lowat && w2 <= io->lowat) + io_callback(io, IO_LOWAT); + break; + } + + leave: + io_frame_leave(io); +} + +static void +io_reload_tls(struct io *io) +{ + short ev = 0; + void (*dispatch)(int, short, void*) = NULL; + + switch (io->state) { + case IO_STATE_CONNECT_TLS: + ev = EV_WRITE; + dispatch = io_dispatch_connect_tls; + break; + case IO_STATE_ACCEPT_TLS: + ev = EV_READ; + dispatch = io_dispatch_accept_tls; + break; + case IO_STATE_UP: + ev = 0; + if (IO_READING(io) && !(io->flags & IO_PAUSE_IN)) { + ev = EV_READ; + dispatch = io_dispatch_read_tls; + } + else if (IO_WRITING(io) && !(io->flags & IO_PAUSE_OUT) && + io_queued(io)) { + ev = EV_WRITE; + dispatch = io_dispatch_write_tls; + } + if (!ev) + return; /* paused */ + break; + default: + fatalx("%s: unexpected state %d", __func__, io->state); + } + + io_reset(io, ev, dispatch); +} + +#endif /* IO_SSL */ diff --git a/extras/filters/smtpfd/io.h b/extras/filters/smtpfd/io.h new file mode 100644 index 0000000..59f7939 --- /dev/null +++ b/extras/filters/smtpfd/io.h @@ -0,0 +1,85 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +enum { + IO_CONNECTED = 0, /* connection successful */ + IO_TLSREADY, /* TLS started successfully */ + IO_DATAIN, /* new data in input buffer */ + IO_LOWAT, /* output queue running low */ + IO_CLOSED, /* normally terminated */ + IO_DISCONNECTED, /* error? */ + IO_TIMEOUT, /* error? */ + IO_ERROR, /* details? */ + IO_TLSERROR, /* XXX - needs more work */ +}; + +#define IO_IN 0x1 +#define IO_OUT 0x2 + +struct io; + +void io_trace(int); +const char* io_strio(struct io *); +const char* io_strevent(int); + +/* IO management */ +struct io *io_new(void); +void io_free(struct io *); + +/* IO setup */ +int io_set_callback(struct io *, void(*)(struct io *, int, void *), void *); +int io_set_bindaddr(struct io *, const struct sockaddr *); +int io_set_bufsize(struct io *, size_t); +void io_set_timeout(struct io *, int); +void io_set_lowat(struct io *, size_t); + +/* State retreival */ +const char *io_error(struct io *); +int io_fileno(struct io *); + +/* Connection management */ +int io_attach(struct io *io, int); +int io_detach(struct io *io); +int io_close(struct io *io); +int io_connect(struct io *, struct addrinfo *); +int io_disconnect(struct io *io); +int io_starttls(struct io *, void *); + +/* Flow control */ +void io_pause(struct io *, int); +void io_resume(struct io *, int); + +/* IO direction */ +void io_set_read(struct io *); +void io_set_write(struct io *); + +/* Output buffering */ +int io_write(struct io *, const void *, size_t); +int io_writev(struct io *, const struct iovec *, int); +int io_print(struct io *, const char *); +int io_printf(struct io *, const char *, ...); +int io_vprintf(struct io *, const char *, va_list); +size_t io_queued(struct io *); + +/* Buffered input */ +void * io_data(struct io *); +size_t io_datalen(struct io *); +char * io_getline(struct io *, size_t *); +void io_drop(struct io *, size_t); diff --git a/extras/filters/smtpfd/iobuf.c b/extras/filters/smtpfd/iobuf.c new file mode 100644 index 0000000..8706793 --- /dev/null +++ b/extras/filters/smtpfd/iobuf.c @@ -0,0 +1,467 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2012 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef IO_SSL +#include +#include +#endif + +#include "iobuf.h" + +#define IOBUF_MAX 65536 +#define IOBUFQ_MIN 4096 + +struct ioqbuf *ioqbuf_alloc(struct iobuf *, size_t); +void iobuf_drain(struct iobuf *, size_t); + +int +iobuf_init(struct iobuf *io, size_t size, size_t max) +{ + memset(io, 0, sizeof *io); + + if (max == 0) + max = IOBUF_MAX; + + if (size == 0) + size = max; + + if (size > max) + return (-1); + + if ((io->buf = calloc(size, 1)) == NULL) + return (-1); + + io->size = size; + io->max = max; + + return (0); +} + +void +iobuf_clear(struct iobuf *io) +{ + struct ioqbuf *q; + + 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 = recallocarray(io->buf, io->size, io->size + n, 1); + 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 (len); +} + +int +iobuf_queuev(struct iobuf *io, const struct iovec *iov, int iovcnt) +{ + int i; + size_t len = 0; + char *buf; + + for (i = 0; i < iovcnt; i++) + len += iov[i].iov_len; + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len == 0) + continue; + memmove(buf, iov[i].iov_base, iov[i].iov_len); + buf += iov[i].iov_len; + } + + return (0); + +} + +int +iobuf_fqueue(struct iobuf *io, const char *fmt, ...) +{ + va_list ap; + int len; + + va_start(ap, fmt); + len = iobuf_vfqueue(io, fmt, ap); + va_end(ap); + + return (len); +} + +int +iobuf_vfqueue(struct iobuf *io, const char *fmt, va_list ap) +{ + char *buf; + int len; + + len = vasprintf(&buf, fmt, ap); + + if (len == -1) + return (-1); + + len = iobuf_queue(io, buf, len); + free(buf); + + return (len); +} + +ssize_t +iobuf_write(struct iobuf *io, int fd) +{ + struct iovec iov[IOV_MAX]; + struct ioqbuf *q; + int i; + ssize_t n; + + i = 0; + for (q = io->outq; q ; q = q->next) { + if (i >= IOV_MAX) + break; + iov[i].iov_base = q->buf + q->rpos; + iov[i].iov_len = q->wpos - q->rpos; + i++; + } + + n = writev(fd, iov, i); + if (n == -1) { + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_WRITE); + if (errno == EPIPE) + return (IOBUF_CLOSED); + return (IOBUF_ERROR); + } + + iobuf_drain(io, n); + + return (n); +} + +int +iobuf_flush(struct iobuf *io, int fd) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write(io, fd)) < 0) + return (s); + + return (0); +} + +#ifdef IO_SSL + +int +iobuf_flush_ssl(struct iobuf *io, void *ssl) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write_ssl(io, ssl)) < 0) + return (s); + + return (0); +} + +ssize_t +iobuf_write_ssl(struct iobuf *io, void *ssl) +{ + struct ioqbuf *q; + int r; + ssize_t n; + + q = io->outq; + n = SSL_write(ssl, q->buf + q->rpos, q->wpos - q->rpos); + if (n <= 0) { + switch ((r = SSL_get_error(ssl, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_ZERO_RETURN: /* connection closed */ + return (IOBUF_CLOSED); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_SSLERROR); + if (r == 0) + errno = EPIPE; + return (IOBUF_ERROR); + default: + return (IOBUF_SSLERROR); + } + } + iobuf_drain(io, n); + + return (n); +} + +ssize_t +iobuf_read_ssl(struct iobuf *io, void *ssl) +{ + ssize_t n; + int r; + + n = SSL_read(ssl, io->buf + io->wpos, iobuf_left(io)); + if (n < 0) { + switch ((r = SSL_get_error(ssl, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_SSLERROR); + if (r == 0) + errno = EPIPE; + return (IOBUF_ERROR); + default: + return (IOBUF_SSLERROR); + } + } else if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +#endif /* IO_SSL */ diff --git a/extras/filters/smtpfd/iobuf.h b/extras/filters/smtpfd/iobuf.h new file mode 100644 index 0000000..f1385a2 --- /dev/null +++ b/extras/filters/smtpfd/iobuf.h @@ -0,0 +1,68 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +struct ioqbuf { + struct ioqbuf *next; + char *buf; + size_t size; + size_t wpos; + size_t rpos; +}; + +struct iobuf { + char *buf; + size_t max; + size_t size; + size_t wpos; + size_t rpos; + + size_t queued; + struct ioqbuf *outq; + struct ioqbuf *outqlast; +}; + +#define IOBUF_WANT_READ -1 +#define IOBUF_WANT_WRITE -2 +#define IOBUF_CLOSED -3 +#define IOBUF_ERROR -4 +#define IOBUF_SSLERROR -5 + +int iobuf_init(struct iobuf *, size_t, size_t); +void iobuf_clear(struct iobuf *); + +int iobuf_extend(struct iobuf *, size_t); +void iobuf_normalize(struct iobuf *); +void iobuf_drop(struct iobuf *, size_t); +size_t iobuf_space(struct iobuf *); +size_t iobuf_len(struct iobuf *); +size_t iobuf_left(struct iobuf *); +char *iobuf_data(struct iobuf *); +char *iobuf_getline(struct iobuf *, size_t *); +ssize_t iobuf_read(struct iobuf *, int); +ssize_t iobuf_read_ssl(struct iobuf *, void *); + +size_t iobuf_queued(struct iobuf *); +void* iobuf_reserve(struct iobuf *, size_t); +int iobuf_queue(struct iobuf *, const void*, size_t); +int iobuf_queuev(struct iobuf *, const struct iovec *, int); +int iobuf_fqueue(struct iobuf *, const char *, ...); +int iobuf_vfqueue(struct iobuf *, const char *, va_list); +int iobuf_flush(struct iobuf *, int); +int iobuf_flush_ssl(struct iobuf *, void *); +ssize_t iobuf_write(struct iobuf *, int); +ssize_t iobuf_write_ssl(struct iobuf *, void *); diff --git a/extras/filters/smtpfd/log.c b/extras/filters/smtpfd/log.c new file mode 100644 index 0000000..b42a45d --- /dev/null +++ b/extras/filters/smtpfd/log.c @@ -0,0 +1,220 @@ +/* $OpenBSD: log.c,v 1.20 2017/03/21 12:06:56 bluhm Exp $ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static int debug; +static int verbose; +const char *log_procname; + +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + +void +log_init(int n_debug, int facility) +{ + extern char *__progname; + + debug = n_debug; + verbose = n_debug; + log_procinit(__progname); + + if (!debug) + openlog(__progname, LOG_PID | LOG_NDELAY, facility); + + tzset(); +} + +void +log_procinit(const char *procname) +{ + if (procname != NULL) + log_procname = procname; +} + +void +log_setverbose(int v) +{ + verbose = v; +} + +int +log_getverbose(void) +{ + return (verbose); +} + +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; + int saved_errno = errno; + + if (debug) { + /* best effort in out of mem situations */ + if (asprintf(&nfmt, "[%s(%d)] %s\n", log_procname, + (int)getpid(), fmt) == -1) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } else { + vfprintf(stderr, nfmt, ap); + free(nfmt); + } + fflush(stderr); + } else + vsyslog(pri, fmt, ap); + + errno = saved_errno; +} + +void +log_warn(const char *emsg, ...) +{ + char *nfmt; + va_list ap; + int saved_errno = errno; + + /* best effort to even work in out of memory situations */ + if (emsg == NULL) + logit(LOG_ERR, "%s", strerror(saved_errno)); + else { + va_start(ap, emsg); + + if (asprintf(&nfmt, "%s: %s", emsg, + strerror(saved_errno)) == -1) { + /* we tried it... */ + vlog(LOG_ERR, emsg, ap); + logit(LOG_ERR, "%s", strerror(saved_errno)); + } else { + vlog(LOG_ERR, nfmt, ap); + free(nfmt); + } + va_end(ap); + } + + errno = saved_errno; +} + +void +log_warnx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_ERR, emsg, ap); + va_end(ap); +} + +void +log_info(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_INFO, emsg, ap); + va_end(ap); +} + +void +log_debug(const char *emsg, ...) +{ + va_list ap; + + if (verbose > 1) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +static void +vfatalc(int code, const char *emsg, va_list ap) +{ + static char s[BUFSIZ]; + const char *sep; + + if (emsg != NULL) { + (void)vsnprintf(s, sizeof(s), emsg, ap); + sep = ": "; + } else { + s[0] = '\0'; + sep = ""; + } + if (code) + logit(LOG_CRIT, "%s: %s%s%s", + log_procname, s, sep, strerror(code)); + else + logit(LOG_CRIT, "%s%s%s", log_procname, sep, s); +} + +void +fatal(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(errno, emsg, ap); + va_end(ap); + exit(1); +} + +void +fatalx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(0, emsg, ap); + va_end(ap); + exit(1); +} diff --git a/extras/filters/smtpfd/log.h b/extras/filters/smtpfd/log.h new file mode 100644 index 0000000..d680e86 --- /dev/null +++ b/extras/filters/smtpfd/log.h @@ -0,0 +1,42 @@ +/* $OpenBSD: log.h,v 1.7 2017/01/09 14:49:22 reyk Exp $ */ + +/* + * Copyright (c) 2010 Gilles Chehade + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +/* log.c */ +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); diff --git a/extras/filters/smtpfd/logmsg.c b/extras/filters/smtpfd/logmsg.c new file mode 100644 index 0000000..6b3d2db --- /dev/null +++ b/extras/filters/smtpfd/logmsg.c @@ -0,0 +1,143 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#include "smtpfd.h" + +#include "io.h" +#include "log.h" +#include "proc.h" + +const char * +log_fmt_proto(int p) +{ + switch (p) { + case PROTO_SMTPF: + return "smtpf"; + default: + return NULL; + } +}; + +const char * +log_fmt_imsgtype(int type) +{ + static char buf[16]; + + switch (type) { + case IMSG_NONE: + return "IMSG_NONE"; + case IMSG_SOCK_ENGINE: + return "IMSG_SOCK_ENGINE"; + case IMSG_SOCK_FRONTEND: + return "IMSG_SOCK_FRONTEND"; + case IMSG_CONF_START: + return "IMSG_CONF_START"; + case IMSG_CONF_FILTER_PROC: + return "IMSG_CONF_FILTER_PROC"; + case IMSG_CONF_LISTENER: + return "IMSG_CONF_LISTENER"; + case IMSG_CONF_END: + return "IMSG_CONF_END"; + case IMSG_RES_GETADDRINFO: + return "IMSG_RES_GETADDRINFO"; + case IMSG_RES_GETADDRINFO_END: + return "IMSG_RES_GETADDRINFO_END"; + case IMSG_RES_GETNAMEINFO: + return "IMSG_RES_GETNAMEINFO"; + default: + snprintf(buf, sizeof(buf), "?%d", type); + return buf; + } +} + +const char * +log_fmt_proctype(int proctype) +{ + switch (proctype) { + case PROC_CLIENT: + return "client"; + case PROC_CONTROL: + return "control"; + case PROC_ENGINE: + return "engine"; + case PROC_FILTER: + return "filter"; + case PROC_FRONTEND: + return "frontend"; + case PROC_PRIV: + return "priv"; + default: + return NULL; + } +}; + +const char * +log_fmt_sockaddr(const struct sockaddr *sa) +{ + static char buf[PATH_MAX]; + char host[NI_MAXHOST], serv[NI_MAXSERV]; + + switch (sa->sa_family) { + case AF_LOCAL: + (void)strlcpy(buf, ((const struct sockaddr_un*)sa)->sun_path, + sizeof(buf)); + return buf; + + case AF_INET: + case AF_INET6: + if (getnameinfo(sa, sa->sa_len, host, sizeof(host), + serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV)) { + log_warnx("%s: getnameinfo", __func__); + return NULL; + } + if (sa->sa_family == AF_INET6) + snprintf(buf, sizeof(buf), "[%s]:%s", host, serv); + else + snprintf(buf, sizeof(buf), "%s:%s", host, serv); + return buf; + + default: + return NULL; + } +} + +void +log_imsg(struct imsgproc *proc, struct imsg *imsg) +{ + if (imsg == NULL) + log_debug("imsg src=%s closed", + log_fmt_proctype(proc_gettype(proc))); + else + log_debug("imsg src=%s type=%s len=%d fd=%d", + log_fmt_proctype(proc_gettype(proc)), + log_fmt_imsgtype(imsg->hdr.type), + imsg->hdr.len, imsg->fd); +} + +void +log_io(const char *name, struct io *io, int ev) +{ + log_debug("io %s evt=%s io=%s", name, io_strevent(ev), + io_strio(io)); +} diff --git a/extras/filters/smtpfd/parse.y b/extras/filters/smtpfd/parse.y new file mode 100644 index 0000000..acfcc58 --- /dev/null +++ b/extras/filters/smtpfd/parse.y @@ -0,0 +1,1040 @@ +/* $OpenBSD: parse.y,v 1.183 2016/02/22 16:19:05 gilles Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade + * Copyright (c) 2008 Pierre-Yves Ritschard + * Copyright (c) 2002, 2003, 2004 Henning Brauer + * Copyright (c) 2001 Markus Friedl. All rights reserved. + * Copyright (c) 2001 Daniel Hartmeier. All rights reserved. + * Copyright (c) 2001 Theo de Raadt. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +%{ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "smtpfd.h" + +#include "log.h" + +TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); +static struct file { + TAILQ_ENTRY(file) entry; + FILE *stream; + char *name; + int lineno; + int errors; +} *file, *topfile; +struct file *pushfile(const char *, int); +int popfile(void); +int check_file_secrecy(int, const char *); +int yyparse(void); +int yylex(void); +int kw_cmp(const void *, const void *); +int lookup(char *); +int lgetc(int); +int lungetc(int); +int findeol(void); +int yyerror(const char *, ...) + __attribute__((__format__ (printf, 1, 2))) + __attribute__((__nonnull__ (1))); + +TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead); +struct sym { + TAILQ_ENTRY(sym) entry; + int used; + int persist; + char *nam; + char *val; +}; +int symset(const char *, const char *, int); +char *symget(const char *); + +static int errors = 0; + +struct smtpfd_conf *conf = NULL; +static struct filter_conf *filter; + +enum listen_options { + LO_FAMILY = 0x000001, + LO_PORT = 0x000002, +}; + +static struct listen_opts { + char *ifx; + int family; + int proto; + in_port_t port; + uint32_t options; +} listen_opts; + +static void config_free(struct smtpfd_conf *); +static void create_listeners(struct listen_opts *); +static void config_listener(struct listener *, struct listen_opts *); +static int local(struct listen_opts *); +static int host_v4(struct listen_opts *); +static int host_v6(struct listen_opts *); +static int host_dns(struct listen_opts *); +static int interface(struct listen_opts *); +static int is_if_in_group(const char *, const char *); + +typedef struct { + union { + int64_t number; + char *string; + struct host *host; + } v; + int lineno; +} YYSTYPE; + +%} + +%token ERROR ARROW INCLUDE +%token LISTEN ON PORT INET4 INET6 LOCAL SOCKET +%token CHAIN FILTER +%token STRING +%token NUMBER +%type family_inet portno + +%% + +grammar : /* empty */ + | grammar '\n' + | grammar include '\n' + | grammar varset '\n' + | grammar main '\n' + | grammar error '\n' { file->errors++; } + ; + +include : INCLUDE STRING { + struct file *nfile; + + if ((nfile = pushfile($2, 0)) == NULL) { + yyerror("failed to include file %s", $2); + free($2); + YYERROR; + } + free($2); + + file = nfile; + lungetc('\n'); + } + ; + +varset : STRING '=' STRING { + if (symset($1, $3, 0) == -1) + fatal("cannot store variable"); + free($1); + free($3); + } + ; + +portno : STRING { + struct servent *servent; + servent = getservbyname($1, "tcp"); + if (servent == NULL) { + yyerror("invalid port: %s", $1); + free($1); + YYERROR; + } + free($1); + $$ = ntohs(servent->s_port); + } + | NUMBER { + if ($1 <= 0 || $1 >= (int)USHRT_MAX) { + yyerror("invalid port: %" PRId64, $1); + YYERROR; + } + $$ = $1; + } + ; + +family_inet : INET4 { $$ = AF_INET; } + | INET6 { $$ = AF_INET6; } + ; + +opt_listen : family_inet { + if (listen_opts.options & LO_FAMILY) { + yyerror("address family already specified"); + YYERROR; + } + listen_opts.options |= LO_FAMILY; + listen_opts.family = $1; + } + | PORT portno { + if (listen_opts.options & LO_PORT) { + yyerror("port already specified"); + YYERROR; + } + listen_opts.options |= LO_PORT; + listen_opts.port = htons($2); + } + ; + +listener : opt_listen listener + | /* empty */ { + create_listeners(&listen_opts); + } + ; + +filter_args : STRING { + struct filter_conf *f; + + if (filter->argc == SMTPFD_MAXFILTERARG) { + yyerror("too many args for filter %s", + filter->name); + YYERROR; + } + + if (filter->chain) { + TAILQ_FOREACH(f, &conf->filters, entry) + if (!strcmp(f->name, $1)) + break; + if (f == NULL) { + yyerror("unknown filter %s", $1); + YYERROR; + } + if (f == filter) { + yyerror("cannot chain %s with itself", + f->name); + YYERROR; + } + } + filter->argv[filter->argc++] = $1; + } filter_args + | /* empty */ + ; + +filter : FILTER STRING STRING { + TAILQ_FOREACH(filter, &conf->filters, entry) + if (!strcmp(filter->name, $2)) { + yyerror("filter %s already defined", + filter->name); + YYERROR; + } + filter = calloc(1, sizeof(*filter)); + if (filter == NULL) + fatal("calloc"); + filter->argv[filter->argc++] = $3; + filter->argv[filter->argc++] = $2; + filter->name = strdup($2); + if (filter->name == NULL) + fatal("strdup"); + TAILQ_INSERT_TAIL(&conf->filters, filter, entry); + } filter_args + ; + +chain : CHAIN STRING { + TAILQ_FOREACH(filter, &conf->filters, entry) + if (!strcmp(filter->name, $2)) { + yyerror("filter %s already defined", + filter->name); + YYERROR; + } + filter = calloc(1, sizeof(*filter)); + if (filter == NULL) + fatal("calloc"); + filter->chain = 1; + filter->name = $2; + TAILQ_INSERT_TAIL(&conf->filters, filter, entry); + } filter_args + ; + +main : LISTEN ON STRING { + memset(&listen_opts, 0, sizeof listen_opts); + listen_opts.ifx = $3; + listen_opts.family = AF_UNSPEC; + listen_opts.proto = PROTO_SMTPF; + listen_opts.port = htons(PORT_SMTPF); + } listener + | filter + | chain + ; +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +int +yyerror(const char *fmt, ...) +{ + va_list ap; + char *msg; + + file->errors++; + va_start(ap, fmt); + if (vasprintf(&msg, fmt, ap) == -1) + fatalx("yyerror vasprintf"); + va_end(ap); + log_warnx("%s:%d: %s", file->name, yylval.lineno, msg); + free(msg); + return (0); +} + +int +kw_cmp(const void *k, const void *e) +{ + return (strcmp(k, ((const struct keywords *)e)->k_name)); +} + +int +lookup(char *s) +{ + /* this has to be sorted always */ + static const struct keywords keywords[] = { + { "chain", CHAIN }, + { "filter", FILTER }, + { "include", INCLUDE }, + { "inet4", INET4 }, + { "inet6", INET6 }, + { "listen", LISTEN }, + { "on", ON }, + { "port", PORT }, + { "socket", SOCKET }, + }; + const struct keywords *p; + + p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]), + sizeof(keywords[0]), kw_cmp); + + if (p) + return (p->k_val); + else + return (STRING); +} + +#define MAXPUSHBACK 128 + +unsigned char *parsebuf; +int parseindex; +unsigned char pushback_buffer[MAXPUSHBACK]; +int pushback_index = 0; + +int +lgetc(int quotec) +{ + int c, next; + + if (parsebuf) { + /* Read character from the parsebuffer instead of input. */ + if (parseindex >= 0) { + c = parsebuf[parseindex++]; + if (c != '\0') + return (c); + parsebuf = NULL; + } else + parseindex++; + } + + if (pushback_index) + return (pushback_buffer[--pushback_index]); + + if (quotec) { + if ((c = getc(file->stream)) == EOF) { + yyerror("reached end of file while parsing " + "quoted string"); + if (file == topfile || popfile() == EOF) + return (EOF); + return (quotec); + } + return (c); + } + + while ((c = getc(file->stream)) == '\\') { + next = getc(file->stream); + if (next != '\n') { + c = next; + break; + } + yylval.lineno = file->lineno; + file->lineno++; + } + + while (c == EOF) { + if (file == topfile || popfile() == EOF) + return (EOF); + c = getc(file->stream); + } + return (c); +} + +int +lungetc(int c) +{ + if (c == EOF) + return (EOF); + if (parsebuf) { + parseindex--; + if (parseindex >= 0) + return (c); + } + if (pushback_index < MAXPUSHBACK-1) + return (pushback_buffer[pushback_index++] = c); + else + return (EOF); +} + +int +findeol(void) +{ + int c; + + parsebuf = NULL; + pushback_index = 0; + + /* skip to either EOF or the first real EOL */ + while (1) { + c = lgetc(0); + if (c == '\n') { + file->lineno++; + break; + } + if (c == EOF) + break; + } + return (ERROR); +} + +int +yylex(void) +{ + unsigned char buf[8096]; + unsigned char *p, *val; + int quotec, next, c; + int token; + +top: + p = buf; + while ((c = lgetc(0)) == ' ' || c == '\t') + ; /* nothing */ + + yylval.lineno = file->lineno; + if (c == '#') + while ((c = lgetc(0)) != '\n' && c != EOF) + ; /* nothing */ + if (c == '$' && parsebuf == NULL) { + while (1) { + if ((c = lgetc(0)) == EOF) + return (0); + + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + if (isalnum(c) || c == '_') { + *p++ = c; + continue; + } + *p = '\0'; + lungetc(c); + break; + } + val = symget(buf); + if (val == NULL) { + yyerror("macro '%s' not defined", buf); + return (findeol()); + } + parsebuf = val; + parseindex = 0; + goto top; + } + + switch (c) { + case '\'': + case '"': + quotec = c; + while (1) { + if ((c = lgetc(quotec)) == EOF) + return (0); + if (c == '\n') { + file->lineno++; + continue; + } else if (c == '\\') { + if ((next = lgetc(quotec)) == EOF) + return (0); + if (next == quotec || c == ' ' || c == '\t') + c = next; + else if (next == '\n') { + file->lineno++; + continue; + } else + lungetc(next); + } else if (c == quotec) { + *p = '\0'; + break; + } else if (c == '\0') { + yyerror("syntax error"); + return (findeol()); + } + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + *p++ = c; + } + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "yylex: strdup"); + return (STRING); + } + +#define allowed_to_end_number(x) \ + (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') + + if (c == '-' || isdigit(c)) { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && isdigit(c)); + lungetc(c); + if (p == buf + 1 && buf[0] == '-') + goto nodigits; + if (c == EOF || allowed_to_end_number(c)) { + const char *errstr = NULL; + + *p = '\0'; + yylval.v.number = strtonum(buf, LLONG_MIN, + LLONG_MAX, &errstr); + if (errstr) { + yyerror("\"%s\" invalid number: %s", + buf, errstr); + return (findeol()); + } + return (NUMBER); + } else { +nodigits: + while (p > buf + 1) + lungetc(*--p); + c = *--p; + if (c == '-') + return (c); + } + } + + if (c == '=') { + if ((c = lgetc(0)) != EOF && c == '>') + return (ARROW); + lungetc(c); + c = '='; + } + +#define allowed_in_string(x) \ + (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \ + x != '{' && x != '}' && x != '<' && x != '>' && \ + x != '!' && x != '=' && x != '#' && \ + x != ',')) + + if (isalnum(c) || c == ':' || c == '_') { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && (allowed_in_string(c))); + lungetc(c); + *p = '\0'; + if ((token = lookup(buf)) == STRING) + if ((yylval.v.string = strdup(buf)) == NULL) + err(1, "yylex: strdup"); + return (token); + } + if (c == '\n') { + yylval.lineno = file->lineno; + file->lineno++; + } + if (c == EOF) + return (0); + return (c); +} + +int +check_file_secrecy(int fd, const char *fname) +{ + struct stat st; + + if (fstat(fd, &st)) { + log_warn("warn: cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + log_warnx("warn: %s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) { + log_warnx("warn: %s: group/world readable/writeable", fname); + return (-1); + } + return (0); +} + +struct file * +pushfile(const char *name, int secret) +{ + struct file *nfile; + + if ((nfile = calloc(1, sizeof(struct file))) == NULL) { + log_warn("warn: malloc"); + return (NULL); + } + if ((nfile->name = strdup(name)) == NULL) { + log_warn("warn: malloc"); + free(nfile); + return (NULL); + } + if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { + log_warn("warn: %s", nfile->name); + free(nfile->name); + free(nfile); + return (NULL); + } else if (secret && + check_file_secrecy(fileno(nfile->stream), nfile->name)) { + fclose(nfile->stream); + free(nfile->name); + free(nfile); + return (NULL); + } + nfile->lineno = 1; + TAILQ_INSERT_TAIL(&files, nfile, entry); + return (nfile); +} + +int +popfile(void) +{ + struct file *prev; + + if ((prev = TAILQ_PREV(file, files, entry)) != NULL) + prev->errors += file->errors; + + TAILQ_REMOVE(&files, file, entry); + fclose(file->stream); + free(file->name); + free(file); + file = prev; + return (file ? 0 : EOF); +} + +struct smtpfd_conf * +parse_config(const char *filename, int verbose) +{ + struct sym *sym, *next; + + conf = calloc(1, sizeof(*conf)); + if (conf == NULL) + return NULL; + TAILQ_INIT(&conf->listeners); + TAILQ_INIT(&conf->filters); + + errors = 0; + + if ((file = pushfile(filename, 0)) == NULL) { + config_free(conf); + return NULL; + } + topfile = file; + + /* + * parse configuration + */ + setservent(1); + yyparse(); + errors = file->errors; + popfile(); + endservent(); + + /* Free macros and check which have not been used. */ + for (sym = TAILQ_FIRST(&symhead); sym != NULL; sym = next) { + next = TAILQ_NEXT(sym, entry); + if ((verbose) && !sym->used) + log_warnx("warning: macro '%s' not used\n", sym->nam); + if (!sym->persist) { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + + if (errors) { + config_free(conf); + return NULL; + } + + return conf; +} + +int +symset(const char *nam, const char *val, int persist) +{ + struct sym *sym; + + for (sym = TAILQ_FIRST(&symhead); sym && strcmp(nam, sym->nam); + sym = TAILQ_NEXT(sym, entry)) + ; /* nothing */ + + if (sym != NULL) { + if (sym->persist == 1) + return (0); + else { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + if ((sym = calloc(1, sizeof(*sym))) == NULL) + return (-1); + + sym->nam = strdup(nam); + if (sym->nam == NULL) { + free(sym); + return (-1); + } + sym->val = strdup(val); + if (sym->val == NULL) { + free(sym->nam); + free(sym); + return (-1); + } + sym->used = 0; + sym->persist = persist; + TAILQ_INSERT_TAIL(&symhead, sym, entry); + return (0); +} + +int +cmdline_symset(char *s) +{ + char *sym, *val; + int ret; + size_t len; + + if ((val = strrchr(s, '=')) == NULL) + return (-1); + + len = strlen(s) - strlen(val) + 1; + if ((sym = malloc(len)) == NULL) + errx(1, "cmdline_symset: malloc"); + + (void)strlcpy(sym, s, len); + + ret = symset(sym, val + 1, 1); + free(sym); + + return (ret); +} + +char * +symget(const char *nam) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) + if (strcmp(nam, sym->nam) == 0) { + sym->used = 1; + return (sym->val); + } + return (NULL); +} + +static void +config_free(struct smtpfd_conf *c) +{ + struct listener *l; + struct filter_conf *f; + + while ((l = TAILQ_FIRST(&c->listeners))) { + TAILQ_REMOVE(&c->listeners, l, entry); + free(l); + } + while ((f = TAILQ_FIRST(&c->filters))) { + TAILQ_REMOVE(&c->filters, f, entry); + free(f->name); + free(f); + } + + free(c); +} + +static void +create_listeners(struct listen_opts *lo) +{ + if (local(lo)) + return; + if (interface(lo)) + return; + if (host_v4(lo)) + return; + if (host_v6(lo)) + return; + if (host_dns(lo)) + return; + + errx(1, "invalid virtual ip or interface: %s", lo->ifx); +} + +static void +config_listener(struct listener *l, struct listen_opts *lo) +{ + l->sock = -1; + l->proto = lo->proto; + + TAILQ_INSERT_TAIL(&conf->listeners, l, entry); +} + +static int +local(struct listen_opts *lo) +{ + struct sockaddr_un *sun; + struct listener *h; + + if (lo->family != AF_UNSPEC && lo->family != AF_LOCAL) + return 0; + + if (lo->ifx[0] != '/') + return 0; + + h = calloc(1, sizeof(*h)); + sun = (struct sockaddr_un *)&h->ss; + sun->sun_len = sizeof(*sun); + sun->sun_family = AF_LOCAL; + if (strlcpy(sun->sun_path, lo->ifx, sizeof(sun->sun_path)) + >= sizeof(sun->sun_path)) + fatalx("path too long"); + + config_listener(h, lo); + + return (1); +} + +static int +host_v4(struct listen_opts *lo) +{ + struct in_addr ina; + struct sockaddr_in *sain; + struct listener *h; + + if (lo->family != AF_UNSPEC && lo->family != AF_INET) + return (0); + + memset(&ina, 0, sizeof(ina)); + if (inet_pton(AF_INET, lo->ifx, &ina) != 1) + return (0); + + h = calloc(1, sizeof(*h)); + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_family = AF_INET; + sain->sin_addr.s_addr = ina.s_addr; + sain->sin_port = lo->port; + + config_listener(h, lo); + + return (1); +} + +static int +host_v6(struct listen_opts *lo) +{ + struct in6_addr ina6; + struct sockaddr_in6 *sin6; + struct listener *h; + + if (lo->family != AF_UNSPEC && lo->family != AF_INET6) + return (0); + + memset(&ina6, 0, sizeof(ina6)); + if (inet_pton(AF_INET6, lo->ifx, &ina6) != 1) + return (0); + + h = calloc(1, sizeof(*h)); + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = lo->port; + memcpy(&sin6->sin6_addr, &ina6, sizeof(ina6)); + + config_listener(h, lo); + + return (1); +} + +static int +host_dns(struct listen_opts *lo) +{ + struct addrinfo hints, *res0, *res; + int error, cnt = 0; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct listener *h; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = lo->family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_ADDRCONFIG; + error = getaddrinfo(lo->ifx, NULL, &hints, &res0); + if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME) + return (0); + if (error) { + log_warnx("warn: host_dns: could not parse \"%s\": %s", lo->ifx, + gai_strerror(error)); + return (-1); + } + + for (res = res0; res; res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + h = calloc(1, sizeof(*h)); + h->ss.ss_family = res->ai_family; + if (res->ai_family == AF_INET) { + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_addr.s_addr = ((struct sockaddr_in *) + res->ai_addr)->sin_addr.s_addr; + sain->sin_port = lo->port; + } else { + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *) + res->ai_addr)->sin6_addr, sizeof(struct in6_addr)); + sin6->sin6_port = lo->port; + } + + config_listener(h, lo); + + cnt++; + } + + freeaddrinfo(res0); + return (cnt); +} + +static int +interface(struct listen_opts *lo) +{ + struct ifaddrs *ifap, *p; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct listener *h; + int ret = 0; + + if (getifaddrs(&ifap) == -1) + fatal("getifaddrs"); + + for (p = ifap; p != NULL; p = p->ifa_next) { + if (p->ifa_addr == NULL) + continue; + if (strcmp(p->ifa_name, lo->ifx) != 0 && + !is_if_in_group(p->ifa_name, lo->ifx)) + continue; + if (lo->family != AF_UNSPEC && lo->family != p->ifa_addr->sa_family) + continue; + + h = calloc(1, sizeof(*h)); + + switch (p->ifa_addr->sa_family) { + case AF_INET: + sain = (struct sockaddr_in *)&h->ss; + *sain = *(struct sockaddr_in *)p->ifa_addr; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_port = lo->port; + break; + + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&h->ss; + *sin6 = *(struct sockaddr_in6 *)p->ifa_addr; + sin6->sin6_len = sizeof(struct sockaddr_in6); + sin6->sin6_port = lo->port; + break; + + default: + free(h); + continue; + } + + config_listener(h, lo); + ret = 1; + } + + freeifaddrs(ifap); + return ret; +} + +static int +is_if_in_group(const char *ifname, const char *groupname) +{ + unsigned int len; + struct ifgroupreq ifgr; + struct ifg_req *ifg; + int s; + int ret = 0; + + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + err(1, "socket"); + + memset(&ifgr, 0, sizeof(ifgr)); + if (strlcpy(ifgr.ifgr_name, ifname, IFNAMSIZ) >= IFNAMSIZ) + errx(1, "interface name too large"); + + if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) { + if (errno == EINVAL || errno == ENOTTY) + goto end; + err(1, "SIOCGIFGROUP"); + } + + len = ifgr.ifgr_len; + ifgr.ifgr_groups = calloc(len/sizeof(struct ifg_req), + sizeof(struct ifg_req)); + if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) + err(1, "SIOCGIFGROUP"); + + ifg = ifgr.ifgr_groups; + for (; ifg && len >= sizeof(struct ifg_req); ifg++) { + len -= sizeof(struct ifg_req); + if (strcmp(ifg->ifgrq_group, groupname) == 0) { + ret = 1; + break; + } + } + free(ifgr.ifgr_groups); + +end: + close(s); + return ret; +} diff --git a/extras/filters/smtpfd/proc.c b/extras/filters/smtpfd/proc.c new file mode 100644 index 0000000..3c1bfbe --- /dev/null +++ b/extras/filters/smtpfd/proc.c @@ -0,0 +1,508 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "proc.h" + +struct imsgproc { + TAILQ_ENTRY(imsgproc) tqe; + int type; + int instance; + char *title; + pid_t pid; + void *arg; + void (*cb)(struct imsgproc *, struct imsg *, void *); + struct imsgbuf imsgbuf; + short events; + struct event ev; + + struct { + const uint8_t *pos; + const uint8_t *end; + } m_in; + + struct m_out { + char *buf; + size_t alloc; + size_t pos; + uint32_t type; + uint32_t peerid; + pid_t pid; + int fd; + } m_out; +}; + +static struct imsgproc *proc_new(int); +static void proc_setsock(struct imsgproc *, int); +static void proc_callback(struct imsgproc *, struct imsg *); +static void proc_dispatch(int, short, void *); +static void proc_event_add(struct imsgproc *); + +static TAILQ_HEAD(, imsgproc) procs = TAILQ_HEAD_INITIALIZER(procs); + +pid_t +proc_getpid(struct imsgproc *p) +{ + return p->pid; +} + +int +proc_gettype(struct imsgproc *p) +{ + return p->type; +} + +int +proc_getinstance(struct imsgproc *p) +{ + return p->instance; +} + +const char * +proc_gettitle(struct imsgproc *p) +{ + return p->title; +} + +struct imsgproc * +proc_bypid(pid_t pid) +{ + struct imsgproc *p; + + TAILQ_FOREACH(p, &procs, tqe) + if (pid == p->pid) + return p; + + return NULL; +} + +struct imsgproc * +proc_exec(int type, char **argv) +{ + struct imsgproc *p; + int sp[2]; + pid_t pid; + + p = proc_new(type); + if (p == NULL) { + log_warn("%s: proc_new", __func__); + return NULL; + } + + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK, PF_UNSPEC, sp) == -1) { + log_warn("%s: socketpair", __func__); + proc_free(p); + return NULL; + } + + switch (pid = fork()) { + case -1: + log_warn("%s: fork", __func__); + close(sp[0]); + close(sp[1]); + proc_free(p); + return NULL; + case 0: + break; + default: + close(sp[0]); + p->pid = pid; + proc_setsock(p, sp[1]); + return p; + } + + if (dup2(sp[0], 3) == -1) + fatal("%s: dup2", __func__); + + if (closefrom(4) == -1) + fatal("%s: closefrom", __func__); + + execvp(argv[0], argv); + fatal("%s: execvp: %s", __func__, argv[0]); +} + +struct imsgproc * +proc_attach(int type, int fd) +{ + struct imsgproc *p; + + p = proc_new(type); + if (p == NULL) + return NULL; + + proc_setsock(p, fd); + return p; +} + +void +proc_settitle(struct imsgproc *p, const char *title) +{ + free(p->title); + if (title) { + p->title = strdup(title); + if (p->title == NULL) + log_warn("%s: strdup", __func__); + } + else + p->title = NULL; +} + +void +proc_setpid(struct imsgproc *p, pid_t pid) +{ + p->pid = pid; +} + +void +proc_setcallback(struct imsgproc *p, + void(*cb)(struct imsgproc *, struct imsg *, void *), void *arg) +{ + p->cb = cb; + p->arg = arg; +} + +void +proc_enable(struct imsgproc *p) +{ + proc_event_add(p); +} + +void +proc_free(struct imsgproc *p) +{ + if (p == NULL) + return; + + TAILQ_REMOVE(&procs, p, tqe); + + if (event_initialized(&p->ev)) + event_del(&p->ev); + close(p->imsgbuf.fd); + imsg_clear(&p->imsgbuf); + free(p->title); + free(p); +} + +static struct imsgproc * +proc_new(int type) +{ + struct imsgproc *p; + + p = calloc(1, sizeof(*p)); + if (p == NULL) + return NULL; + + p->type = type; + p->instance = -1; + p->pid = -1; + imsg_init(&p->imsgbuf, -1); + + TAILQ_INSERT_TAIL(&procs, p, tqe); + + return p; +} + +static void +proc_setsock(struct imsgproc *p, int sock) +{ + p->imsgbuf.fd = sock; + p->imsgbuf.w.fd = sock; +} + +static void +proc_event_add(struct imsgproc *p) +{ + short events; + + events = EV_READ; + if (p->imsgbuf.w.queued) + events |= EV_WRITE; + + if (p->events) + event_del(&p->ev); + + p->events = events; + if (events) { + event_set(&p->ev, p->imsgbuf.fd, events, proc_dispatch, p); + event_add(&p->ev, NULL); + } +} + +static void +proc_callback(struct imsgproc *p, struct imsg *imsg) +{ + if (imsg != NULL) { + p->m_in.pos = imsg->data; + p->m_in.end = p->m_in.pos + (imsg->hdr.len - sizeof(imsg->hdr)); + } + else { + p->m_in.pos = NULL; + p->m_in.end = NULL; + } + + p->cb(p, imsg, p->arg); +} + +static void +proc_dispatch(int fd, short event, void *arg) +{ + struct imsgproc *p = arg; + struct imsg imsg; + ssize_t n; + + p->events = 0; + + if (event & EV_READ) { + n = imsg_read(&p->imsgbuf); + switch (n) { + case -1: + if (errno == EAGAIN) + break; + log_warn("%s: imsg_read", __func__); + proc_callback(p, NULL); + return; + case 0: + /* This pipe is dead. */ + proc_callback(p, NULL); + return; + default: + break; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&p->imsgbuf.w); + switch (n) { + case -1: + if (errno == EAGAIN) + break; + log_warn("%s: msgbuf_write", __func__); + proc_callback(p, NULL); + return; + case 0: + /* This pipe is dead. */ + proc_callback(p, NULL); + return; + default: + break; + } + } + + for (;;) { + if ((n = imsg_get(&p->imsgbuf, &imsg)) == -1) { + log_warn("%s: imsg_get", __func__); + proc_callback(p, NULL); + return; + } + if (n == 0) + break; + + proc_callback(p, &imsg); + imsg_free(&imsg); + } + + proc_event_add(p); +} + +void +m_compose(struct imsgproc *p, uint32_t type, uint32_t peerid, pid_t pid, int fd, + const void *data, size_t len) +{ + if (imsg_compose(&p->imsgbuf, type, peerid, pid, fd, data, len) == -1) + fatal("%s: imsg_compose", __func__); + + proc_event_add(p); +} + +void +m_create(struct imsgproc *p, uint32_t type, uint32_t peerid, pid_t pid, int fd) +{ + p->m_out.pos = 0; + p->m_out.type = type; + p->m_out.peerid = peerid; + p->m_out.pid = pid; + p->m_out.fd = fd; +} + +void +m_close(struct imsgproc *p) +{ + if (imsg_compose(&p->imsgbuf, p->m_out.type, p->m_out.peerid, + p->m_out.pid, p->m_out.fd, p->m_out.buf, p->m_out.pos) == -1) + fatal("%s: imsg_compose", __func__); + + proc_event_add(p); +} + +void +m_add(struct imsgproc *p, const void *data, size_t len) +{ + size_t alloc; + void *tmp; + + if (p->m_out.pos + len + IMSG_HEADER_SIZE > MAX_IMSGSIZE) + fatalx("%s: message too large", __func__); + + alloc = p->m_out.alloc ? p->m_out.alloc : 128; + while (p->m_out.pos + len > alloc) + alloc *= 2; + if (alloc != p->m_out.alloc) { + tmp = recallocarray(p->m_out.buf, p->m_out.alloc, alloc, 1); + if (tmp == NULL) + fatal("%s: reallocarray", __func__); + p->m_out.alloc = alloc; + p->m_out.buf = tmp; + } + + memmove(p->m_out.buf + p->m_out.pos, data, len); + p->m_out.pos += len; +} + +void +m_add_int(struct imsgproc *p, int v) +{ + m_add(p, &v, sizeof(v)); +}; + +void +m_add_u32(struct imsgproc *p, uint32_t v) +{ + m_add(p, &v, sizeof(v)); +}; + +void +m_add_u64(struct imsgproc *p, uint64_t v) +{ + m_add(p, &v, sizeof(v)); +} + +void +m_add_size(struct imsgproc *p, size_t v) +{ + m_add(p, &v, sizeof(v)); +} + +void +m_add_time(struct imsgproc *p, time_t v) +{ + m_add(p, &v, sizeof(v)); +} + +void +m_add_string(struct imsgproc *p, const char *str) +{ + m_add(p, str, strlen(str) + 1); +} + +void +m_add_sockaddr(struct imsgproc *p, const struct sockaddr *sa) +{ + m_add_size(p, sa->sa_len); + m_add(p, sa, sa->sa_len); +} + +void +m_end(struct imsgproc *p) +{ + if (p->m_in.pos != p->m_in.end) + fatal("%s: %zi bytes left", __func__, + p->m_in.end - p->m_in.pos); +} + +int +m_is_eom(struct imsgproc *p) +{ + return (p->m_in.pos == p->m_in.end); +} + +void +m_get(struct imsgproc *p, void *dst, size_t sz) +{ + if (sz > MAX_IMSGSIZE || + p->m_in.end - p->m_in.pos < (ssize_t)sz ) + fatalx("%s: %zu bytes requested, %zi left", __func__, sz, + p->m_in.end - p->m_in.pos); + + memmove(dst, p->m_in.pos, sz); + p->m_in.pos += sz; +} + +void +m_get_int(struct imsgproc *p, int *dst) +{ + m_get(p, dst, sizeof(*dst)); +} + +void +m_get_u32(struct imsgproc *p, uint32_t *dst) +{ + m_get(p, dst, sizeof(*dst)); +} + +void +m_get_u64(struct imsgproc *p, uint64_t *dst) +{ + m_get(p, dst, sizeof(*dst)); +} + +void +m_get_size(struct imsgproc *p, size_t *dst) +{ + m_get(p, dst, sizeof(*dst)); +} + +void +m_get_time(struct imsgproc *p, time_t *dst) +{ + m_get(p, dst, sizeof(*dst)); +} + +void +m_get_string(struct imsgproc *p, const char **dst) +{ + char *end; + + if (p->m_in.pos >= p->m_in.end) + fatalx("%s: no data left", __func__); + + end = memchr(p->m_in.pos, 0, p->m_in.end - p->m_in.pos); + if (end == NULL) + fatalx("%s: unterminated string", __func__); + + *dst = p->m_in.pos; + p->m_in.pos = end + 1; +} + +void +m_get_sockaddr(struct imsgproc *p, struct sockaddr *dst) +{ + size_t len; + + m_get_size(p, &len); + m_get(p, dst, len); +} diff --git a/extras/filters/smtpfd/proc.h b/extras/filters/smtpfd/proc.h new file mode 100644 index 0000000..bf0f19d --- /dev/null +++ b/extras/filters/smtpfd/proc.h @@ -0,0 +1,57 @@ +/* $OpenBSD: log.h,v 1.7 2017/01/09 14:49:22 reyk Exp $ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +struct imsgproc; + +struct imsgproc *proc_bypid(pid_t); +struct imsgproc *proc_exec(int, char **); +struct imsgproc *proc_attach(int, int); +void proc_enable(struct imsgproc *); +void proc_free(struct imsgproc *); +pid_t proc_getpid(struct imsgproc *); +int proc_gettype(struct imsgproc *); +int proc_getinstance(struct imsgproc *); +const char *proc_gettitle(struct imsgproc *); +void proc_setpid(struct imsgproc *, pid_t); +void proc_settitle(struct imsgproc *, const char *); +void proc_setinstance(struct imsgproc *, int); +void proc_setcallback(struct imsgproc *, + void(*)(struct imsgproc *, struct imsg *, void *), void *); + +void m_compose(struct imsgproc *, uint32_t, uint32_t, pid_t, int, const void *, + size_t); +void m_create(struct imsgproc *, uint32_t, uint32_t, pid_t, int); +void m_close(struct imsgproc *); +void m_add(struct imsgproc *, const void *, size_t); +void m_add_int(struct imsgproc *, int); +void m_add_u32(struct imsgproc *, uint32_t); +void m_add_u64(struct imsgproc *, uint64_t); +void m_add_size(struct imsgproc *, size_t); +void m_add_time(struct imsgproc *, time_t); +void m_add_string(struct imsgproc *, const char *); +void m_add_sockaddr(struct imsgproc *, const struct sockaddr *); +void m_end(struct imsgproc *); +int m_is_eom(struct imsgproc *); +void m_get(struct imsgproc *, void *, size_t); +void m_get_int(struct imsgproc *, int *); +void m_get_u32(struct imsgproc *, uint32_t *); +void m_get_u64(struct imsgproc *, uint64_t *); +void m_get_size(struct imsgproc *, size_t *); +void m_get_time(struct imsgproc *, time_t *); +void m_get_string(struct imsgproc *, const char **); +void m_get_sockaddr(struct imsgproc *, struct sockaddr *); diff --git a/extras/filters/smtpfd/resolver.c b/extras/filters/smtpfd/resolver.c new file mode 100644 index 0000000..c09ce8e --- /dev/null +++ b/extras/filters/smtpfd/resolver.c @@ -0,0 +1,355 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "smtpfd.h" + +#include "log.h" +#include "proc.h" + +struct request { + SPLAY_ENTRY(request) entry; + uint32_t id; + void (*cb_ai)(void *, int, struct addrinfo *); + void (*cb_ni)(void *, int, const char *, const char *); + void *arg; + struct addrinfo *ai; +}; + +struct session { + uint32_t reqid; + struct imsgproc *proc; + char *host; + char *serv; +}; + +SPLAY_HEAD(reqtree, request); + +static void resolver_init(void); +static void resolver_getaddrinfo_cb(struct asr_result *, void *); +static void resolver_getnameinfo_cb(struct asr_result *, void *); + +static int request_cmp(struct request *, struct request *); +SPLAY_PROTOTYPE(reqtree, request, entry, request_cmp); + +static struct reqtree reqs; + +void +resolver_getaddrinfo(const char *hostname, const char *servname, + const struct addrinfo *hints, void (*cb)(void *, int, struct addrinfo *), + void *arg) +{ + struct request *req; + + resolver_init(); + + req = calloc(1, sizeof(*req)); + if (req == NULL) { + cb(arg, EAI_MEMORY, NULL); + return; + } + + while (req->id == 0 || SPLAY_FIND(reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_ai = cb; + req->arg = arg; + + SPLAY_INSERT(reqtree, &reqs, req); + + m_create(p_engine, IMSG_RES_GETADDRINFO, req->id, 0, -1); + m_add_int(p_engine, hints ? hints->ai_flags : 0); + m_add_int(p_engine, hints ? hints->ai_family : 0); + m_add_int(p_engine, hints ? hints->ai_socktype : 0); + m_add_int(p_engine, hints ? hints->ai_protocol : 0); + m_add_string(p_engine, hostname); + m_add_string(p_engine, servname ? servname : ""); + m_close(p_engine); +} + +void +resolver_getnameinfo(const struct sockaddr *sa, int flags, + void(*cb)(void *, int, const char *, const char *), void *arg) +{ + struct request *req; + + resolver_init(); + + req = calloc(1, sizeof(*req)); + if (req == NULL) { + cb(arg, EAI_MEMORY, NULL, NULL); + return; + } + + while (req->id == 0 || SPLAY_FIND(reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_ni = cb; + req->arg = arg; + + m_create(p_engine, IMSG_RES_GETNAMEINFO, req->id, 0, -1); + m_add_sockaddr(p_engine, sa); + m_add_int(p_engine, flags); + m_close(p_engine); +} + +void +resolver_dispatch_request(struct imsgproc *proc, struct imsg *imsg) +{ + const char *hostname, *servname; + struct session *s; + struct asr_query *q; + struct addrinfo hints; + struct sockaddr_storage ss; + struct sockaddr *sa; + uint32_t reqid; + int flags, save_errno; + + reqid = imsg->hdr.peerid; + + switch (imsg->hdr.type) { + + case IMSG_RES_GETADDRINFO: + servname = NULL; + memset(&hints, 0 , sizeof(hints)); + m_get_int(proc, &hints.ai_flags); + m_get_int(proc, &hints.ai_family); + m_get_int(proc, &hints.ai_socktype); + m_get_int(proc, &hints.ai_protocol); + m_get_string(proc, &hostname); + if (!m_is_eom(proc)) + m_get_string(proc, &servname); + m_end(proc); + + s = NULL; + q = NULL; + if ((s = calloc(1, sizeof(*s))) && + (q = getaddrinfo_async(hostname, servname, &hints, NULL)) && + (event_asr_run(q, resolver_getaddrinfo_cb, s))) { + s->reqid = reqid; + s->proc = proc; + break; + } + save_errno = errno; + + if (q) + asr_abort(q); + if (s) + free(s); + + m_create(proc, IMSG_RES_GETADDRINFO_END, reqid, 0, -1); + m_add_int(proc, EAI_SYSTEM); + m_add_int(proc, save_errno); + m_close(proc); + break; + + case IMSG_RES_GETNAMEINFO: + sa = (struct sockaddr*)&ss; + m_get_sockaddr(proc, sa); + m_get_int(proc, &flags); + m_end(proc); + + s = NULL; + q = NULL; + if ((s = calloc(1, sizeof(*s))) && + (s->host = malloc(NI_MAXHOST)) && + (s->serv = malloc(NI_MAXSERV)) && + (q = getnameinfo_async(sa, sa->sa_len, s->host, NI_MAXHOST, + s->serv, NI_MAXSERV, flags, NULL)) && + (event_asr_run(q, resolver_getnameinfo_cb, s))) { + s->reqid = reqid; + s->proc = proc; + break; + } + save_errno = errno; + + if (q) + asr_abort(q); + if (s) { + free(s->host); + free(s->serv); + free(s); + } + + m_create(proc, IMSG_RES_GETNAMEINFO, reqid, 0, -1); + m_add_int(proc, EAI_SYSTEM); + m_add_int(proc, save_errno); + m_add_string(proc, ""); + m_add_string(proc, ""); + m_close(proc); + break; + + default: + fatalx("%s: %s", __func__, log_fmt_imsgtype(imsg->hdr.type)); + } +} + +void +resolver_dispatch_result(struct imsgproc *proc, struct imsg *imsg) +{ + struct request key, *req; + struct sockaddr_storage ss; + struct addrinfo *ai; + const char *cname, *host, *serv; + int gai_errno; + + key.id = imsg->hdr.peerid; + req = SPLAY_FIND(reqtree, &reqs, &key); + if (req == NULL) + fatalx("%s: unknown request %08x", __func__, imsg->hdr.peerid); + + switch (imsg->hdr.type) { + + case IMSG_RES_GETADDRINFO: + ai = calloc(1, sizeof(*ai)); + if (ai == NULL) { + log_warn("%s: calloc", __func__); + break; + } + m_get_int(proc, &ai->ai_flags); + m_get_int(proc, &ai->ai_family); + m_get_int(proc, &ai->ai_socktype); + m_get_int(proc, &ai->ai_protocol); + m_get_sockaddr(proc, (struct sockaddr *)&ss); + m_get_string(proc, &cname); + m_end(proc); + + ai->ai_addr = malloc(ss.ss_len); + if (ai->ai_addr == NULL) { + log_warn("%s: malloc", __func__); + free(ai); + break; + } + + memmove(ai->ai_addr, &ss, ss.ss_len); + + if (cname[0]) { + ai->ai_canonname = strdup(cname); + if (ai->ai_canonname == NULL) { + log_warn("%s: strdup", __func__); + free(ai->ai_addr); + free(ai); + break; + } + } + + ai->ai_next = req->ai; + req->ai = ai; + break; + + case IMSG_RES_GETADDRINFO_END: + m_get_int(proc, &gai_errno); + m_get_int(proc, &errno); + m_end(proc); + + SPLAY_REMOVE(reqtree, &reqs, req); + req->cb_ai(req->arg, gai_errno, req->ai); + free(req); + break; + + case IMSG_RES_GETNAMEINFO: + m_get_int(proc, &gai_errno); + m_get_int(proc, &errno); + m_get_string(proc, &host); + m_get_string(proc, &serv); + m_end(proc); + + SPLAY_REMOVE(reqtree, &reqs, req); + req->cb_ni(req->arg, gai_errno, host[0] ? host : NULL, + serv[0] ? serv : NULL); + free(req); + break; + } +} + +static void +resolver_init(void) +{ + static int init = 0; + + if (init == 0) { + SPLAY_INIT(&reqs); + init = 1; + } +} + +static void +resolver_getaddrinfo_cb(struct asr_result *ar, void *arg) +{ + struct session *s = arg; + struct addrinfo *ai; + + for (ai = ar->ar_addrinfo; ai; ai = ai->ai_next) { + m_create(s->proc, IMSG_RES_GETADDRINFO, s->reqid, 0, -1); + m_add_int(s->proc, ai->ai_flags); + m_add_int(s->proc, ai->ai_family); + m_add_int(s->proc, ai->ai_socktype); + m_add_int(s->proc, ai->ai_protocol); + m_add_sockaddr(s->proc, ai->ai_addr); + m_add_string(s->proc, ai->ai_canonname ? + ai->ai_canonname : ""); + m_close(s->proc); + } + + m_create(s->proc, IMSG_RES_GETADDRINFO_END, s->reqid, 0, -1); + m_add_int(s->proc, ar->ar_gai_errno); + m_add_int(s->proc, ar->ar_errno); + m_close(s->proc); + + freeaddrinfo(ar->ar_addrinfo); + free(s); +} + +static void +resolver_getnameinfo_cb(struct asr_result *ar, void *arg) +{ + struct session *s = arg; + + m_create(s->proc, IMSG_RES_GETNAMEINFO, s->reqid, 0, -1); + m_add_int(s->proc, ar->ar_gai_errno); + m_add_int(s->proc, ar->ar_errno); + m_add_string(s->proc, s->host ? s->host : ""); + m_add_string(s->proc, s->serv ? s->serv : ""); + m_close(s->proc); + + free(s->host); + free(s->serv); + free(s); +} + +static int +request_cmp(struct request *a, struct request *b) +{ + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + return (0); +} + +SPLAY_GENERATE(reqtree, request, entry, request_cmp); diff --git a/extras/filters/smtpfd/smtpfd.8 b/extras/filters/smtpfd/smtpfd.8 new file mode 100644 index 0000000..df783f4 --- /dev/null +++ b/extras/filters/smtpfd/smtpfd.8 @@ -0,0 +1,98 @@ +.\" $OpenBSD$ +.\" +.\" Copyright (c) 2016 Kenneth R Westerback +.\" +.\" 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: July 27 2015 $ +.Dt SMTPFD 8 +.Os +.Sh NAME +.Nm smtpfd +.Nd SMTP filter daemon +.Sh SYNOPSIS +.Nm +.Op Fl dnv +.Op Fl f Ar file +.Op Fl s Ar socket +.Sh DESCRIPTION +The +.Nm +daemon is an SMTP filter server. It is typically used by +.Xr smtpd 8 +to provide custom filtering on SMTP sessions. +.Pp +.Nm +is usually started at boot time, and can be enabled by +setting the following in +.Pa /etc/rc.conf.local : +.Pp +.Dl smtpfd_flags=\&"\&" +.Pp +See +.Xr rc 8 +and +.Xr rc.conf 8 +for more information on the boot process +and enabling daemons. +.Pp +A running +.Nm +can be controlled with the +.Xr smtpfctl 8 +utility. +.Pp +The options are as follows: +.Bl -tag -width Dssmacro=value +.It Fl D Ar macro Ns = Ns Ar value +Set a +.Ar macro +to a +.Ar value . +Macros can be referenced in the configuration files. +.It Fl d +Do not daemonize. +If this option is specified, +.Nm +will run in the foreground and log to +.Em stderr . +.It Fl f Ar file +Specify an alternative configuration file. +.It Fl n +Configtest mode. +Only check the configuration file for validity. +.It Fl s Ar socket +Use an alternate location for the default control socket. +.It Fl v +Produce more verbose output. +.El +.Sh FILES +.Bl -tag -width "/var/run/smtpfd.sockXX" -compact +.It Pa /etc/mail/smtpfd.conf +Default +.Nm +configuration file. +.It Pa /var/run/smtpfd.sock +.Ux Ns -domain +socket used for communication with +.Xr newctl 8 . +.El +.Sh SEE ALSO +.Xr smtpfd.conf 5 , +.Xr smtpfctl 8 , +.Xr smtpd 8 +.Sh HISTORY +The +.Nm +program first appeared in +.Ox X.Y . diff --git a/extras/filters/smtpfd/smtpfd.c b/extras/filters/smtpfd/smtpfd.c new file mode 100644 index 0000000..03a89fe --- /dev/null +++ b/extras/filters/smtpfd/smtpfd.c @@ -0,0 +1,432 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "smtpfd.h" + +#include "log.h" +#include "proc.h" + +struct smtpfd_conf *env; +struct imsgproc *p_control; +struct imsgproc *p_engine; +struct imsgproc *p_frontend; +struct imsgproc *p_priv; + +static void priv_dispatch_control(struct imsgproc *, struct imsg *, void *); +static void priv_dispatch_engine(struct imsgproc *, struct imsg *, void *); +static void priv_dispatch_frontend(struct imsgproc *, struct imsg *, void *); +static void priv_open_listener(struct listener *); +static void priv_open_filter(struct filter_conf *); +static void priv_send_config(void); +static void priv_sighandler(int, short, void *); +static void priv_shutdown(void); + +static char **saved_argv; +static int saved_argc; + +static void +usage(void) +{ + extern char *__progname; + + fprintf(stderr, "usage: %s [-dnv] [-D macro=value] [-f file]\n", + __progname); + exit(1); +} + +int +main(int argc, char **argv) +{ + struct listener *l; + struct filter_conf *f; + struct event evt_sigchld, evt_sigint, evt_sigterm, evt_sighup; + const char *conffile = SMTPFD_CONFIG, *reexec = NULL; + int sp[2], ch, debug = 0, nflag = 0, verbose = 1; + + saved_argv = argv; + saved_argc = argc; + + log_init(1, LOG_LPR); + log_setverbose(0); + + while ((ch = getopt(argc, argv, "D:df:nvX:")) != -1) { + switch (ch) { + case 'D': + if (cmdline_symset(optarg) < 0) + log_warnx("could not parse macro definition %s", + optarg); + break; + case 'd': + debug = 1; + break; + case 'f': + conffile = optarg; + break; + case 'n': + nflag = 1; + break; + case 'v': + verbose++; + break; + case 'X': + reexec = optarg; + break; + default: + usage(); + } + } + + argv += optind; + argc -= optind; + + if (argc || *argv) + usage(); + + if (reexec) { + if (!strcmp(reexec, "control")) + control(debug, verbose); + if (!strcmp(reexec, "engine")) + engine(debug, verbose); + if (!strcmp(reexec, "frontend")) + frontend(debug, verbose); + fatalx("unknown process %s", reexec); + } + + /* Parse config file. */ + env = parse_config(conffile, verbose); + if (env == NULL) + exit(1); + + if (nflag) { + fprintf(stderr, "configuration OK\n"); + exit(0); + } + + /* Check for root privileges. */ + if (geteuid()) + fatalx("need root privileges"); + + /* Check for assigned daemon user. */ + if (getpwnam(SMTPFD_USER) == NULL) + fatalx("unknown user %s", SMTPFD_USER); + + log_init(debug, LOG_DAEMON); + log_setverbose(verbose); + log_procinit("priv"); + setproctitle("priv"); + + if (!debug) + if (daemon(1, 0) == -1) + fatal("daemon"); + + log_info("startup"); + + TAILQ_FOREACH(l, &env->listeners, entry) + priv_open_listener(l); + TAILQ_FOREACH(f, &env->filters, entry) + priv_open_filter(f); + + event_init(); + + signal_set(&evt_sigint, SIGINT, priv_sighandler, NULL); + signal_add(&evt_sigint, NULL); + signal_set(&evt_sigterm, SIGTERM, priv_sighandler, NULL); + signal_add(&evt_sigterm, NULL); + signal_set(&evt_sigchld, SIGCHLD, priv_sighandler, NULL); + signal_add(&evt_sigchld, NULL); + signal_set(&evt_sighup, SIGHUP, priv_sighandler, NULL); + signal_add(&evt_sighup, NULL); + signal(SIGPIPE, SIG_IGN); + + /* Fork and exec unpriviledged processes. */ + argv = calloc(saved_argc + 3, sizeof(*argv)); + if (argv == NULL) + fatal("calloc"); + for (argc = 0; argc < saved_argc; argc++) + argv[argc] = saved_argv[argc]; + argv[argc++] = "-X"; + argv[argc++] = ""; + argv[argc++] = NULL; + + argv[argc - 2] = "control"; + p_control = proc_exec(PROC_CONTROL, argv); + proc_setcallback(p_control, priv_dispatch_control, NULL); + proc_enable(p_control); + + argv[argc - 2] = "engine"; + p_engine = proc_exec(PROC_ENGINE, argv); + proc_setcallback(p_engine, priv_dispatch_engine, NULL); + proc_enable(p_engine); + + argv[argc - 2] = "frontend"; + p_frontend = proc_exec(PROC_FRONTEND, argv); + proc_setcallback(p_frontend, priv_dispatch_frontend, NULL); + proc_enable(p_frontend); + + /* Connect processes. */ + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK, PF_UNSPEC, sp) == -1) + fatal("socketpair"); + m_compose(p_engine, IMSG_SOCK_FRONTEND, 0, 0, sp[1], NULL, 0); + m_compose(p_frontend, IMSG_SOCK_ENGINE, 0, 0, sp[0], NULL, 0); + + priv_send_config(); + + if (pledge("stdio sendfd proc", NULL) == -1) + fatal("pledge"); + + event_dispatch(); + + priv_shutdown(); + + return (0); +} + +static void +priv_sighandler(int sig, short ev, void *arg) +{ + pid_t pid; + int status; + + switch (sig) { + case SIGTERM: + case SIGINT: + event_loopbreak(); + break; + case SIGCHLD: + do { + pid = waitpid(-1, &status, WNOHANG); + if (pid <= 0) + continue; + if (WIFSIGNALED(status)) + log_warnx("process %d terminated by signal %d", + (int)pid, WTERMSIG(status)); + else if (WIFEXITED(status) && WEXITSTATUS(status)) + log_warnx("process %d exited with status %d", + (int)pid, WEXITSTATUS(status)); + else if (WIFEXITED(status)) + log_debug("process %d exited normally", + (int)pid); + else + /* WIFSTOPPED or WIFCONTINUED */ + continue; + } while (pid > 0 || (pid == -1 && errno == EINTR)); + break; + default: + fatalx("signal %d", sig); + } +} + +static void +priv_shutdown(void) +{ + pid_t pid; + + proc_free(p_control); + proc_free(p_engine); + proc_free(p_frontend); + + do { + pid = waitpid(WAIT_MYPGRP, NULL, 0); + } while (pid != -1 || (pid == -1 && errno == EINTR)); + + log_info("exiting"); + + exit(0); +} + +static void +priv_open_listener(struct listener *l) +{ + struct sockaddr_un *su; + struct sockaddr *sa; + const char *path; + mode_t old_umask; + int opt, sock, r; + + sa = (struct sockaddr *)&l->ss; + + sock = socket(sa->sa_family, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (sock == -1) { + if (errno == EAFNOSUPPORT) { + log_warn("%s: socket", __func__); + return; + } + fatal("%s: socket", __func__); + } + + switch (sa->sa_family) { + case AF_LOCAL: + su = (struct sockaddr_un *)sa; + path = su->sun_path; + if (connect(sock, sa, sa->sa_len) == 0) + fatalx("%s already in use", path); + + if (unlink(path) == -1) + if (errno != ENOENT) + fatal("unlink: %s", path); + + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + r = bind(sock, sa, sizeof(*su)); + (void)umask(old_umask); + + if (r == -1) + fatal("bind: %s", path); + break; + + case AF_INET: + case AF_INET6: + opt = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, + sizeof(opt)) < 0) + fatal("setsockopt: %s", log_fmt_sockaddr(sa)); + + if (bind(sock, sa, sa->sa_len) == -1) + fatal("bind: %s", log_fmt_sockaddr(sa)); + break; + + default: + fatalx("bad address family %d", sa->sa_family); + } + + l->sock = sock; +} + +static void +priv_open_filter(struct filter_conf *f) +{ + int sp[2]; + pid_t pid; + + if (f->chain) + return; + + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK, PF_UNSPEC, sp) == -1) + fatal("%s: socketpair", __func__); + + switch (pid = fork()) { + case -1: + fatal("%s: fork", __func__); + case 0: + break; + default: + close(sp[0]); + log_debug("forked filter %s as pid %d", f->name, (int)pid); + f->pid = pid; + f->sock = sp[1]; + return; + } + + if (dup2(sp[0], 3) == -1) + fatal("%s: dup2", __func__); + + if (closefrom(4) == -1) + fatal("%s: closefrom", __func__); + + execvp(f->argv[0], f->argv+1); + fatal("%s: execvp: %s", __func__, f->argv[0]); +} + +static void +priv_send_config(void) +{ + struct listener *l; + struct filter_conf *f; + + m_compose(p_control, IMSG_CONF_START, 0, 0, -1, NULL, 0); + m_compose(p_control, IMSG_CONF_END, 0, 0, -1, NULL, 0); + + m_compose(p_engine, IMSG_CONF_START, 0, 0, -1, NULL, 0); + TAILQ_FOREACH(f, &env->filters, entry) { + if (f->chain) + continue; + m_compose(p_engine, IMSG_CONF_FILTER_PROC, 0, f->pid, f->sock, + f->name, strlen(f->name) + 1); + } + m_compose(p_engine, IMSG_CONF_END, 0, 0, -1, NULL, 0); + + m_compose(p_frontend, IMSG_CONF_START, 0, 0, -1, NULL, 0); + TAILQ_FOREACH(l, &env->listeners, entry) { + m_create(p_frontend, IMSG_CONF_LISTENER, 0, 0, l->sock); + m_add_int(p_frontend, l->proto); + m_add_sockaddr(p_frontend, (struct sockaddr *)(&l->ss)); + m_close(p_frontend); + } + m_compose(p_frontend, IMSG_CONF_END, 0, 0, -1, NULL, 0); +} + +static void +priv_dispatch_control(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) + fatalx("%s: imsg connection lost", __func__); + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} + +static void +priv_dispatch_engine(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) + fatalx("%s: imsg connection lost", __func__); + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} + +static void +priv_dispatch_frontend(struct imsgproc *proc, struct imsg *imsg, void *arg) +{ + if (imsg == NULL) + fatalx("%s: imsg connection lost", __func__); + + if (log_getverbose() > LOGLEVEL_IMSG) + log_imsg(proc, imsg); + + switch (imsg->hdr.type) { + default: + fatalx("%s: unexpected imsg %s", __func__, + log_fmt_imsgtype(imsg->hdr.type)); + } +} diff --git a/extras/filters/smtpfd/smtpfd.conf.5 b/extras/filters/smtpfd/smtpfd.conf.5 new file mode 100644 index 0000000..0b006f5 --- /dev/null +++ b/extras/filters/smtpfd/smtpfd.conf.5 @@ -0,0 +1,139 @@ +.\" $OpenBSD$ +.\" +.\" Copyright (c) 2005 Esben Norby +.\" Copyright (c) 2004 Claudio Jeker +.\" Copyright (c) 2003, 2004 Henning Brauer +.\" Copyright (c) 2002 Daniel Hartmeier +.\" +.\" 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 11 2015 $ +.Dt SMTPFD.CONF 5 +.Os +.Sh NAME +.Nm smtpfd.conf +.Nd New Daemon configuration file +.Sh DESCRIPTION +The +.Xr smtpfd 8 +daemon is a skeleton daemon implementing a privileged separated daemon +in accord with current +.Ox +practices. +.Sh SECTIONS +The +.Nm +config file is divided into three sections. +.Bl -tag -width xxxx +.It Sy Macros +User-defined variables may be defined and used later, simplifying the +configuration file. +.It Sy Global Configuration +Zero or more +.Xr smtpfd 8 +attibutes. +.It Sy Groups +Named lists of +zero or more +.Xr smtpfd 8 +attributes. +.El +.Pp +Additional configuration files can be included with the +.Ic include +keyword. +.Sh MACROS +Macros can be defined that will later be expanded in context. +Macro names must start with a letter, digit, or underscore, +and may contain any of those characters. +Macro names may not be reserved words (for example, +.Ic group ) +Macros are not expanded inside quotes. +.Sh GLOBAL CONFIGURATION +The global configuration section is a list of attribute specifications. +.Pp +Attributes with a +.Sq global- +prefix can be used +.Em only +in the global configuration section. +These are +.Pp +.Bl -tag -width Ds -compact +.It Ic global-text Ar string +.El +.Sh GROUPS +A group is a named list of attributes, specified with +.Bl -tag -width group-name +.It Ic group Ar name { attribute list } +.El +.Pp +Attibutes with the +.Sq group- +prefix can be used only a group. +These are +.Pp +.Bl -tag -width Ds -compact +.It Ic group-v4address Ar IPv4address +.Pp +.It Ic group-v6address Ar IPv6address +.El +.Sh COMMON ATTRIBUTES +Attributes with neither +.Sq global- +nor +.Sq group- +prefixes can be used in either or both global and group configuration +sections. +If used in only the global configuration section then the specification is +inherited by all groups. +If used in both the sections, the specification in the group will take +precedence in that group. +.Pp +These are +.Pp +.Bl -tag -width Ds -compact +.It Xo +.Ic yesno +.Pq Ic yes Ns | Ns Ic no +.Xc +.Pp +.It Ic integer Ar integer +.El +.Sh EXAMPLE +.Bd -literal -offset indent +include "/etc/mail/smtpfd.sub.conf" +hi="5" +integer=$hi +yesno no; +group Barrymore { + yesno yes; + integer 1; + group-v4address 1.2.3.4/32; +} +.Ed +.Sh FILES +.Bl -tag -width "/etc/mail/smtpfd.conf" -compact +.It Pa /etc/mail/smtpfd.conf +.Xr smtpfd 8 +configuration file +.El +.Sh SEE ALSO +.Xr rc.conf.local 8 , +.Xr smtpfd 8 , +.Xr smtpfdfctl 8 +.Sh HISTORY +The +.Nm +file format first appeared in +.Ox X.Y . diff --git a/extras/filters/smtpfd/smtpfd.conf.example b/extras/filters/smtpfd/smtpfd.conf.example new file mode 100644 index 0000000..4207ef5 --- /dev/null +++ b/extras/filters/smtpfd/smtpfd.conf.example @@ -0,0 +1,18 @@ +global-text "Thanks for all the herring" + +yesno no +integer 24 + +group Starbucks { + group-v4address 1.2.3.4/32 + group-v6address 1::1 +} + +group Timmys { + yesno yes + integer 1 + + group-v4address 8.8.8.8/32 + group-v6address 8::8 + +} diff --git a/extras/filters/smtpfd/smtpfd.h b/extras/filters/smtpfd/smtpfd.h new file mode 100644 index 0000000..3e4afaf --- /dev/null +++ b/extras/filters/smtpfd/smtpfd.h @@ -0,0 +1,144 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2017 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define PORT_SMTPF 2626 + +#define SMTPFD_CONFIG "/etc/mail/smtpfd.conf" +#define SMTPFD_SOCKET "/var/run/smtpfd.sock" +#define SMTPFD_CHROOT "/var/empty" +#define SMTPFD_USER "_smtpfd" + +#define SMTPFD_MAXFILTERARG 32 + +#define LOGLEVEL_CONN 2 +#define LOGLEVEL_IMSG 3 +#define LOGLEVEL_IO 4 + +enum { + IMSG_NONE, + + IMSG_SOCK_ENGINE, + IMSG_SOCK_FRONTEND, + + IMSG_CONF_START, + IMSG_CONF_FILTER_PROC, + IMSG_CONF_LISTENER, + IMSG_CONF_END, + + IMSG_RES_GETADDRINFO, + IMSG_RES_GETADDRINFO_END, + IMSG_RES_GETNAMEINFO +}; + +enum { + PROC_CLIENT, + PROC_CONTROL, + PROC_ENGINE, + PROC_FILTER, + PROC_FRONTEND, + PROC_PRIV +}; + +enum { + PROTO_NONE = 0, + PROTO_SMTPF +}; + +struct listener { + TAILQ_ENTRY(listener) entry; + int sock; + int proto; + struct sockaddr_storage ss; + struct timeval timeout; + struct event ev; + int pause; +}; + +struct filter_conf { + TAILQ_ENTRY(filter_conf) entry; + char *name; + int chain; + int argc; + char *argv[SMTPFD_MAXFILTERARG + 1]; + pid_t pid; + int sock; +}; + +struct smtpfd_conf { + TAILQ_HEAD(, listener) listeners; + TAILQ_HEAD(, filter_conf) filters; +}; + +struct io; +struct imsgproc; + +extern struct smtpfd_conf *env; +extern struct imsgproc *p_control; +extern struct imsgproc *p_engine; +extern struct imsgproc *p_frontend; +extern struct imsgproc *p_priv; + +/* control.c */ +void control(int, int); + +/* engine.c */ +void engine(int, int); + +/* frontend.c */ +void frontend(int, int); +void frontend_conn_closed(uint32_t); + +/* frontend_smtpf.c */ +void frontend_smtpf_init(void); +void frontend_smtpf_conn(uint32_t, struct listener *, int, + const struct sockaddr *); + +/* logmsg.c */ +const char *log_fmt_proto(int); +const char *log_fmt_imsgtype(int); +const char *log_fmt_proctype(int); +const char *log_fmt_sockaddr(const struct sockaddr *); +void log_imsg(struct imsgproc *, struct imsg *); +void log_io(const char *, struct io *, int); + +/* parse.y */ +struct smtpfd_conf *parse_config(const char *, int); +int cmdline_symset(char *); + +/* resolver.c */ +void resolver_getaddrinfo(const char *, const char *, const struct addrinfo *, + void(*)(void *, int, struct addrinfo*), void *); +void resolver_getnameinfo(const struct sockaddr *, int, + void(*)(void *, int, const char *, const char *), void *); +void resolver_dispatch_request(struct imsgproc *, struct imsg *); +void resolver_dispatch_result(struct imsgproc *, struct imsg *); + +/* smtpfd.c */ +struct smtpfd_conf *config_new_empty(void); +void config_clear(struct smtpfd_conf *);