diff options
author | Reyk Floeter <reyk@esdenera.com> | 2014-07-12 19:00:52 +0200 |
---|---|---|
committer | Reyk Floeter <reyk@esdenera.com> | 2014-07-12 19:00:52 +0200 |
commit | 3d88bcbb432cda12794bc4e0aac180e5489901ff (patch) | |
tree | a98a20f0806c3e5476bc64798d503f70442c4a97 | |
download | httpd-3d88bcbb432cda12794bc4e0aac180e5489901ff.tar.gz httpd-3d88bcbb432cda12794bc4e0aac180e5489901ff.zip |
Import httpd experiment based on relayd.
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 18 | ||||
-rw-r--r-- | config.c | 147 | ||||
-rw-r--r-- | control.c | 337 | ||||
-rw-r--r-- | http.h | 122 | ||||
-rw-r--r-- | httpd.8 | 49 | ||||
-rw-r--r-- | httpd.c | 527 | ||||
-rw-r--r-- | httpd.conf.5 | 1567 | ||||
-rw-r--r-- | httpd.h | 395 | ||||
-rw-r--r-- | log.c | 251 | ||||
-rw-r--r-- | parse.y | 1019 | ||||
-rw-r--r-- | proc.c | 627 | ||||
-rw-r--r-- | server.c | 636 | ||||
-rw-r--r-- | server_http.c | 1736 |
14 files changed, 7433 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67f3c82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cvsignore +CVS diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..066c649 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# $OpenBSD$ + +PROG= httpd +SRCS= parse.y +SRCS+= config.c control.c httpd.c log.c proc.c server.c +MAN= httpd.8 httpd.conf.5 + +LDADD= -levent -lssl -lcrypto -lutil +DPADD= ${LIBEVENT} ${LIBSSL} ${LIBCRYPTO} ${LIBUTIL} + +CFLAGS+= -Wall -I${.CURDIR} -Werror +CFLAGS+= -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations +CFLAGS+= -Wshadow -Wpointer-arith +CFLAGS+= -Wsign-compare +CLEANFILES+= y.tab.h + +.include <bsd.prog.mk> diff --git a/config.c b/config.c new file mode 100644 index 0000000..8a64c4b --- /dev/null +++ b/config.c @@ -0,0 +1,147 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2011 - 2014 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/queue.h> +#include <sys/uio.h> + +#include <net/if.h> +#include <net/pfvar.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <arpa/nameser.h> +#include <net/route.h> + +#include <ctype.h> +#include <unistd.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <limits.h> +#include <stdint.h> +#include <stdarg.h> +#include <stdio.h> +#include <netdb.h> +#include <string.h> +#include <ifaddrs.h> + +#include <openssl/ssl.h> + +#include "httpd.h" + +int +config_init(struct httpd *env) +{ + struct privsep *ps = env->sc_ps; + u_int what; + + /* Global configuration */ + if (privsep_process == PROC_PARENT) { + env->sc_prefork_server = SERVER_NUMPROC; + + ps->ps_what[PROC_PARENT] = CONFIG_ALL; + ps->ps_what[PROC_SERVER] = CONFIG_SERVERS; + } + + /* Other configuration */ + what = ps->ps_what[privsep_process]; + + if (what & CONFIG_SERVERS) { +#if 0 + if ((env->sc_servers = + calloc(1, sizeof(*env->sc_relays))) == NULL) + return (-1); + TAILQ_INIT(env->sc_relays); + if ((env->sc_pkeys = + calloc(1, sizeof(*env->sc_pkeys))) == NULL) + return (-1); + TAILQ_INIT(env->sc_pkeys); +#endif + } + + return (0); +} + +void +config_purge(struct httpd *env, u_int reset) +{ + struct privsep *ps = env->sc_ps; + struct server *srv; + u_int what; + + what = ps->ps_what[privsep_process] & reset; + + if (what & CONFIG_SERVERS && env->sc_servers != NULL) { + while ((srv = TAILQ_FIRST(env->sc_servers)) != NULL) { + TAILQ_REMOVE(env->sc_servers, srv, srv_entry); + free(srv); + } + } +} + +int +config_setreset(struct httpd *env, u_int reset) +{ + struct privsep *ps = env->sc_ps; + int id; + + for (id = 0; id < PROC_MAX; id++) { + if ((reset & ps->ps_what[id]) == 0 || + id == privsep_process) + continue; + proc_compose_imsg(ps, id, -1, IMSG_CTL_RESET, -1, + &reset, sizeof(reset)); + } + + return (0); +} + +int +config_getreset(struct httpd *env, struct imsg *imsg) +{ + u_int mode; + + IMSG_SIZE_CHECK(imsg, &mode); + memcpy(&mode, imsg->data, sizeof(mode)); + + config_purge(env, mode); + + return (0); +} + +int +config_getcfg(struct httpd *env, struct imsg *imsg) +{ + struct privsep *ps = env->sc_ps; + struct ctl_flags cf; + u_int what; + + if (IMSG_DATA_SIZE(imsg) != sizeof(cf)) + return (0); /* ignore */ + + /* Update runtime flags */ + memcpy(&cf, imsg->data, sizeof(cf)); + env->sc_opts = cf.cf_opts; + env->sc_flags = cf.cf_flags; + + what = ps->ps_what[privsep_process]; + + return (0); +} diff --git a/control.c b/control.c new file mode 100644 index 0000000..20e0bea --- /dev/null +++ b/control.c @@ -0,0 +1,337 @@ +/* $OpenBSD: control.c,v 1.46 2014/07/11 16:39:10 krw Exp $ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/queue.h> +#include <sys/param.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include <net/if.h> + +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> + +#include <openssl/ssl.h> + +#include "httpd.h" + +#define CONTROL_BACKLOG 5 + +struct ctl_connlist ctl_conns; + +void control_accept(int, short, void *); +void control_close(int, struct control_sock *); + +int +control_init(struct privsep *ps, struct control_sock *cs) +{ + struct httpd *env = ps->ps_env; + struct sockaddr_un sun; + int fd; + mode_t old_umask, mode; + + if (cs->cs_name == NULL) + return (0); + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + log_warn("%s: socket", __func__); + return (-1); + } + + sun.sun_family = AF_UNIX; + if (strlcpy(sun.sun_path, cs->cs_name, + sizeof(sun.sun_path)) >= sizeof(sun.sun_path)) { + log_warn("%s: %s name too long", __func__, cs->cs_name); + close(fd); + return (-1); + } + + if (unlink(cs->cs_name) == -1) + if (errno != ENOENT) { + log_warn("%s: unlink %s", __func__, cs->cs_name); + close(fd); + return (-1); + } + + if (cs->cs_restricted) { + old_umask = umask(S_IXUSR|S_IXGRP|S_IXOTH); + mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH; + } else { + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP; + } + + if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) { + log_warn("%s: bind: %s", __func__, cs->cs_name); + close(fd); + (void)umask(old_umask); + return (-1); + } + (void)umask(old_umask); + + if (chmod(cs->cs_name, mode) == -1) { + log_warn("%s: chmod", __func__); + close(fd); + (void)unlink(cs->cs_name); + return (-1); + } + + socket_set_blockmode(fd, BM_NONBLOCK); + cs->cs_fd = fd; + cs->cs_env = env; + + return (0); +} + +int +control_listen(struct control_sock *cs) +{ + if (cs->cs_name == NULL) + return (0); + + if (listen(cs->cs_fd, CONTROL_BACKLOG) == -1) { + log_warn("%s: listen", __func__); + return (-1); + } + + event_set(&cs->cs_ev, cs->cs_fd, EV_READ, + control_accept, cs); + event_add(&cs->cs_ev, NULL); + evtimer_set(&cs->cs_evt, control_accept, cs); + + return (0); +} + +void +control_cleanup(struct control_sock *cs) +{ + if (cs->cs_name == NULL) + return; + event_del(&cs->cs_ev); + event_del(&cs->cs_evt); + (void)unlink(cs->cs_name); +} + +/* ARGSUSED */ +void +control_accept(int listenfd, short event, void *arg) +{ + int connfd; + socklen_t len; + struct sockaddr_un sun; + struct ctl_conn *c; + struct control_sock *cs = arg; + + event_add(&cs->cs_ev, NULL); + if ((event & EV_TIMEOUT)) + return; + + len = sizeof(sun); + if ((connfd = accept(listenfd, + (struct sockaddr *)&sun, &len)) == -1) { + /* + * Pause accept if we are out of file descriptors, or + * libevent will haunt us here too. + */ + if (errno == ENFILE || errno == EMFILE) { + struct timeval evtpause = { 1, 0 }; + + event_del(&cs->cs_ev); + evtimer_add(&cs->cs_evt, &evtpause); + } else if (errno != EWOULDBLOCK && errno != EINTR && + errno != ECONNABORTED) + log_warn("%s: accept", __func__); + return; + } + + socket_set_blockmode(connfd, BM_NONBLOCK); + + if ((c = calloc(1, sizeof(struct ctl_conn))) == NULL) { + close(connfd); + log_warn("%s: calloc", __func__); + return; + } + + imsg_init(&c->iev.ibuf, connfd); + c->iev.handler = control_dispatch_imsg; + c->iev.events = EV_READ; + c->iev.data = cs; /* proc.c cheats (reuses the handler) */ + event_set(&c->iev.ev, c->iev.ibuf.fd, c->iev.events, + c->iev.handler, cs); + event_add(&c->iev.ev, NULL); + + TAILQ_INSERT_TAIL(&ctl_conns, c, entry); +} + +struct ctl_conn * +control_connbyfd(int fd) +{ + struct ctl_conn *c; + + for (c = TAILQ_FIRST(&ctl_conns); c != NULL && c->iev.ibuf.fd != fd; + c = TAILQ_NEXT(c, entry)) + ; /* nothing */ + + return (c); +} + +void +control_close(int fd, struct control_sock *cs) +{ + struct ctl_conn *c; + + if ((c = control_connbyfd(fd)) == NULL) { + log_warn("%s: fd %d not found", __func__, fd); + return; + } + + msgbuf_clear(&c->iev.ibuf.w); + TAILQ_REMOVE(&ctl_conns, c, entry); + + event_del(&c->iev.ev); + close(c->iev.ibuf.fd); + + /* Some file descriptors are available again. */ + if (evtimer_pending(&cs->cs_evt, NULL)) { + evtimer_del(&cs->cs_evt); + event_add(&cs->cs_ev, NULL); + } + + free(c); +} + +/* ARGSUSED */ +void +control_dispatch_imsg(int fd, short event, void *arg) +{ + struct control_sock *cs = arg; + struct ctl_conn *c; + struct imsg imsg; + int n; + int verbose; + struct httpd *env = cs->cs_env; + + if ((c = control_connbyfd(fd)) == NULL) { + log_warn("%s: fd %d not found", __func__, fd); + return; + } + + if (event & EV_READ) { + if ((n = imsg_read(&c->iev.ibuf)) == -1 || n == 0) { + control_close(fd, cs); + return; + } + } + + if (event & EV_WRITE) { + if (msgbuf_write(&c->iev.ibuf.w) <= 0 && errno != EAGAIN) { + control_close(fd, cs); + return; + } + } + + for (;;) { + if ((n = imsg_get(&c->iev.ibuf, &imsg)) == -1) { + control_close(fd, cs); + return; + } + + if (n == 0) + break; + + if (c->waiting) { + log_debug("%s: unexpected imsg %d", + __func__, imsg.hdr.type); + imsg_free(&imsg); + control_close(fd, cs); + return; + } + + switch (imsg.hdr.type) { + case IMSG_CTL_SHUTDOWN: + case IMSG_CTL_RELOAD: + proc_forward_imsg(env->sc_ps, &imsg, PROC_PARENT, -1); + break; + case IMSG_CTL_NOTIFY: + if (c->flags & CTL_CONN_NOTIFY) { + log_debug("%s: " + "client requested notify more than once", + __func__); + imsg_compose_event(&c->iev, IMSG_CTL_FAIL, + 0, 0, -1, NULL, 0); + break; + } + c->flags |= CTL_CONN_NOTIFY; + break; + case IMSG_CTL_VERBOSE: + IMSG_SIZE_CHECK(&imsg, &verbose); + + memcpy(&verbose, imsg.data, sizeof(verbose)); + + proc_forward_imsg(env->sc_ps, &imsg, PROC_PARENT, -1); + proc_forward_imsg(env->sc_ps, &imsg, PROC_SERVER, -1); + + memcpy(imsg.data, &verbose, sizeof(verbose)); + control_imsg_forward(&imsg); + log_verbose(verbose); + break; + default: + log_debug("%s: error handling imsg %d", + __func__, imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + + imsg_event_add(&c->iev); +} + +void +control_imsg_forward(struct imsg *imsg) +{ + struct ctl_conn *c; + + TAILQ_FOREACH(c, &ctl_conns, entry) + if (c->flags & CTL_CONN_NOTIFY) + imsg_compose_event(&c->iev, imsg->hdr.type, + 0, imsg->hdr.pid, -1, imsg->data, + imsg->hdr.len - IMSG_HEADER_SIZE); +} + +void +socket_set_blockmode(int fd, enum blockmodes bm) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL, 0)) == -1) + fatal("fcntl F_GETFL"); + + if (bm == BM_NONBLOCK) + flags |= O_NONBLOCK; + else + flags &= ~O_NONBLOCK; + + if ((flags = fcntl(fd, F_SETFL, flags)) == -1) + fatal("fcntl F_SETFL"); +} @@ -0,0 +1,122 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2012 - 2014 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _HTTPD_HTTP_H +#define _HTTPD_HTTP_H + +enum httpmethod { + HTTP_METHOD_NONE = 0, + + /* HTTP/1.1, RFC 2616 */ + HTTP_METHOD_GET, + HTTP_METHOD_HEAD, + HTTP_METHOD_POST, + HTTP_METHOD_PUT, + HTTP_METHOD_DELETE, + HTTP_METHOD_OPTIONS, + HTTP_METHOD_TRACE, + HTTP_METHOD_CONNECT, + + /* WebDAV, RFC 4918 */ + HTTP_METHOD_PROPFIND, + HTTP_METHOD_PROPPATCH, + HTTP_METHOD_MKCOL, + HTTP_METHOD_COPY, + HTTP_METHOD_MOVE, + HTTP_METHOD_LOCK, + HTTP_METHOD_UNLOCK, + + /* PATCH, RFC 5789 */ + HTTP_METHOD_PATCH, + + /* Server response (internal value) */ + HTTP_METHOD_RESPONSE +}; + +struct http_method { + enum httpmethod method_id; + const char *method_name; +}; +#define HTTP_METHODS { \ + { HTTP_METHOD_GET, "GET" }, \ + { HTTP_METHOD_HEAD, "HEAD" }, \ + { HTTP_METHOD_POST, "POST" }, \ + { HTTP_METHOD_PUT, "PUT" }, \ + { HTTP_METHOD_DELETE, "DELETE" }, \ + { HTTP_METHOD_OPTIONS, "OPTIONS" }, \ + { HTTP_METHOD_TRACE, "TRACE" }, \ + { HTTP_METHOD_CONNECT, "CONNECT" }, \ + { HTTP_METHOD_PROPFIND, "PROPFIND" }, \ + { HTTP_METHOD_PROPPATCH, "PROPPATCH" }, \ + { HTTP_METHOD_MKCOL, "MKCOL" }, \ + { HTTP_METHOD_COPY, "COPY" }, \ + { HTTP_METHOD_MOVE, "MOVE" }, \ + { HTTP_METHOD_LOCK, "LOCK" }, \ + { HTTP_METHOD_UNLOCK, "UNLOCK" }, \ + { HTTP_METHOD_PATCH, "PATCH" }, \ + { HTTP_METHOD_NONE, NULL } \ +} + +struct http_error { + int error_code; + const char *error_name; +}; +#define HTTP_ERRORS { \ + { 100, "Continue" }, \ + { 101, "Switching Protocols" }, \ + { 200, "OK" }, \ + { 201, "Created" }, \ + { 202, "Accepted" }, \ + { 203, "Non-Authorative Information" }, \ + { 204, "No Content" }, \ + { 205, "Reset Content" }, \ + { 206, "Partial Content" }, \ + { 300, "Multiple Choices" }, \ + { 301, "Moved Permanently" }, \ + { 302, "Moved Temporarily" }, \ + { 303, "See Other" }, \ + { 304, "Not Modified" }, \ + { 307, "Temporary Redirect" }, \ + { 400, "Bad Request" }, \ + { 401, "Unauthorized" }, \ + { 402, "Payment Required" }, \ + { 403, "Forbidden" }, \ + { 404, "Not Found" }, \ + { 405, "Method Not Allowed" }, \ + { 406, "Not Acceptable" }, \ + { 407, "Proxy Authentication Required" }, \ + { 408, "Request Timeout" }, \ + { 409, "Conflict" }, \ + { 410, "Gone" }, \ + { 411, "Length Required" }, \ + { 412, "Precondition Failed" }, \ + { 413, "Request Entity Too Large" }, \ + { 414, "Request-URL Too Long" }, \ + { 415, "Unsupported Media Type" }, \ + { 416, "Requested Range Not Satisfiable" }, \ + { 417, "Expectation Failed" }, \ + { 500, "Internal Server Error" }, \ + { 501, "Not Implemented" }, \ + { 502, "Bad Gateway" }, \ + { 503, "Service Unavailable" }, \ + { 504, "Gateway Timeout" }, \ + { 505, "HTTP Version Not Supported" }, \ + { 0, NULL } \ +} + +#endif /* _HTTPD_HTTP_H */ @@ -0,0 +1,49 @@ +.\" $OpenBSD$ +.\" +.\" Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org> +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate: July 12 2014 $ +.Dt HTTPD 8 +.Os +.Sh NAME +.Nm httpd +.Nd HTTP daemon +.Sh SYNOPSIS +.Nm +.Op Fl dnv +.Op Fl D Ar macro Ns = Ns Ar value +.Op Fl f Ar file +.Sh DESCRIPTION +.Nm +is an HTTP server that serves static files. +.El +.Sh FILES +.Bl -tag -width "/var/run/httpd.sockXX" -compact +.It /etc/httpd.conf +Default configuration file. +.It /var/run/httpd.sock +.Ux Ns -domain +socket used for communication with +.Xr httpctl 8 . +.El +.Sh SEE ALSO +.Xr httpd.conf 5 , +.Xr httpctl 8 +.Sh AUTHORS +.An -nosplit +The +.Nm +program was written by +.An Reyk Floeter Aq Mt reyk@openbsd.org . @@ -0,0 +1,527 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <sys/resource.h> +#include <sys/hash.h> + +#include <net/if.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <fcntl.h> +#include <getopt.h> +#include <fnmatch.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <unistd.h> +#include <ctype.h> +#include <pwd.h> +#include <sha1.h> +#include <md5.h> + +#include <openssl/ssl.h> + +#include "httpd.h" + +__dead void usage(void); + +int parent_configure(struct httpd *); +void parent_configure_done(struct httpd *); +void parent_reload(struct httpd *, u_int, const char *); +void parent_sig_handler(int, short, void *); +void parent_shutdown(struct httpd *); +int parent_dispatch_server(int, struct privsep_proc *, + struct imsg *); + +struct httpd *httpd_env; + +static struct privsep_proc procs[] = { + { "server", PROC_SERVER, parent_dispatch_server, server } +}; + +void +parent_sig_handler(int sig, short event, void *arg) +{ + struct privsep *ps = arg; + int die = 0, status, fail, id; + pid_t pid; + char *cause; + + switch (sig) { + case SIGTERM: + case SIGINT: + die = 1; + /* FALLTHROUGH */ + case SIGCHLD: + do { + pid = waitpid(WAIT_ANY, &status, WNOHANG); + if (pid <= 0) + continue; + + fail = 0; + if (WIFSIGNALED(status)) { + fail = 1; + asprintf(&cause, "terminated; signal %d", + WTERMSIG(status)); + } else if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) { + fail = 1; + asprintf(&cause, "exited abnormally"); + } else + asprintf(&cause, "exited okay"); + } else + fatalx("unexpected cause of SIGCHLD"); + + die = 1; + + for (id = 0; id < PROC_MAX; id++) + if (pid == ps->ps_pid[id]) { + if (fail) + log_warnx("lost child: %s %s", + ps->ps_title[id], cause); + break; + } + + free(cause); + } while (pid > 0 || (pid == -1 && errno == EINTR)); + + if (die) + parent_shutdown(ps->ps_env); + break; + case SIGHUP: + log_info("%s: reload requested with SIGHUP", __func__); + + /* + * This is safe because libevent uses async signal handlers + * that run in the event loop and not in signal context. + */ + parent_reload(ps->ps_env, CONFIG_RELOAD, NULL); + break; + case SIGPIPE: + /* ignore */ + break; + default: + fatalx("unexpected signal"); + } +} + +__dead 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[]) +{ + int c; + int debug = 0, verbose = 0; + u_int32_t opts = 0; + struct httpd *env; + struct privsep *ps; + const char *conffile = CONF_FILE; + + while ((c = getopt(argc, argv, "dD:nf:v")) != -1) { + switch (c) { + case 'd': + debug = 2; + break; + case 'D': + if (cmdline_symset(optarg) < 0) + log_warnx("could not parse macro definition %s", + optarg); + break; + case 'n': + debug = 2; + opts |= HTTPD_OPT_NOACTION; + break; + case 'f': + conffile = optarg; + break; + case 'v': + verbose++; + opts |= HTTPD_OPT_VERBOSE; + break; + default: + usage(); + } + } + + log_init(debug ? debug : 1); /* log to stderr until daemonized */ + + argc -= optind; + if (argc > 0) + usage(); + + if ((env = calloc(1, sizeof(*env))) == NULL || + (ps = calloc(1, sizeof(*ps))) == NULL) + exit(1); + + httpd_env = env; + env->sc_ps = ps; + ps->ps_env = env; + TAILQ_INIT(&ps->ps_rcsocks); + env->sc_conffile = conffile; + env->sc_opts = opts; + + if (parse_config(env->sc_conffile, env) == -1) + exit(1); + + if (debug) + env->sc_opts |= HTTPD_OPT_LOGUPDATE; + + if (geteuid()) + errx(1, "need root privileges"); + + if ((ps->ps_pw = getpwnam(HTTPD_USER)) == NULL) + errx(1, "unknown user %s", HTTPD_USER); + + /* Configure the control socket */ + ps->ps_csock.cs_name = HTTPD_SOCKET; + + log_init(debug); + log_verbose(verbose); + + if (!debug && daemon(1, 0) == -1) + err(1, "failed to daemonize"); + + if (env->sc_opts & HTTPD_OPT_NOACTION) + ps->ps_noaction = 1; + else + log_info("startup"); + + ps->ps_instances[PROC_SERVER] = env->sc_prefork_server; + ps->ps_ninstances = env->sc_prefork_server; + + proc_init(ps, procs, nitems(procs)); + + setproctitle("parent"); + + event_init(); + + signal_set(&ps->ps_evsigint, SIGINT, parent_sig_handler, ps); + signal_set(&ps->ps_evsigterm, SIGTERM, parent_sig_handler, ps); + signal_set(&ps->ps_evsigchld, SIGCHLD, parent_sig_handler, ps); + signal_set(&ps->ps_evsighup, SIGHUP, parent_sig_handler, ps); + signal_set(&ps->ps_evsigpipe, SIGPIPE, parent_sig_handler, ps); + + signal_add(&ps->ps_evsigint, NULL); + signal_add(&ps->ps_evsigterm, NULL); + signal_add(&ps->ps_evsigchld, NULL); + signal_add(&ps->ps_evsighup, NULL); + signal_add(&ps->ps_evsigpipe, NULL); + + proc_listen(ps, procs, nitems(procs)); + + if (load_config(env->sc_conffile, env) == -1) { + proc_kill(env->sc_ps); + exit(1); + } + + if (env->sc_opts & HTTPD_OPT_NOACTION) { + fprintf(stderr, "configuration OK\n"); + proc_kill(env->sc_ps); + exit(0); + } + + if (parent_configure(env) == -1) + fatalx("configuration failed"); + + event_dispatch(); + + parent_shutdown(env); + /* NOTREACHED */ + + return (0); +} + +int +parent_configure(struct httpd *env) +{ + int id; + struct ctl_flags cf; + int ret = -1; + +#if 0 + TAILQ_FOREACH(srv, env->sc_server, entry) + config_setserver(env, srv); +#endif + + /* The servers need to reload their config. */ + env->sc_reload = env->sc_prefork_server; + + for (id = 0; id < PROC_MAX; id++) { + if (id == privsep_process) + continue; + cf.cf_opts = env->sc_opts; + cf.cf_flags = env->sc_flags; + + proc_compose_imsg(env->sc_ps, id, -1, IMSG_CFG_DONE, -1, + &cf, sizeof(cf)); + } + + ret = 0; + + config_purge(env, CONFIG_ALL & ~CONFIG_SERVERS); + return (ret); +} + +void +parent_reload(struct httpd *env, u_int reset, const char *filename) +{ + if (env->sc_reload) { + log_debug("%s: already in progress: %d pending", + __func__, env->sc_reload); + return; + } + + /* Switch back to the default config file */ + if (filename == NULL || *filename == '\0') + filename = env->sc_conffile; + + log_debug("%s: level %d config file %s", __func__, reset, filename); + + config_purge(env, CONFIG_ALL); + + if (reset == CONFIG_RELOAD) { + if (load_config(filename, env) == -1) { + log_debug("%s: failed to load config file %s", + __func__, filename); + } + + config_setreset(env, CONFIG_ALL); + + if (parent_configure(env) == -1) { + log_debug("%s: failed to commit config from %s", + __func__, filename); + } + } else + config_setreset(env, reset); +} + +void +parent_configure_done(struct httpd *env) +{ + int id; + + if (env->sc_reload == 0) { + log_warnx("%s: configuration already finished", __func__); + return; + } + + env->sc_reload--; + if (env->sc_reload == 0) { + for (id = 0; id < PROC_MAX; id++) { + if (id == privsep_process) + continue; + + proc_compose_imsg(env->sc_ps, id, -1, IMSG_CTL_START, + -1, NULL, 0); + } + } +} + +void +parent_shutdown(struct httpd *env) +{ + config_purge(env, CONFIG_ALL); + + proc_kill(env->sc_ps); + control_cleanup(&env->sc_ps->ps_csock); + + free(env->sc_ps); + free(env); + + log_info("parent terminating, pid %d", getpid()); + + exit(0); +} + +int +parent_dispatch_server(int fd, struct privsep_proc *p, struct imsg *imsg) +{ + struct httpd *env = p->p_env; + + switch (imsg->hdr.type) { + case IMSG_CFG_DONE: + parent_configure_done(env); + break; + default: + return (-1); + } + + return (0); +} + +/* + * Utility functions + */ + +void +event_again(struct event *ev, int fd, short event, + void (*fn)(int, short, void *), + struct timeval *start, struct timeval *end, void *arg) +{ + struct timeval tv_next, tv_now, tv; + + getmonotime(&tv_now); + bcopy(end, &tv_next, sizeof(tv_next)); + timersub(&tv_now, start, &tv_now); + timersub(&tv_next, &tv_now, &tv_next); + + bzero(&tv, sizeof(tv)); + if (timercmp(&tv_next, &tv, >)) + bcopy(&tv_next, &tv, sizeof(tv)); + + event_del(ev); + event_set(ev, fd, event, fn, arg); + event_add(ev, &tv); +} + +const char * +canonicalize_host(const char *host, char *name, size_t len) +{ + struct sockaddr_in sin4; + struct sockaddr_in6 sin6; + u_int i, j; + size_t plen; + char c; + + if (len < 2) + goto fail; + + /* + * Canonicalize an IPv4/6 address + */ + if (inet_pton(AF_INET, host, &sin4) == 1) + return (inet_ntop(AF_INET, &sin4, name, len)); + if (inet_pton(AF_INET6, host, &sin6) == 1) + return (inet_ntop(AF_INET6, &sin6, name, len)); + + /* + * Canonicalize a hostname + */ + + /* 1. remove repeated dots and convert upper case to lower case */ + plen = strlen(host); + bzero(name, len); + for (i = j = 0; i < plen; i++) { + if (j >= (len - 1)) + goto fail; + c = tolower(host[i]); + if ((c == '.') && (j == 0 || name[j - 1] == '.')) + continue; + name[j++] = c; + } + + /* 2. remove trailing dots */ + for (i = j; i > 0; i--) { + if (name[i - 1] != '.') + break; + name[i - 1] = '\0'; + j--; + } + if (j <= 0) + goto fail; + + return (name); + + fail: + errno = EINVAL; + return (NULL); +} + +void +socket_rlimit(int maxfd) +{ + struct rlimit rl; + + if (getrlimit(RLIMIT_NOFILE, &rl) == -1) + fatal("socket_rlimit: failed to get resource limit"); + log_debug("%s: max open files %llu", __func__, rl.rlim_max); + + /* + * Allow the maximum number of open file descriptors for this + * login class (which should be the class "daemon" by default). + */ + if (maxfd == -1) + rl.rlim_cur = rl.rlim_max; + else + rl.rlim_cur = MAX(rl.rlim_max, (rlim_t)maxfd); + if (setrlimit(RLIMIT_NOFILE, &rl) == -1) + fatal("socket_rlimit: failed to set resource limit"); +} + +char * +get_string(u_int8_t *ptr, size_t len) +{ + size_t i; + char *str; + + for (i = 0; i < len; i++) + if (!(isprint(ptr[i]) || isspace(ptr[i]))) + break; + + if ((str = calloc(1, i + 1)) == NULL) + return (NULL); + memcpy(str, ptr, i); + + return (str); +} + +void * +get_data(u_int8_t *ptr, size_t len) +{ + u_int8_t *data; + + if ((data = calloc(1, len)) == NULL) + return (NULL); + memcpy(data, ptr, len); + + return (data); +} + +int +accept_reserve(int sockfd, struct sockaddr *addr, socklen_t *addrlen, + int reserve, volatile int *counter) +{ + int ret; + if (getdtablecount() + reserve + + *counter >= getdtablesize()) { + errno = EMFILE; + return (-1); + } + + if ((ret = accept(sockfd, addr, addrlen)) > -1) { + (*counter)++; + DPRINTF("%s: inflight incremented, now %d",__func__, *counter); + } + return (ret); +} diff --git a/httpd.conf.5 b/httpd.conf.5 new file mode 100644 index 0000000..24f823f --- /dev/null +++ b/httpd.conf.5 @@ -0,0 +1,1567 @@ +.\" $OpenBSD: relayd.conf.5,v 1.147 2014/07/11 16:59:38 reyk Exp $ +.\" +.\" Copyright (c) 2006 - 2014 Reyk Floeter <reyk@openbsd.org> +.\" Copyright (c) 2006, 2007 Pierre-Yves Ritschard <pyr@openbsd.org> +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate: July 11 2014 $ +.Dt RELAYD.CONF 5 +.Os +.Sh NAME +.Nm relayd.conf +.Nd relay daemon configuration file +.Sh DESCRIPTION +.Nm +is the configuration file for the relay daemon, +.Xr relayd 8 . +.Sh SECTIONS +.Nm +is divided into seven main 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 +Global settings for +.Xr relayd 8 . +Do note that the config file allows global settings to be added after +defining tables in the config file, but those tables will use the +built-in defaults instead of the global settings below them. +.It Sy Tables +Table definitions describe a list of hosts, +in a similar fashion to +.Xr pf 4 +tables. +They are used for relay, redirection, and router target selection with +the described options and health checking on the host they contain. +.It Sy Redirections +Redirections are translated to +.Xr pf 4 +rdr-to rules for stateful forwarding to a target host from a +health-checked table on layer 3. +.It Sy Relays +Relays allow application layer load balancing, SSL acceleration, and +general purpose TCP proxying on layer 7. +.It Sy Protocols +Protocols are predefined settings and filter rules for relays. +.It Sy Routers +Routers are used to insert routes with health-checked gateways for +(WAN) link balancing. +.El +.Pp +Within the sections, +a host +.Ar address +can be specified by IPv4 address, IPv6 address, interface name, +interface group, or DNS hostname. +If the address is an interface name, +.Xr relayd 8 +will look up the first IPv4 address and any other IPv4 and IPv6 +addresses of the specified network interface. +A +.Ar port +can be specified by number or name. +The port name to number mappings are found in the file +.Pa /etc/services ; +see +.Xr services 5 +for details. +.Pp +The current line can be extended over multiple lines using a backslash +.Pq Sq \e . +Comments can be put anywhere in the file using a hash mark +.Pq Sq # , +and extend to the end of the current line. +Care should be taken when commenting out multi-line text: +the comment is effective until the end of the entire block. +.Pp +Argument names not beginning with a letter, digit, or underscore +must be quoted. +.Pp +Additional configuration files can be included with the +.Ic include +keyword, for example: +.Bd -literal -offset indent +include "/etc/relayd.conf.local" +.Ed +.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 table , +.Ic relay , +or +.Ic timeout ) . +Macros are not expanded inside quotes. +.Pp +For example: +.Bd -literal -offset indent +www1="10.0.0.1" +www2="10.0.0.2" +table \*(Ltwebhosts\*(Gt { + $www1 + $www2 +} +.Ed +.Sh GLOBAL CONFIGURATION +Here are the settings that can be set globally: +.Bl -tag -width Ds +.It Ic interval Ar number +Set the interval in seconds at which the hosts will be checked. +The default interval is 10 seconds. +.It Xo +.Ic log +.Pq Ic updates Ns | Ns Ic all +.Xc +Log state notifications after completed host checks. +Either only log the +.Ic updates +to new states or log +.Ic all +state notifications, even if the state didn't change. +The host state can be +.Ar up +(the health check completed successfully), +.Ar down +(the host is down or didn't match the check criteria), +or +.Ar unknown +(the host is disabled or has not been checked yet). +.It Ic prefork Ar number +When using relays, run the specified number of processes to handle +relayed connections. +This increases the performance and prevents delays when connecting +to a relay. +.Xr relayd 8 +runs 3 relay processes by default and every process will handle +all configured relays. +.It Ic snmp Oo Ic trap Oc Op Qq Ar path +Send an SNMP trap when the state of a host changes. +.Xr relayd 8 +will try to connect to +.Xr snmpd 8 +over the AgentX SNMP socket specified by +.Ar path +and request it send a trap to the registered trap receivers. +If +.Ar path +is not specified, a default path of +.Ar /var/run/agentx.sock +will be used. +See +.Xr snmpd.conf 5 +for more information about SNMP configuration. +.It Ic timeout Ar number +Set the global timeout in milliseconds for checks. +This can be overridden by the timeout value in the table definitions. +The default interval is 200 milliseconds and it must not exceed the +global interval. +Please note that the default value is optimized for checks within the +same collision domain \(en use a higher timeout, such as 1000 milliseconds, +for checks of hosts in other subnets. +If this option is to be set, it should be placed before overrides in tables. +.El +.Sh TABLES +Tables are used to group a set of hosts as the target for redirections +or relays; they will be mapped to a +.Xr pf 4 +table for redirections. +Tables may be defined with the following attribute: +.Bl -tag -width disable +.It Ic disable +Start the table disabled \(en no hosts will be checked in this table. +The table can be later enabled through +.Xr relayctl 8 . +.El +.Pp +Each table must contain at least one host +.Ar address ; +multiple hosts are separated by newline, comma, or whitespace. +Host entries may be defined with the following attributes: +.Bl -tag -width retry +.It Ic ip ttl Ar number +Change the default time-to-live value in the IP headers for host checks. +.It Ic parent Ar number +The optional parent option inherits the state from a parent +host with the specified identifier. +The check will be skipped for this host and copied from the parent host. +This can be used to prevent multiple checks on hosts with multiple IP +addresses for the same service. +The host identifiers are sequentially assigned to the configured hosts +starting with 1; it can be shown with the +.Xr relayctl 8 +.Ic show summary +commands. +.It Ic priority Ar number +Change the route priority used when adding a route. +If not specified, the kernel will set a priority of 8 (RTP_STATIC). +In ordinary use, a fallback route should be added statically with a very +high (e.g. 52) priority. +Unused in all other modes. +.It Ic retry Ar number +The optional retry option adds a tolerance for failed host checks; +the check will be retried for +.Ar number +more times before setting the host state to down. +If this table is used by a relay, it will also specify the number of +retries for outgoing connection attempts. +.El +.Pp +For example: +.Bd -literal -offset indent +table \*(Ltservice\*(Gt { 192.168.1.1, 192.168.1.2, 192.168.2.3 } +table \*(Ltfallback\*(Gt disable { 10.1.5.1 retry 2 } + +redirect "www" { + listen on www.example.com port 80 + forward to \*(Ltservice\*(Gt check http "/" code 200 + forward to \*(Ltfallback\*(Gt check http "/" code 200 +} +.Ed +.Pp +Tables are used by +.Ic forward to +directives in redirections or relays with a set of general options, +health-checking rules, and timings; +see the +.Sx REDIRECTIONS +and +.Sx RELAYS +sections for more information about the forward context. +Table specific configuration directives are described below. +Multiple options can be appended to +.Ic forward to +directives, separated by whitespaces. +.Pp +The following options will configure the health-checking method for +the table, and is mandatory for redirections: +.Bl -tag -width Ds +.It Xo +.Ic check http Ar path +.Op Ic host Ar hostname +.Ic code Ar number +.Xc +For each host in the table, verify that retrieving the URL +.Ar path +gives the HTTP return code +.Ar number . +If +.Ar hostname +is specified, it is used as the +.Dq Host: +header to query a specific hostname at the target host. +To validate the HTTP return code, use this shell command: +.Bd -literal -offset indent +$ echo -n "HEAD <path> HTTP/1.0\er\en\er\en" | \e + nc <host> <port> | head -n1 +.Ed +.Pp +This prints the status header including the actual return code: +.Bd -literal -offset indent +HTTP/1.1 200 OK +.Ed +.It Xo +.Ic check https Ar path +.Op Ic host Ar hostname +.Ic code Ar number +.Xc +This has the same effect as above but wraps the HTTP request in SSL. +.It Xo +.Ic check http Ar path +.Op Ic host Ar hostname +.Ic digest Ar string +.Xc +For each host in the table, verify that retrieving the URL +.Ar path +produces non-binary content whose message digest matches the defined string. +The algorithm used is determined by the string length of the +.Ar digest +argument, either SHA1 (40 characters) or MD5 (32 characters). +If +.Ar hostname +is specified, it is used as the +.Dq Host: +header to query a specific hostname at the target host. +The digest does not take the HTTP headers into account. +Do not specify a binary object (such as a graphic) as the target of the +request, as +.Nm +expects the data returned to be a string. +To compute the digest, use this simple command: +.Bd -literal -offset indent +$ ftp -o - http://host[:port]/path | sha1 +.Ed +.Pp +This gives a digest that can be used as-is in a digest statement: +.Bd -literal -offset indent +a9993e36476816aba3e25717850c26c9cd0d89d +.Ed +.It Xo +.Ic check https Ar path +.Op Ic host Ar hostname +.Ic digest Ar string +.Xc +This has the same effect as above but wraps the HTTP request in SSL. +.It Ic check icmp +Ping hosts in this table to determine whether they are up or not. +This method will automatically use ICMP or ICMPV6 depending on the +address family of each host. +.It Ic check script Ar path +Execute an external program to check the host state. +The program will be executed for each host by specifying the hostname +on the command line: +.Bd -literal -offset indent +/usr/local/bin/checkload.pl front-www1.private.example.com +.Ed +.Pp +.Xr relayd 8 +expects a positive return value on success and zero on failure. +Note that the script will be executed with the privileges of the +.Qq _relayd +user and terminated after +.Ar timeout +milliseconds. +.It Xo +.Ic check send +.Ar data +.Ic expect +.Ar pattern +.Op Ic ssl +.Xc +For each host in the table, a TCP connection is established on the +port specified, then +.Ar data +is sent. +Incoming data is then read and is expected to match against +.Ar pattern +using shell globbing rules. +If +.Ar data +is an empty string or +.Ic nothing +then nothing is sent on the connection and data is immediately +read. +This can be useful with protocols that output a banner like +SMTP, NNTP, and FTP. +If the +.Ic ssl +keyword is present, +the transaction will occur in an SSL tunnel. +.It Ic check ssl +Perform a complete SSL handshake with each host to check their availability. +.It Ic check tcp +Use a simple TCP connect to check that hosts are up. +.El +.Pp +The following general table options are available: +.Bl -tag -width Ds +.It Ic demote Ar group +Enable the per-table +.Xr carp 4 +demotion option. +This will increment the carp demotion counter for the +specified interface group if all hosts in the table are down. +For more information on interface groups, +see the +.Ic group +keyword in +.Xr ifconfig 8 . +.It Ic interval Ar number +Override the global interval and specify one for this table. +It must be a multiple of the global interval. +.It Ic timeout Ar number +Set the timeout in milliseconds for each host that is checked using +TCP as the transport. +This will override the global timeout, which is 200 milliseconds by default. +.El +.Pp +The following options will set the scheduling algorithm to select a +host from the specified table: +.Bl -tag -width Ds +.It Ic mode hash +Balances the outgoing connections across the active hosts based on the +hashed name of the relay, the hashed name of the table, and the IP +address and port of the relay. +Additional input can be fed into the +hash by looking at HTTP headers and GET variables; +see the +.Sx PROTOCOLS +section below. +This mode is only supported by relays. +.It Ic mode least-states +Forward each outgoing connection to the active host with the least +active +.Xr pf 4 +states. +This mode is only supported by redirections. +.It Ic mode loadbalance +Balances the outgoing connections across the active hosts based on the +hashed name of the relay, the hashed name of the table, the source IP +address of the client, and the IP address and port of the relay. +This mode is only supported by relays. +.It Ic mode random +Distributes the outgoing connections randomly through all active hosts. +This mode is only supported by relays. +.It Ic mode roundrobin +Distributes the outgoing connections using a round-robin scheduler +through all active hosts. +This is the default mode and will be used if no option has been specified. +This mode is supported by redirections and relays. +.It Ic mode source-hash +Balances the outgoing connections across the active hosts based on the +hashed name of the redirection or relay, the hashed name of the table, +and the source IP address of the client. +This mode is only supported by relays. +.El +.Sh REDIRECTIONS +Redirections represent a +.Xr pf 4 +rdr-to rule. +They are used for stateful redirections to the hosts in the specified +tables. +.Xr pf 4 +rewrites the target IP addresses and ports of the incoming +connections, operating on layer 3. +The configuration directives that are valid in the +.Ic redirect +context are described below: +.Bl -tag -width Ds +.It Ic disable +The redirection is initially disabled. +It can be later enabled through +.Xr relayctl 8 . +.It Xo +.Ic forward to +.Aq Ar table +.Op Ic port Ar number +.Ar options ... +.Xc +Specify the tables of target hosts to be used; see the +.Sx TABLES +section above for information about table options. +If the +.Ic port +option is not specified, the first port from the +.Ic listen on +directive will be used. +This directive can be specified twice \(en the second entry will be used +as the backup table if all hosts in the main table are down. +At least one entry for the main table is mandatory. +.It Xo +.Ic listen on Ar address +.Op ip-proto +.Ic port Ar port +.Op Ic interface Ar name +.Xc +Specify an +.Ar address +and a +.Ar port +to listen on. +.Xr pf 4 +will redirect incoming connections for the specified target to the +hosts in the main or backup table. +The +.Ar port +argument can optionally specify a port range instead of a single port; +the format is +.Ar min-port : Ns Ar max-port . +The optional argument +.Ar ip-proto +can be used to specify an IP protocol like +.Ar tcp +or +.Ar udp ; +it defaults to +.Ar tcp . +The rule can be optionally restricted to a given interface name. +.It Xo +.Ic route to +.Aq Ar table +.Op Ic port Ar number +.Ar options ... +.Xc +Like the +.Ic forward to +directive, but directly routes the packets to the target host without +modifying the target address using a +.Xr pf 4 +route-to rule. +This can be used for +.Dq direct server return +to force the target host to respond via a different gateway. +Note that hosts have to accept sessions for the same address as +the gateway, which is typically done by configuring a loopback +interface on the host with this address. +.It Ic session timeout Ar seconds +Specify the inactivity timeout in seconds for established redirections. +The default timeout is 600 seconds (10 minutes). +The maximum is 2147483647 seconds (68 years). +.It Ic sticky-address +This has the same effect as specifying sticky-address +for an rdr-to rule in +.Xr pf.conf 5 . +It will ensure that multiple connections from the same source are +mapped to the same redirection address. +.It Xo +.Op Ic match +.Ic pftag Ar name +.Xc +Automatically tag packets passing through the +.Xr pf 4 +rdr-to rule with the name supplied. +This allows simpler filter rules. +The optional +.Ic match +keyword will change the default rule action from +.Ar pass in quick +to +.Ar match in +to allow further evaluation in the pf ruleset using the +.Ar tagged name +rule option. +.El +.Sh RELAYS +Relays will forward traffic between a client and a target server. +In contrast to redirections and IP forwarding in the network stack, a +relay will accept incoming connections from remote clients as a +server, open an outgoing connection to a target host, and forward +any traffic between the target host and the remote client, +operating on layer 7. +A relay is also called an application layer gateway or layer 7 proxy. +.Pp +The main purpose of a relay is to provide advanced load balancing +functionality based on specified protocol characteristics, such as +HTTP headers, to provide SSL acceleration and to allow +basic handling of the underlying application protocol. +.Pp +The +.Ic relay +configuration directives are described below: +.Bl -tag -width Ds +.It Ic disable +Start the relay but immediately close any accepted connections. +.It Xo +.Op Ic transparent +.Ic forward +.Op Ic with ssl +.Ic to +.Ar address +.Op Ic port Ar port +.Ar options ... +.Xc +Specify the address and port of the target host to connect to. +If the +.Ic port +option is not specified, the port from the +.Ic listen on +directive will be used. +Use the +.Ic transparent +keyword to enable fully-transparent mode; the source address of the +client will be retained in this case. +.Pp +The +.Ic with ssl +directive enables client-side SSL mode to connect to the remote host. +Verification of server certificates can be enabled by setting the +.Ic ca file +option in the protocol section. +.Pp +The following options may be specified for forward directives: +.Bl -tag -width Ds +.It Ic retry Ar number +The optional host +.Ic retry +option will be used as a tolerance for failed +host connections; the connection will be retried for +.Ar number +more times. +.It Ic inet +If the requested destination is an IPv6 address, +.Xr relayd 8 +will forward the connection to an IPv4 address which is determined by +the last 4 octets of the original IPv6 destination. +For example, if the original IPv6 destination address is +2001:db8:7395:ffff::a01:101, the session is relayed to the IPv4 +address 10.1.1.1 (a01:101). +.It Ic inet6 Ar address-prefix +If the requested destination is an IPv4 address, +.Xr relayd 8 +will forward the connection to an IPv6 address which is determined by +setting the last 4 octets of the specified IPv6 +.Ar address-prefix +to the 4 octets of the original IPv4 destination. +For example, if the original IPv4 destination address is 10.1.1.1 and +the specified address prefix is 2001:db8:7395:ffff::, the session is +relayed to the IPv6 address 2001:db8:7395:ffff::a01:101. +.El +.It Xo +.Ic forward to +.Aq Ar table +.Op Ic port Ar port +.Ar options ... +.Xc +Like the previous directive, but connect to a host from the specified +table; see the +.Sx TABLES +section above for information about table options. +This directive can be specified multiple times \(en subsequent entries +will be used as the backup table if all hosts in the previous table +are down. +At least one entry for the main table is mandatory. +.It Xo +.Ic forward to +.Ic destination +.Ar options ... +.Xc +When redirecting connections with a divert-to rule in +.Xr pf.conf 5 +to a relay listening on localhost, this directive will +look up the real destination address of the intended target host, +allowing the relay to be run as a transparent proxy. +If an additional +.Ic forward to +directive to a specified address or table is present, +it will be used as a backup if the lookup failed. +.It Xo +.Ic forward to +.Ic nat lookup +.Ar options ... +.Xc +Like the previous directive, but for redirections with rdr-to in +.Xr pf.conf 5 . +.It Xo +.Ic listen on Ar address +.Op Ic port Ar port +.Op Ic ssl +.Xc +Specify the address and port for the relay to listen on. +The relay will accept incoming connections to the specified address. +If the +.Ic port +option is not specified, the port from the +.Ic listen on +directive will be used. +.Pp +If the +.Ic ssl +keyword is present, the relay will accept connections using the +encrypted SSL protocol. +The relay will attempt to look up a private key in +.Pa /etc/ssl/private/address:port.key +and a public certificate in +.Pa /etc/ssl/address:port.crt , +where +.Ar address +is the specified IP address and +.Ar port +is the specified port that the relay listens on. +If these files are not present, the relay will continue to look in +.Pa /etc/ssl/private/address.key +and +.Pa /etc/ssl/address.crt . +See +.Xr ssl 8 +for details about SSL server certificates. +.It Ic protocol Ar name +Use the specified protocol definition for the relay. +The generic TCP protocol options will be used by default; +see the +.Sx PROTOCOLS +section below. +.It Ic session timeout Ar seconds +Specify the inactivity timeout in seconds for accepted sessions. +The default timeout is 600 seconds (10 minutes). +The maximum is 2147483647 seconds (68 years). +.El +.Sh SSL RELAYS +In addition to plain TCP, +.Xr relayd 8 +supports the Secure Sockets Layer (SSL) and Transport Layer Security +(TLS) cryptographic protocols for authenticated and encrypted relays. +TLS is the successor of the original SSL protocol but the term SSL can +refer to either of the protocols in +.Nm . +.Xr relayd 8 +can operate as an SSL client or server to offer a variety of options +for different use cases related to SSL. +.Bl -tag -width Ds +.It Ic SSL client +When configuring the relay +.Ic forward +statements with the +.Ic with ssl +directive, +.Xr relayd 8 +will enable client-side SSL to connect to the remote host. +This is commonly used for SSL tunneling and transparent encapsulation +of plain TCP connections. +See the +.Ic forward to +description in the +.Sx RELAYS +section for more details. +.It Ic SSL server +When specifying the +.Ic ssl +keyword in the relay +.Ic listen +statements, +.Xr relayd 8 +will accept connections from clients as an SSL server. +This mode is also known as +.Dq SSL acceleration . +See the +.Ic listen on +description in the +.Sx RELAYS +section for more details. +.It Ic SSL client and server +When combining both modes, SSL server and client, +.Xr relayd 8 +can filter SSL connections as a man-in-the-middle. +This combined mode is also called +.Dq SSL inspection . +The configuration requires additional X.509 certificate settings; +see the +.Ic ca key +description in the +.Sx PROTOCOLS +section for more details. +.El +.Pp +When configured for +.Dq SSL inspection +mode, +.Xr relayd 8 +will listen for incoming connections which have been diverted to the +local socket by PF. +Before accepting and negotiating the incoming SSL connection as a +server, it will look up the original destination address on the +diverted socket, and pre-connect to the target server as an SSL client +to obtain the remote SSL certificate. +It will update or patch the obtained SSL certificate by replacing the +included public key with its local server key because it doesn't have +the private key of the remote server certificate. +It also updates the X.509 issuer name to the local CA subject name and +signs the certificate with its local CA key. +This way it keeps all the other X.509 attributes that are already +present in the server certificate, including the "green bar" extended +validation attributes. +Now it finally accepts the SSL connection from the diverted client +using the updated certificate and continues to handle the connection +and to connect to the remote server. +.Sh PROTOCOLS +Protocols are templates defining settings and rules for relays. +They allow setting generic TCP options, SSL settings, and rules +for the selected application layer protocol. +.Pp +The protocol directive is available for a number of different +application layer protocols. +There is no generic handler for UDP-based protocols because it is a +stateless datagram-based protocol which has to look into the +application layer protocol to find any possible state information. +.Bl -tag -width Ds +.It Ic dns protocol +(UDP) +Domain Name System (DNS) protocol. +The requested IDs in the DNS header will be used to match the state. +.Xr relayd 8 +replaces these IDs with random values to compensate for +predictable values generated by some hosts. +.It Ic http protocol +Handle the HyperText Transfer Protocol +(HTTP, or "HTTPS" if encapsulated in an SSL tunnel). +.It Xo +.Op Ic tcp +.Ic protocol +.Xc +Generic handler for TCP-based protocols. +This is the default. +.El +.Pp +The available configuration directives are described below: +.Bl -tag -width Ds +.It Xo +.Pq Ic block Ns | Ns Ic pass Ns | Ns Ic match +.Op Ar rule +.Xc +Specify one or more rules to filter connections based on their +network or application layer headers; +see the +.Sx FILTER RULES +section for more details. +.It Ic return error Op Ar option +Return an error response to the client if an internal operation or the +forward connection to the client failed. +By default, the connection will be silently dropped. +The effect of this option depends on the protocol: HTTP will send an +error header and page to the client before closing the connection. +Additional valid options are: +.Bl -tag -width Ds +.It Ic style Ar string +Specify a Cascading Style Sheet (CSS) to be used for the returned +HTTP error pages, for example: +.Bd -literal -offset indent +body { background: #a00000; color: white; } +.Ed +.El +.It Ic ssl Ar option +Set the SSL options and session settings. +This is only used if SSL is enabled in the relay. +Valid options are: +.Bl -tag -width Ds +.It Ic ca cert Ar path +Specify a CA certificate for SSL inspection. +For more information, see the +.Ic ca key +option below. +.It Ic ca file Ar path +This option enables CA verification in SSL client mode. +The daemon will load the CA (Certificate Authority) certificates from +the specified path to verify the server certificates. +.Ox +provides a default CA bundle in +.Pa /etc/ssl/cert.pem . +.It Ic ca key Ar path Ic password Ar password +Specify a CA key for SSL inspection. +The +.Ar password +argument will specify the password to decrypt the CA key +(typically an RSA key). +This option will enable SSL inspection if the following conditions +are true: +.Pp +.Bl -bullet -compact -offset indent +.It +SSL client mode is enabled by the +.Ic listen +directive: +.Ic listen on ... ssl . +.It +SSL server mode and divert lookups are enabled by the +.Ic forward +directive: +.Ic forward with ssl to destination . +.It +The +.Ic ca cert +option is specified. +.It +The +.Ic ca key +option is specified. +.El +.It Ic ciphers Ar string +Set the string defining the SSL cipher suite. +If not specified, the default value +.Ar HIGH:!aNULL +will be used (strong crypto cipher suites without anonymous DH). +See the CIPHERS section of +.Xr openssl 1 +for information about SSL cipher suites and preference lists. +.It Oo Ic no Oc Ic cipher-server-preference +Prefer the server's cipher list over the client's preferences when +choosing a cipher for the connection; +disabled by default. +.It Oo Ic no Oc Ic client-renegotiation +Allow client-initiated renegotiation; +enabled by default. +Disable to mitigate a potential DoS risk. +.It Ic ecdh Op Ic curve Ar name +Set a named curve to use when generating EC keys for ECDHE-based +cipher suites with Perfect Forward Security (PFS). +If the curve +.Ar name +is not specified, the default curve +.Ar prime256v1 +will be used. +ECDHE is enabled by default. +.It Ic no ecdh +Disable ECDHE support. +.It Ic edh Op Ic params Ar maximum +Enable EDH-based cipher suites with Perfect Forward Security (PFS) for +older clients that do not support ECDHE. +If the +.Ar maximum +length of the DH params for EDH is not specified, the default value of +.Ar 1024 +bits will be used. +Other possible values are numbers between 1024 and 8192, including +.Ar 1024 , +.Ar 1536 , +.Ar 2048 , +.Ar 4096 , +or +.Ar 8192 . +Values higher than 1024 bits can cause incompatibilities with older +SSL clients. +.It Ic no edh +Disable EDH support. +This is the default. +.It Ic session cache Ar value +Set the maximum size of the SSL session cache. +If the +.Ar value +is zero, the default size defined by the SSL library will be used. +A positive number will set the maximum size in bytes and the keyword +.Ic disable +will disable the SSL session cache. +.It Xo +.Op Ic no +.Ic sslv2 +.Xc +Enable the SSLv2 protocol; +disabled by default. +.It Xo +.Op Ic no +.Ic sslv3 +.Xc +Disable the SSLv3 protocol; +enabled by default. +.It Xo +.Op Ic no +.Ic tlsv1 +.Xc +Disable the TLSv1/SSLv3.1 protocol; +enabled by default. +.El +.It Ic tcp Ar option +Enable or disable the specified TCP/IP options; see +.Xr tcp 4 +and +.Xr ip 4 +for more information about the options. +Valid options are: +.Bl -tag -width Ds +.It Ic backlog Ar number +Set the maximum length the queue of pending connections may grow to. +The backlog option is 10 by default and is limited by the +.Ic kern.somaxconn +.Xr sysctl 8 +variable. +.It Ic ip minttl Ar number +This option for the underlying IP connection may be used to discard packets +with a TTL lower than the specified value. +This can be used to implement the +.Ar Generalized TTL Security Mechanism (GTSM) +according to RFC 5082. +.It Ic ip ttl Ar number +Change the default time-to-live value in the IP headers. +.It Xo +.Op Ic no +.Ic nodelay +.Xc +Enable the TCP NODELAY option for this connection. +This is recommended to avoid delays in the relayed data stream, +e.g. for SSH connections. +.It Xo +.Op Ic no +.Ic sack +.Xc +Use selective acknowledgements for this connection. +.It Ic socket buffer Ar number +Set the socket-level buffer size for input and output for this +connection. +This will affect the TCP window size. +.It Xo +.Op Ic no +.Ic splice +.Xc +Use socket splicing for zero-copy data transfer. +This option is enabled by default. +.El +.El +.Sh FILTER RULES +Relays have the ability to filter connections based +on their network or application layer headers. +Filter rules apply options to connections based on the specified +filter parameters. +.Pp +For each connection that is processed by a relay, the filter rules are +evaluated in sequential order, from first to last. +For +.Ar block +and +.Ar pass , +the last matching rule decides what action is taken; +if no rule matches the connection, the default action is to establish +the connection without any additional action. +For +.Ar match , +rules are evaluated every time they match; +the pass/block state of a connection remains unchanged. +.Pp +The filter action may be one of the following: +.Bl -tag -width Ds +.It Ic block +The connection is blocked. +If a +.Ic block +rule matches a new connection attempt, it will not be established. +.Ic block +rules can also trigger for existing connections after evaluating +application layer parameters; +any connection of the relay session will be instantly dropped. +.It Ic match +The connection is matched. +This action does not alter the connection state, but allows +additional parameters to the connection. +.It Ic pass +The connection is passed; +.Xr relayd 8 +will continue to process the relay session normally. +.El +.Pp +These filter parameters can be used in the rules: +.Bl -tag -width Ds +.It Ic request No or Ic response +A relay session always consists of two connections: +the +.Ic request , +a client initiating a new connection to a server via the relay, +and the +.Ic response , +the server accepting the connection. +Depending on the protocol, +an established session can be purely request/response-based (like +HTTP), exchange data in a bidirectional way (like arbitrary TCP +sessions), or just contain a single datagram and an optional response +(like UDP-based protocols). +But the client always +.Ar requests +to communicate with a remote peer; the server. +.It Ic quick +If a connection is matched by a rule with the +.Ic quick +option set, +the rule is considered to be the last matching rule and any further +evaluation is skipped. +.It Ic inet No or Ic inet6 +Only match connections with the specified address family, +either of type IPv4 or IPv6. +.\" XXX .It Ic from +.\" XXX .It Ic to +.It Ic label Ar string +The label will be printed as part of the error message if the +.Ic return error +option is set and may contain HTML tags, for example: +.Bd -literal -offset indent +block request url digest 5c1e03f58f8ce0b457474ffb371fd1ef \e + label "\*(Lta href='http://example.com/adv.pl?id=7359'\*(Gt\e + Advisory provided by example.com\*(Lt/a\*(Gt" +.Ed +.It Ic no Ar parameter +Reset a sticky parameter that was previously set by a matching rule. +The +.Ar parameter +is a keyword that can be either +.Ic label +or +.Ic tag . +.It Ic tag Ar string +Add a "sticky" tag to connections matching this filter rule. +Tags can be used to filter the connection by further rules using the +.Ic tagged +option. +Only one tag is assigned per connection; +the tag will be replaced if the connection is already tagged. +.It Ic tagged Ar string +Match the connection if it is already tagged with a given tag by a +previous rule. +.El +.Pp +The following parameters are available when using the +.Ic http +protocol: +.Bl -tag -width Ds +.It Ic method Ar NAME +Match the HTTP request method. +The method is specified by +.Ar name +and can be either +.Ic CONNECT , +.Ic COPY , +.Ic DELETE , +.Ic GET , +.Ic HEAD , +.Ic LOCK , +.Ic MKCOL , +.Ic MOVE , +.Ic OPTIONS , +.Ic PATCH , +.Ic POST , +.Ic PROPFIND , +.Ic PROPPATCH , +.Ic PUT , +.Ic TRACE , +or +.Ic UNLOCK . +.It Xo +.Ar type Ar option +.Oo Oo Ic digest Oc +.Pq Ar key Ns | Ns Ic file Ar path +.Oo Ic value Ar value Oc Oc +.Xc +Match a specified HTTP header entity and an optional +.Ic key +and +.Ic value . +An +.Ic option +can be specified to modify the matched entity or to trigger an event. +The entity is extracted from the HTTP request or response header and +can be either of +.Ar type +.Ic cookie , +.Ic header , +.Ic path , +.Ic query , +or +.Ic url . +.Pp +Instead of a single +.Ar key , +multiple keys can be loaded from a +.Ic file +specified by +.Ar path +that contains one key per line. +Lines will be stripped at the first whitespace or newline character +and any empty lines or lines beginning with a hash mark (`#') will be +ignored. +.Pp +If the +.Ic digest +keyword is specified, +compare the message digest of the key against the defined string. +The algorithm used is determined by the string length of the +.Ar key +argument, either SHA1 (40 characters) or MD5 (32 characters). +To compute the digest, +for example for a +.Ic url , +use this simple command: +.Bd -literal -offset indent +$ echo -n "example.com/path/?args" | sha1 +.Ed +.El +.Pp +.Bq Ar type +may be one of: +.Bl -tag -width Ds +.It Ic cookie Ar option Oo Ar key Oo Ic value Ar value Oc Oc +Look up the entity as a value in the Cookie header. +This type is only available with the direction +.Ic request . +.It Ic header Ar option Oo Ar key Oo Ic value Ar value Oc Oc +Look up the entity in the application protocol headers, like HTTP +headers in +.Ic http +mode. +.It Ic path Ar option Oo Ar key Oo Ic value Ar value Oc Oc +Look up the entity as a value in the URL path when using the +.Ic http +protocol. +This type is only available with the direction +.Ic request . +The +.Ar key +will match the path of the requested URL without the hostname +and query and the value will match the complete query, +for example: +.Bd -literal -offset indent +block path "/index.html" +block path "/cgi-bin/t.cgi" value "foo=bar*" +.Ed +.It Ic query Ar option Oo Ar key Oo Ic value Ar value Oc Oc +Look up the entity as a query variable in the URL when using the +.Ic http +protocol. +This type is only available with the direction +.Ic request , +for example: +.Bd -literal -offset indent +# Will match /cgi-bin/example.pl?foo=bar&ok=yes +request query expect "bar" from "foo" +.Ed +.It Ic url Ar option Oo Oo Ic digest Oc Ar key Oo Ic value Ar value Oc Oc +Look up the entity as a URL suffix/prefix expression consisting of a +canonicalized hostname without port or suffix and a path name or +prefix when using the +.Ic http +protocol. +This type is only available with the direction +.Ic request , +for example: +.Bd -literal -offset indent +block url "example.com/index.html" +block url "example.com/test.cgi?val=1" +.Ed +.Pp +.Xr relayd 8 +will match the full URL and different possible suffix/prefix +combinations by stripping subdomains and path components (up to 5 +levels), and the query string. +For example, the following +lookups will be done for +.Ar http://www.example.com:81/1/2/3/4/5.html?query=yes : +.Bd -literal -offset indent +www.example.com/1/2/3/4/5.html?query=yes +www.example.com/1/2/3/4/5.html +www.example.com/ +www.example.com/1/ +www.example.com/1/2/ +www.example.com/1/2/3/ +example.com/1/2/3/4/5.html?query=yes +example.com/1/2/3/4/5.html +example.com/ +example.com/1/ +example.com/1/2/ +example.com/1/2/3/ +.Ed +.El +.Pp +.Bq Ar option +may be one of: +.Bl -tag -width Ds +.It Ic append +Append the specified +.Ar value +to a protocol entity with the selected +.Ar key +name. +If it does not exist, it will be created with the new value. +.Pp +The value string may contain predefined macros that will be expanded +at runtime: +.Pp +.Bl -tag -width $SERVER_ADDR -offset indent -compact +.It Ic $REMOTE_ADDR +The IP address of the connected client. +.It Ic $REMOTE_PORT +The TCP source port of the connected client. +.It Ic $SERVER_ADDR +The configured IP address of the relay. +.It Ic $SERVER_PORT +The configured TCP server port of the relay. +.It Ic $SERVER_NAME +The server software name of +.Xr relayd 8 . +.It Ic $TIMEOUT +The configured session timeout of the relay. +.El +.It Ic hash +Feed the +.Ar value +of the selected entity into the load balancing hash to select the +target host. +See the +.Ic table +keyword in the +.Sx RELAYS +section above. +.It Ic log +Log the +.Ar key +name and the +.Ar value +of the entity. +.It Ic remove +Remove the entity with the selected +.Ar key +name. +.It Ic set +Like the +.Ic append +directive above, but change the contents of the specified entity. +If +.Ar key +does not exist in the request, it will be created with the new +.Ar value . +.Pp +The +.Ar value +string +may contain predefined macros that will be expanded at runtime, +as detailed for the +.Ic append +directive above. +.El +.Sh ROUTERS +Routers represent routing table entries in the kernel forwarding +database, see +.Xr route 4 , +and a table of associated gateways. +They are used to dynamically insert or remove routes with gateways +based on their availability and health-check results. +A router can include multiple network statements and a single forward +statement with a table of one or more gateways. +All entries in a single router directive must match the same address +family, either IPv4 or IPv6. +.Pp +The kernel supports multipath routing when multiple gateways exist to +the same destination address. +The multipath routing behaviour can be changed globally using the +.Xr sysctl 8 +variables +.Va net.inet.ip.multipath +and +.Va net.inet6.ip6.multipath . +With the default setting of 0, +the first route selected will be used for subsequent packets to that +destination regardless of source. +Setting it to 1 will enable load balancing based on the packet source +address across gateways; multiple routes with the same priority are +used equally. +The kernel will also check the link state of the related network +interface and try a different route if it is not active. +.Pp +The configuration directives that are valid in the +.Ic routers +context are described below: +.Bl -tag -width Ds +.It Xo +.Ic forward to +.Aq Ar table +.Ic port Ar number +.Ar options ... +.Xc +Specify the table of target gateways to be used; see the +.Sx TABLES +section above for information about table options. +This entry is mandatory and must be specified once. +.It Xo +.Ic route +.Ar address Ns Li / Ns Ar prefix +.Xc +Specify the network address and prefix length of a route destination +that is reachable via the active gateways. +This entry must be specified at least once in a router directive. +.It Ic rtable Ar id +Add the routes to the kernel routing table with the specified +.Ar id . +.It Ic rtlabel Ar label +Add the routes with the specified +.Ar label +to the kernel routing table. +.El +.Sh FILES +.Bl -tag -width Ds -compact +.It Pa /etc/relayd.conf +.Xr relayd 8 +configuration file. +.Pp +.It Pa /etc/services +Service name database. +.Pp +.It Pa /etc/ssl/address.crt +.It Pa /etc/ssl/address:port.crt +.It Pa /etc/ssl/private/address.key +.It Pa /etc/ssl/private/address:port.key +Location of the relay SSL server certificates, where +.Ar address +is the configured IP address +and +.Ar port +is the configured port number of the relay. +.Pp +.It Pa /etc/ssl/cert.pem +Default location of the CA bundle that can be used with +.Xr relayd 8 . +.El +.Sh EXAMPLES +This configuration file would create a redirection service +.Dq www +which load balances four hosts +and falls back to one host containing a +.Dq sorry page : +.Bd -literal -offset indent +www1=front-www1.private.example.com +www2=front-www2.private.example.com +www3=front-www3.private.example.com +www4=front-www4.private.example.com + +interval 5 + +table \*(Ltphphosts\*(Gt { $www1, $www2, $www3, $www4 } +table \*(Ltsorryhost\*(Gt disable { sorryhost.private.example.com } + +redirect "www" { + listen on www.example.com port 8080 interface trunk0 + listen on www6.example.com port 80 interface trunk0 + + pftag REDIRECTED + + forward to \*(Ltphphosts\*(Gt port 8080 timeout 300 \e + check http "/" digest "630aa3c2f..." + forward to \*(Ltsorryhost\*(Gt port 8080 timeout 300 check icmp +} +.Ed +.Pp +It is possible to specify multiple listen directives with different IP +protocols in a single redirection configuration: +.Bd -literal -offset indent +redirect "dns" { + listen on dns.example.com tcp port 53 + listen on dns.example.com udp port 53 + + forward to \*(Ltdnshosts\*(Gt port 53 check tcp +} +.Ed +.Pp +The following configuration would add a relay to forward +secure HTTPS connections to a pool of HTTP webservers +using the +.Ic loadbalance +mode (SSL acceleration and layer 7 load balancing). +The HTTP protocol definition will add two HTTP headers containing +address information of the client and the server, set the +.Dq Keep-Alive +header value to the configured session timeout, +and include the +.Dq sessid +variable in the hash to calculate the target host: +.Bd -literal -offset indent +http protocol "http_ssl" { + match header append "X-Forwarded-For" \e + value "$REMOTE_ADDR" + match header append "X-Forwarded-By" \e + value "$REMOTE_ADDR:$SERVER_PORT" + match header set "Keep-Alive" value "$TIMEOUT" + + match query hash "sessid" + match hash "sessid" + + pass + block path "/cgi-bin/index.cgi" value "*command=*" + + ssl { sslv2, ciphers "MEDIUM:HIGH" } +} + +relay "sslaccel" { + listen on www.example.com port 443 ssl + protocol "http_ssl" + forward to \*(Ltphphosts\*(Gt port 8080 mode loadbalance check tcp +} +.Ed +.Pp +The second relay example will accept incoming connections to port +2222 and forward them to a remote SSH server. +The TCP +.Ic nodelay +option will allow a +.Dq smooth +SSH session without delays between keystrokes or displayed output on +the terminal: +.Bd -literal -offset indent +protocol "myssh" { + tcp { nodelay, socket buffer 65536 } +} + +relay "sshforward" { + listen on www.example.com port 2222 + protocol "myssh" + forward to shell.example.com port 22 +} +.Ed +.Pp +The following relay example will configure +.Dq SSL inspection +as described in the +.Sx SSL RELAYS +section. +To start, first generate a new local CA key and certificate: +.Bd -literal -offset indent +# openssl req -x509 -days 365 -newkey rsa:2048 \e + -keyout /etc/ssl/private/ca.key -out /etc/ssl/ca.crt +.Ed +.Pp +An SSL server key and self-signed cert for 127.0.0.1 are also required; +see +.Ic listen on +in the +.Sx RELAYS +section for more details about certificate locations. +Configure the packet filter with a matching divert rule in +.Xr pf.conf 5 : +.Bd -literal -offset indent +# Divert incoming HTTPS traffic to relayd +pass in on vlan1 inet proto tcp to port 443 \e + divert-to localhost port 8443 +.Ed +.Pp +And finally configure the SSL inspection in +.Nm : +.Bd -literal -offset indent +http protocol httpfilter { + return error + + pass + match label "Prohibited!" + block url "social.network.example.com/" + + # New configuration directives for SSL Interception + ssl ca key "/etc/ssl/private/ca.key" password "password123" + ssl ca cert "/etc/ssl/ca.crt" +} + +relay sslinspect { + listen on 127.0.0.1 port 8443 ssl + protocol httpfilter + forward with ssl to destination +} +.Ed +.Pp +The next simple router configuration example can be used to run +redundant, health-checked WAN links: +.Bd -literal -offset indent +table \*(Ltgateways\*(Gt { $gw1 ip ttl 1, $gw2 ip ttl 1 } +router "uplinks" { + route 0.0.0.0/0 + forward to \*(Ltgateways\*(Gt check icmp +} +.Ed +.Sh SEE ALSO +.Xr relayctl 8 , +.Xr relayd 8 , +.Xr snmpd 8 , +.Xr ssl 8 +.Sh HISTORY +The +.Nm +file format, formerly known as +.Ic hoststated.conf , +first appeared in +.Ox 4.1 . +It was renamed to +.Nm +in +.Ox 4.3 . +.Sh AUTHORS +.An -nosplit +The +.Xr relayd 8 +program was written by +.An Pierre-Yves Ritschard Aq Mt pyr@openbsd.org +and +.An Reyk Floeter Aq Mt reyk@openbsd.org . +.Sh CAVEATS +.Xr relayd 8 +Verification of SSL server certificates is based on a static CA bundle +and +.Xr relayd 8 +currently does not support CRLs (Certificate Revocation Lists). @@ -0,0 +1,395 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2006 - 2014 Reyk Floeter <reyk@openbsd.org> + * Copyright (c) 2006, 2007 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _HTTPD_H +#define _HTTPD_H + +#include <sys/tree.h> + +#include <sys/param.h> /* MAXHOSTNAMELEN */ +#include <limits.h> +#include <imsg.h> + +#define CONF_FILE "/etc/httpd.conf" +#define HTTPD_SOCKET "/var/run/httpd.sock" +#define HTTPD_USER "www" +#define HTTPD_SERVERNAME "OpenBSD httpd" +#define FD_RESERVE 5 + +#define SERVER_MAX_CLIENTS 1024 +#define SERVER_TIMEOUT 600 +#define SERVER_CACHESIZE -1 /* use default size */ +#define SERVER_NUMPROC 3 +#define SERVER_MAXPROC 32 +#define SERVER_MAXHEADERLENGTH 8192 +#define SERVER_BACKLOG 10 +#define SERVER_OUTOF_FD_RETRIES 5 + +#define CONFIG_RELOAD 0x00 +#define CONFIG_SERVERS 0x01 +#define CONFIG_ALL 0xff + +#define TCPFLAG_NODELAY 0x01 +#define TCPFLAG_NNODELAY 0x02 +#define TCPFLAG_SACK 0x04 +#define TCPFLAG_NSACK 0x08 +#define TCPFLAG_BUFSIZ 0x10 +#define TCPFLAG_IPTTL 0x20 +#define TCPFLAG_IPMINTTL 0x40 +#define TCPFLAG_NSPLICE 0x80 +#define TCPFLAG_DEFAULT 0x00 + +#define TCPFLAG_BITS \ + "\10\01NODELAY\02NO_NODELAY\03SACK\04NO_SACK" \ + "\05SOCKET_BUFFER_SIZE\06IP_TTL\07IP_MINTTL\10NO_SPLICE" + +enum httpchunk { + TOREAD_UNLIMITED = -1, + TOREAD_HTTP_HEADER = -2, + TOREAD_HTTP_CHUNK_LENGTH = -3, + TOREAD_HTTP_CHUNK_TRAILER = -4 +}; + +#if DEBUG > 1 +#define DPRINTF log_debug +#define DEBUG_CERT 1 +#else +#define DPRINTF(x...) do {} while(0) +#endif + +struct ctl_flags { + u_int8_t cf_opts; + u_int32_t cf_flags; +}; + +struct portrange { + in_port_t val[2]; + u_int8_t op; +}; + +struct address { + struct sockaddr_storage ss; + int ipproto; + struct portrange port; + char ifname[IFNAMSIZ]; + TAILQ_ENTRY(address) entry; +}; +TAILQ_HEAD(addresslist, address); + +/* initially control.h */ +struct control_sock { + const char *cs_name; + struct event cs_ev; + struct event cs_evt; + int cs_fd; + int cs_restricted; + void *cs_env; + + TAILQ_ENTRY(control_sock) cs_entry; +}; +TAILQ_HEAD(control_socks, control_sock); + +struct { + struct event ev; + int fd; +} control_state; + +enum blockmodes { + BM_NORMAL, + BM_NONBLOCK +}; + +struct imsgev { + struct imsgbuf ibuf; + void (*handler)(int, short, void *); + struct event ev; + struct privsep_proc *proc; + void *data; + short events; +}; + +#define IMSG_SIZE_CHECK(imsg, p) do { \ + if (IMSG_DATA_SIZE(imsg) < sizeof(*p)) \ + fatalx("bad length imsg received"); \ +} while (0) +#define IMSG_DATA_SIZE(imsg) ((imsg)->hdr.len - IMSG_HEADER_SIZE) + +struct ctl_conn { + TAILQ_ENTRY(ctl_conn) entry; + u_int8_t flags; + u_int waiting; +#define CTL_CONN_NOTIFY 0x01 + struct imsgev iev; + +}; +TAILQ_HEAD(ctl_connlist, ctl_conn); + +enum imsg_type { + IMSG_NONE, + IMSG_CTL_OK, + IMSG_CTL_FAIL, + IMSG_CTL_VERBOSE, + IMSG_CTL_RESET, + IMSG_CTL_SHUTDOWN, + IMSG_CTL_RELOAD, + IMSG_CTL_NOTIFY, + IMSG_CTL_END, + IMSG_CTL_START, + IMSG_CFG_DONE +}; + +enum privsep_procid { + PROC_ALL = -1, + PROC_PARENT = 0, + PROC_SERVER, + PROC_MAX +} privsep_process; + +/* Attach the control socket to the following process */ +#define PROC_CONTROL PROC_PARENT + +struct privsep_pipes { + int *pp_pipes[PROC_MAX]; +}; + +struct privsep { + struct privsep_pipes *ps_pipes[PROC_MAX]; + struct privsep_pipes *ps_pp; + + struct imsgev *ps_ievs[PROC_MAX]; + const char *ps_title[PROC_MAX]; + pid_t ps_pid[PROC_MAX]; + u_int8_t ps_what[PROC_MAX]; + + u_int ps_instances[PROC_MAX]; + u_int ps_ninstances; + u_int ps_instance; + + struct control_sock ps_csock; + struct control_socks ps_rcsocks; + + /* Event and signal handlers */ + struct event ps_evsigint; + struct event ps_evsigterm; + struct event ps_evsigchld; + struct event ps_evsighup; + struct event ps_evsigpipe; + + int ps_noaction; + struct passwd *ps_pw; + struct httpd *ps_env; +}; + +struct privsep_proc { + const char *p_title; + enum privsep_procid p_id; + int (*p_cb)(int, struct privsep_proc *, + struct imsg *); + pid_t (*p_init)(struct privsep *, + struct privsep_proc *); + void (*p_shutdown)(void); + u_int p_instance; + const char *p_chroot; + struct privsep *p_ps; + struct httpd *p_env; +}; + +struct client { + u_int32_t clt_id; + pid_t clt_pid; + void *clt_server; + u_int32_t clt_serverid; + + int clt_s; + in_port_t clt_port; + struct sockaddr_storage clt_ss; + struct bufferevent *clt_bev; + struct evbuffer *clt_output; + struct event clt_ev; + + off_t clt_toread; + int clt_line; + size_t clt_headerlen; + int clt_done; + + struct evbuffer *clt_log; + struct timeval clt_timeout; + struct timeval clt_tv_start; + struct timeval clt_tv_last; + struct event clt_inflightevt; + + SPLAY_ENTRY(client) clt_nodes; +}; +SPLAY_HEAD(client_tree, client); + +struct server_config { + u_int32_t id; + u_int32_t flags; + char name[MAXHOSTNAMELEN]; + in_port_t port; + struct sockaddr_storage ss; + struct timeval timeout; +}; + +struct server { + TAILQ_ENTRY(server) srv_entry; + struct server_config srv_conf; + + u_int8_t srv_tcpflags; + int srv_tcpbufsiz; + int srv_tcpbacklog; + u_int8_t srv_tcpipttl; + u_int8_t srv_tcpipminttl; + + int srv_s; + in_port_t src_port; + struct sockaddr_storage srv_ss; + struct bufferevent *srv_bev; + int srv_dsts; + struct bufferevent *srv_dstbev; + + struct event srv_ev; + struct event srv_evt; + + struct client_tree srv_clients; +}; +TAILQ_HEAD(serverlist, server); + +struct httpd { + u_int8_t sc_opts; + u_int32_t sc_flags; + const char *sc_conffile; + struct event sc_ev; + u_int16_t sc_prefork_server; + u_int16_t sc_id; + + struct serverlist *sc_servers; + + struct privsep *sc_ps; + int sc_reload; +}; + +#define HTTPD_OPT_VERBOSE 0x01 +#define HTTPD_OPT_NOACTION 0x04 +#define HTTPD_OPT_LOGUPDATE 0x08 +#define HTTPD_OPT_LOGNOTIFY 0x10 +#define HTTPD_OPT_LOGALL 0x18 + +/* control.c */ +int control_init(struct privsep *, struct control_sock *); +int control_listen(struct control_sock *); +void control_cleanup(struct control_sock *); +void control_dispatch_imsg(int, short, void *); +void control_imsg_forward(struct imsg *); +struct ctl_conn * + control_connbyfd(int); +void socket_set_blockmode(int, enum blockmodes); + +extern struct ctl_connlist ctl_conns; + +/* parse.y */ +int parse_config(const char *, struct httpd *); +int load_config(const char *, struct httpd *); +int cmdline_symset(char *); + +/* server.c */ +pid_t server(struct privsep *, struct privsep_proc *); +int server_privinit(struct server *); +int server_socket_af(struct sockaddr_storage *, in_port_t); +in_port_t + server_socket_getport(struct sockaddr_storage *); +void server_write(struct bufferevent *, void *); +void server_read(struct bufferevent *, void *); +void server_error(struct bufferevent *, short, void *); +void server_close(struct client *, const char *); +void server_dump(struct client *, const void *, size_t); +int server_client_cmp(struct client *, struct client *); +int server_bufferevent_print(struct client *, const char *); +int server_bufferevent_write_buffer(struct client *, + struct evbuffer *); +int server_bufferevent_write_chunk(struct client *, + struct evbuffer *, size_t); +int server_bufferevent_add(struct event *, int); +int server_bufferevent_write(struct client *, void *, size_t); + +SPLAY_PROTOTYPE(client_tree, client, clt_nodes, server_client_cmp); + +/* httpd.c */ +void event_again(struct event *, int, short, + void (*)(int, short, void *), + struct timeval *, struct timeval *, void *); +const char *canonicalize_host(const char *, char *, size_t); +void imsg_event_add(struct imsgev *); +int imsg_compose_event(struct imsgev *, u_int16_t, u_int32_t, + pid_t, int, void *, u_int16_t); +void socket_rlimit(int); +char *get_string(u_int8_t *, size_t); +void *get_data(u_int8_t *, size_t); +int accept_reserve(int, struct sockaddr *, socklen_t *, int, + volatile int *); + +/* log.c */ +void log_init(int); +void log_verbose(int); +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 vlog(int, const char *, va_list) __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *); +__dead void fatalx(const char *); +const char *print_host(struct sockaddr_storage *, char *, size_t); +const char *print_time(struct timeval *, struct timeval *, char *, size_t); +const char *printb_flags(const u_int32_t, const char *); +void getmonotime(struct timeval *); + +/* proc.c */ +void proc_init(struct privsep *, struct privsep_proc *, u_int); +void proc_kill(struct privsep *); +void proc_listen(struct privsep *, struct privsep_proc *, size_t); +void proc_dispatch(int, short event, void *); +pid_t proc_run(struct privsep *, struct privsep_proc *, + struct privsep_proc *, u_int, + void (*)(struct privsep *, struct privsep_proc *, void *), void *); +void proc_range(struct privsep *, enum privsep_procid, int *, int *); +int proc_compose_imsg(struct privsep *, enum privsep_procid, int, + u_int16_t, int, void *, u_int16_t); +int proc_composev_imsg(struct privsep *, enum privsep_procid, int, + u_int16_t, int, const struct iovec *, int); +int proc_forward_imsg(struct privsep *, struct imsg *, + enum privsep_procid, int); +struct imsgbuf * + proc_ibuf(struct privsep *, enum privsep_procid, int); +struct imsgev * + proc_iev(struct privsep *, enum privsep_procid, int); +void imsg_event_add(struct imsgev *); +int imsg_compose_event(struct imsgev *, u_int16_t, u_int32_t, + pid_t, int, void *, u_int16_t); +int imsg_composev_event(struct imsgev *, u_int16_t, u_int32_t, + pid_t, int, const struct iovec *, int); + +/* config.c */ +int config_init(struct httpd *); +void config_purge(struct httpd *, u_int); +int config_setreset(struct httpd *, u_int); +int config_getreset(struct httpd *, struct imsg *); +int config_getcfg(struct httpd *, struct imsg *); + +#endif /* _HTTPD_H */ @@ -0,0 +1,251 @@ +/* $OpenBSD: log.c,v 1.22 2014/04/18 16:13:02 reyk Exp $ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/tree.h> + +#include <net/if.h> +#include <netinet/in_systm.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <event.h> +#include <netdb.h> +#include <ctype.h> + +#include <openssl/ssl.h> + +#include "httpd.h" + +int debug; +int verbose; + +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); + +void +log_init(int n_debug) +{ + extern char *__progname; + + debug = n_debug; + verbose = n_debug; + + if (!debug) + openlog(__progname, LOG_PID | LOG_NDELAY, LOG_DAEMON); + + tzset(); +} + +void +log_verbose(int v) +{ + verbose = v; +} + +void +logit(int pri, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vlog(pri, fmt, ap); + va_end(ap); +} + +void +vlog(int pri, const char *fmt, va_list ap) +{ + char *nfmt; + + if (debug) { + /* best effort in out of mem situations */ + if (asprintf(&nfmt, "%s\n", fmt) == -1) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } else { + vfprintf(stderr, nfmt, ap); + free(nfmt); + } + fflush(stderr); + } else + vsyslog(pri, fmt, ap); +} + + +void +log_warn(const char *emsg, ...) +{ + char *nfmt; + va_list ap; + + /* best effort to even work in out of memory situations */ + if (emsg == NULL) + logit(LOG_CRIT, "%s", strerror(errno)); + else { + va_start(ap, emsg); + + if (asprintf(&nfmt, "%s: %s", emsg, strerror(errno)) == -1) { + /* we tried it... */ + vlog(LOG_CRIT, emsg, ap); + logit(LOG_CRIT, "%s", strerror(errno)); + } else { + vlog(LOG_CRIT, nfmt, ap); + free(nfmt); + } + va_end(ap); + } +} + +void +log_warnx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_CRIT, 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); + } +} + +void +fatal(const char *emsg) +{ + if (emsg == NULL) + logit(LOG_CRIT, "fatal: %s", strerror(errno)); + else + if (errno) + logit(LOG_CRIT, "fatal: %s: %s", + emsg, strerror(errno)); + else + logit(LOG_CRIT, "fatal: %s", emsg); + + exit(1); +} + +void +fatalx(const char *emsg) +{ + errno = 0; + fatal(emsg); +} + +const char * +print_host(struct sockaddr_storage *ss, char *buf, size_t len) +{ + if (getnameinfo((struct sockaddr *)ss, ss->ss_len, + buf, len, NULL, 0, NI_NUMERICHOST) != 0) { + buf[0] = '\0'; + return (NULL); + } + return (buf); +} + +const char * +print_time(struct timeval *a, struct timeval *b, char *buf, size_t len) +{ + struct timeval tv; + u_long h, sec, min; + + timerclear(&tv); + timersub(a, b, &tv); + sec = tv.tv_sec % 60; + min = tv.tv_sec / 60 % 60; + h = tv.tv_sec / 60 / 60; + + snprintf(buf, len, "%.2lu:%.2lu:%.2lu", h, min, sec); + return (buf); +} + +const char * +printb_flags(const u_int32_t v, const char *bits) +{ + static char buf[2][BUFSIZ]; + static int idx = 0; + int i, any = 0; + char c, *p, *r; + + p = r = buf[++idx % 2]; + bzero(p, BUFSIZ); + + if (bits) { + bits++; + while ((i = *bits++)) { + if (v & (1 << (i - 1))) { + if (any) { + *p++ = ','; + *p++ = ' '; + } + any = 1; + for (; (c = *bits) > 32; bits++) { + if (c == '_') + *p++ = ' '; + else + *p++ = tolower((u_char)c); + } + } else + for (; *bits > 32; bits++) + ; + } + } + + return (r); +} + +void +getmonotime(struct timeval *tv) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + fatal("clock_gettime"); + + TIMESPEC_TO_TIMEVAL(tv, &ts); +} @@ -0,0 +1,1019 @@ +/* $OpenBSD: parse.y,v 1.187 2014/07/11 17:35:16 reyk Exp $ */ + +/* + * Copyright (c) 2007 - 2014 Reyk Floeter <reyk@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2006 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org> + * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org> + * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org> + * 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 <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/queue.h> +#include <sys/ioctl.h> +#include <sys/hash.h> + +#include <net/if.h> +#include <net/pfvar.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <arpa/nameser.h> +#include <net/route.h> + +#include <ctype.h> +#include <unistd.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <limits.h> +#include <stdint.h> +#include <stdarg.h> +#include <stdio.h> +#include <netdb.h> +#include <string.h> +#include <ifaddrs.h> +#include <syslog.h> + +#include <openssl/ssl.h> + +#include "httpd.h" +#include "http.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 yyerror(const char *, ...); +int kw_cmp(const void *, const void *); +int lookup(char *); +int lgetc(int); +int lungetc(int); +int findeol(void); + +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 *); + +struct httpd *conf = NULL; +static int errors = 0; +static int loadcfg = 0; +uint32_t last_server_id = 0; + +static struct server *srv = NULL; +struct serverlist servers; + +struct address *host_v4(const char *); +struct address *host_v6(const char *); +int host_dns(const char *, struct addresslist *, + int, struct portrange *, const char *, int); +int host_if(const char *, struct addresslist *, + int, struct portrange *, const char *, int); +int host(const char *, struct addresslist *, + int, struct portrange *, const char *, int); +void host_free(struct addresslist *); +int getservice(char *); +int is_if_in_group(const char *, const char *); + +typedef struct { + union { + int64_t number; + char *string; + struct timeval tv; + struct portrange port; + struct { + struct sockaddr_storage ss; + char name[MAXHOSTNAMELEN]; + } addr; + } v; + int lineno; +} YYSTYPE; + +%} + +%token ALL PORT LISTEN PREFORK SERVER ERROR INCLUDE LOG VERBOSE +%token UPDATES INCLUDE +%token <v.string> STRING +%token <v.number> NUMBER +%type <v.number> loglevel + +%% + +grammar : /* empty */ + | grammar include '\n' + | grammar '\n' + | grammar varset '\n' + | grammar main '\n' + | grammar server '\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); + } + ; + +main : LOG loglevel { + if (loadcfg) + break; + conf->sc_opts |= $2; + } + | PREFORK NUMBER { + if (loadcfg) + break; + if ($2 <= 0 || $2 > SERVER_MAXPROC) { + yyerror("invalid number of preforked " + "servers: %d", $2); + YYERROR; + } + conf->sc_prefork_server = $2; + } + ; + +server : SERVER STRING { + free($2); + } + ; + +loglevel : UPDATES { $$ = HTTPD_OPT_LOGUPDATE; } + | ALL { $$ = HTTPD_OPT_LOGALL; } + ; + +comma : ',' + | nl + | /* empty */ + ; + +optnl : '\n' optnl + | + ; + +nl : '\n' optnl + ; + +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +int +yyerror(const char *fmt, ...) +{ + va_list ap; + char *nfmt; + + file->errors++; + va_start(ap, fmt); + if (asprintf(&nfmt, "%s:%d: %s", file->name, yylval.lineno, fmt) == -1) + fatalx("yyerror asprintf"); + vlog(LOG_CRIT, nfmt, ap); + va_end(ap); + free(nfmt); + 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[] = { + { "all", ALL }, + { "include", INCLUDE }, + { "listen", LISTEN }, + { "log", LOG }, + { "port", PORT }, + { "prefork", PREFORK }, + { "server", SERVER }, + { "updates", UPDATES } + }; + 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 + +u_char *parsebuf; +int parseindex; +u_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; + + /* skip to either EOF or the first real EOL */ + while (1) { + if (pushback_index) + c = pushback_buffer[--pushback_index]; + else + c = lgetc(0); + if (c == '\n') { + file->lineno++; + break; + } + if (c == EOF) + break; + } + return (ERROR); +} + +int +yylex(void) +{ + u_char buf[8096]; + u_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; + } + 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); + } + } + +#define allowed_in_string(x) \ + (isalnum(x) || (ispunct(x) && 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("cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + log_warnx("%s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) { + log_warnx("%s: group writable or world read/writable", 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("%s: malloc", __func__); + return (NULL); + } + if ((nfile->name = strdup(name)) == NULL) { + log_warn("%s: malloc", __func__); + free(nfile); + return (NULL); + } + if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { + log_warn("%s: %s", __func__, 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); +} + +int +parse_config(const char *filename, struct httpd *x_conf) +{ + struct sym *sym, *next; + + conf = x_conf; + if (config_init(conf) == -1) { + log_warn("%s: cannot initialize configuration", __func__); + return (-1); + } + + errors = 0; + + if ((file = pushfile(filename, 0)) == NULL) + return (-1); + + topfile = file; + setservent(1); + + yyparse(); + errors = file->errors; + popfile(); + + endservent(); + endprotoent(); + + /* Free macros */ + for (sym = TAILQ_FIRST(&symhead); sym != NULL; sym = next) { + next = TAILQ_NEXT(sym, entry); + if (!sym->persist) { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + + return (errors ? -1 : 0); +} + +int +load_config(const char *filename, struct httpd *x_conf) +{ + struct sym *sym, *next; + + conf = x_conf; + conf->sc_flags = 0; + + loadcfg = 1; + errors = 0; + last_server_id = 0; + + srv = NULL; + + if ((file = pushfile(filename, 0)) == NULL) + return (-1); + + topfile = file; + setservent(1); + + yyparse(); + errors = file->errors; + popfile(); + + endservent(); + endprotoent(); + + /* Free macros and check which have not been used. */ + for (sym = TAILQ_FIRST(&symhead); sym != NULL; sym = next) { + next = TAILQ_NEXT(sym, entry); + if ((conf->sc_opts & HTTPD_OPT_VERBOSE) && !sym->used) + fprintf(stderr, "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 (TAILQ_EMPTY(conf->sc_servers)) { + log_warnx("no actions, nothing to do"); + errors++; + } + + return (errors ? -1 : 0); +} + +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); +} + +struct address * +host_v4(const char *s) +{ + struct in_addr ina; + struct sockaddr_in *sain; + struct address *h; + + bzero(&ina, sizeof(ina)); + if (inet_pton(AF_INET, s, &ina) != 1) + return (NULL); + + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(NULL); + 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; + + return (h); +} + +struct address * +host_v6(const char *s) +{ + struct addrinfo hints, *res; + struct sockaddr_in6 *sa_in6; + struct address *h = NULL; + + bzero(&hints, sizeof(hints)); + hints.ai_family = AF_INET6; + hints.ai_socktype = SOCK_DGRAM; /* dummy */ + hints.ai_flags = AI_NUMERICHOST; + if (getaddrinfo(s, "0", &hints, &res) == 0) { + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(NULL); + sa_in6 = (struct sockaddr_in6 *)&h->ss; + sa_in6->sin6_len = sizeof(struct sockaddr_in6); + sa_in6->sin6_family = AF_INET6; + memcpy(&sa_in6->sin6_addr, + &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr, + sizeof(sa_in6->sin6_addr)); + sa_in6->sin6_scope_id = + ((struct sockaddr_in6 *)res->ai_addr)->sin6_scope_id; + + freeaddrinfo(res); + } + + return (h); +} + +int +host_dns(const char *s, struct addresslist *al, int max, + struct portrange *port, const char *ifname, int ipproto) +{ + struct addrinfo hints, *res0, *res; + int error, cnt = 0; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct address *h; + + if ((cnt = host_if(s, al, max, port, ifname, ipproto)) != 0) + return (cnt); + + bzero(&hints, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; /* DUMMY */ + error = getaddrinfo(s, NULL, &hints, &res0); + if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME) + return (0); + if (error) { + log_warnx("%s: could not parse \"%s\": %s", __func__, s, + gai_strerror(error)); + return (-1); + } + + for (res = res0; res && cnt < max; res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(NULL); + + if (port != NULL) + bcopy(port, &h->port, sizeof(h->port)); + if (ifname != NULL) { + if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >= + sizeof(h->ifname)) + log_warnx("%s: interface name truncated", + __func__); + freeaddrinfo(res0); + free(h); + return (-1); + } + if (ipproto != -1) + h->ipproto = ipproto; + 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; + } 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)); + } + + TAILQ_INSERT_HEAD(al, h, entry); + cnt++; + } + if (cnt == max && res) { + log_warnx("%s: %s resolves to more than %d hosts", __func__, + s, max); + } + freeaddrinfo(res0); + return (cnt); +} + +int +host_if(const char *s, struct addresslist *al, int max, + struct portrange *port, const char *ifname, int ipproto) +{ + struct ifaddrs *ifap, *p; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct address *h; + int cnt = 0, af; + + if (getifaddrs(&ifap) == -1) + fatal("getifaddrs"); + + /* First search for IPv4 addresses */ + af = AF_INET; + + nextaf: + for (p = ifap; p != NULL && cnt < max; p = p->ifa_next) { + if (p->ifa_addr->sa_family != af || + (strcmp(s, p->ifa_name) != 0 && + !is_if_in_group(p->ifa_name, s))) + continue; + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal("calloc"); + + if (port != NULL) + bcopy(port, &h->port, sizeof(h->port)); + if (ifname != NULL) { + if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >= + sizeof(h->ifname)) + log_warnx("%s: interface name truncated", + __func__); + freeifaddrs(ifap); + return (-1); + } + if (ipproto != -1) + h->ipproto = ipproto; + h->ss.ss_family = af; + + if (af == AF_INET) { + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_addr.s_addr = ((struct sockaddr_in *) + p->ifa_addr)->sin_addr.s_addr; + } else { + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *) + p->ifa_addr)->sin6_addr, sizeof(struct in6_addr)); + sin6->sin6_scope_id = ((struct sockaddr_in6 *) + p->ifa_addr)->sin6_scope_id; + } + + TAILQ_INSERT_HEAD(al, h, entry); + cnt++; + } + if (af == AF_INET) { + /* Next search for IPv6 addresses */ + af = AF_INET6; + goto nextaf; + } + + if (cnt > max) { + log_warnx("%s: %s resolves to more than %d hosts", __func__, + s, max); + } + freeifaddrs(ifap); + return (cnt); +} + +int +host(const char *s, struct addresslist *al, int max, + struct portrange *port, const char *ifname, int ipproto) +{ + struct address *h; + + h = host_v4(s); + + /* IPv6 address? */ + if (h == NULL) + h = host_v6(s); + + if (h != NULL) { + if (port != NULL) + bcopy(port, &h->port, sizeof(h->port)); + if (ifname != NULL) { + if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >= + sizeof(h->ifname)) { + log_warnx("%s: interface name truncated", + __func__); + free(h); + return (-1); + } + } + if (ipproto != -1) + h->ipproto = ipproto; + + TAILQ_INSERT_HEAD(al, h, entry); + return (1); + } + + return (host_dns(s, al, max, port, ifname, ipproto)); +} + +int +getservice(char *n) +{ + struct servent *s; + const char *errstr; + long long llval; + + llval = strtonum(n, 0, UINT16_MAX, &errstr); + if (errstr) { + s = getservbyname(n, "tcp"); + if (s == NULL) + s = getservbyname(n, "udp"); + if (s == NULL) { + yyerror("unknown port %s", n); + return (-1); + } + return (s->s_port); + } + + return (htons((u_short)llval)); +} + +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) + err(1, "IFNAMSIZ"); + 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 = + (struct ifg_req *)calloc(len / sizeof(struct ifg_req), + sizeof(struct ifg_req)); + if (ifgr.ifgr_groups == NULL) + err(1, "getifgroups"); + 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); +} @@ -0,0 +1,627 @@ +/* $OpenBSD: proc.c,v 1.15 2014/07/11 16:39:10 krw Exp $ */ + +/* + * Copyright (c) 2010 - 2014 Reyk Floeter <reyk@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <sys/tree.h> + +#include <net/if.h> +#include <netinet/in_systm.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <arpa/inet.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <signal.h> +#include <pwd.h> +#include <event.h> + +#include <openssl/ssl.h> + +#include "httpd.h" + +void proc_open(struct privsep *, struct privsep_proc *, + struct privsep_proc *, size_t); +void proc_close(struct privsep *); +int proc_ispeer(struct privsep_proc *, u_int, enum privsep_procid); +void proc_shutdown(struct privsep_proc *); +void proc_sig_handler(int, short, void *); +void proc_range(struct privsep *, enum privsep_procid, int *, int *); + +int +proc_ispeer(struct privsep_proc *procs, u_int nproc, enum privsep_procid type) +{ + u_int i; + + for (i = 0; i < nproc; i++) + if (procs[i].p_id == type) + return (1); + return (0); +} + +void +proc_init(struct privsep *ps, struct privsep_proc *procs, u_int nproc) +{ + u_int i, j, src, dst; + struct privsep_pipes *pp; + + /* + * Allocate pipes for all process instances (incl. parent) + * + * - ps->ps_pipes: N:M mapping + * N source processes connected to M destination processes: + * [src][instances][dst][instances], for example + * [PROC_RELAY][3][PROC_CA][3] + * + * - ps->ps_pp: per-process 1:M part of ps->ps_pipes + * Each process instance has a destination array of socketpair fds: + * [dst][instances], for example + * [PROC_PARENT][0] + */ + for (src = 0; src < PROC_MAX; src++) { + /* Allocate destination array for each process */ + if ((ps->ps_pipes[src] = calloc(ps->ps_ninstances, + sizeof(struct privsep_pipes))) == NULL) + fatal("proc_init: calloc"); + + for (i = 0; i < ps->ps_ninstances; i++) { + pp = &ps->ps_pipes[src][i]; + + for (dst = 0; dst < PROC_MAX; dst++) { + /* Allocate maximum fd integers */ + if ((pp->pp_pipes[dst] = + calloc(ps->ps_ninstances, + sizeof(int))) == NULL) + fatal("proc_init: calloc"); + + /* Mark fd as unused */ + for (j = 0; j < ps->ps_ninstances; j++) + pp->pp_pipes[dst][j] = -1; + } + } + } + + /* + * Setup and run the parent and its children + */ + privsep_process = PROC_PARENT; + ps->ps_instances[PROC_PARENT] = 1; + ps->ps_title[PROC_PARENT] = "parent"; + ps->ps_pid[PROC_PARENT] = getpid(); + ps->ps_pp = &ps->ps_pipes[privsep_process][0]; + + for (i = 0; i < nproc; i++) { + /* Default to 1 process instance */ + if (ps->ps_instances[procs[i].p_id] < 1) + ps->ps_instances[procs[i].p_id] = 1; + ps->ps_title[procs[i].p_id] = procs[i].p_title; + } + + proc_open(ps, NULL, procs, nproc); + + /* Engage! */ + for (i = 0; i < nproc; i++) + ps->ps_pid[procs[i].p_id] = (*procs[i].p_init)(ps, &procs[i]); +} + +void +proc_kill(struct privsep *ps) +{ + pid_t pid; + u_int i; + + if (privsep_process != PROC_PARENT) + return; + + for (i = 0; i < PROC_MAX; i++) { + if (ps->ps_pid[i] == 0) + continue; + killpg(ps->ps_pid[i], SIGTERM); + } + + do { + pid = waitpid(WAIT_ANY, NULL, 0); + } while (pid != -1 || (pid == -1 && errno == EINTR)); + + proc_close(ps); +} + +void +proc_open(struct privsep *ps, struct privsep_proc *p, + struct privsep_proc *procs, size_t nproc) +{ + struct privsep_pipes *pa, *pb; + int fds[2]; + u_int i, j, src, proc; + + if (p == NULL) + src = privsep_process; /* parent */ + else + src = p->p_id; + + /* + * Open socket pairs for our peers + */ + for (proc = 0; proc < nproc; proc++) { + procs[proc].p_ps = ps; + procs[proc].p_env = ps->ps_env; + + for (i = 0; i < ps->ps_instances[src]; i++) { + for (j = 0; j < ps->ps_instances[procs[proc].p_id]; + j++) { + pa = &ps->ps_pipes[src][i]; + pb = &ps->ps_pipes[procs[proc].p_id][j]; + + /* Check if fds are already set by peer */ + if (pa->pp_pipes[procs[proc].p_id][j] != -1) + continue; + + if (socketpair(AF_UNIX, SOCK_STREAM, + PF_UNSPEC, fds) == -1) + fatal("socketpair"); + + socket_set_blockmode(fds[0], BM_NONBLOCK); + socket_set_blockmode(fds[1], BM_NONBLOCK); + + pa->pp_pipes[procs[proc].p_id][j] = fds[0]; + pb->pp_pipes[src][i] = fds[1]; + } + } + } +} + +void +proc_listen(struct privsep *ps, struct privsep_proc *procs, size_t nproc) +{ + u_int i, dst, src, n, m; + struct privsep_pipes *pp; + + /* + * Close unused pipes + */ + for (src = 0; src < PROC_MAX; src++) { + for (n = 0; n < ps->ps_instances[src]; n++) { + /* Ingore current process */ + if (src == (u_int)privsep_process && + n == ps->ps_instance) + continue; + + pp = &ps->ps_pipes[src][n]; + + for (dst = 0; dst < PROC_MAX; dst++) { + if (src == dst) + continue; + for (m = 0; m < ps->ps_instances[dst]; m++) { + if (pp->pp_pipes[dst][m] == -1) + continue; + + /* Close and invalidate fd */ + close(pp->pp_pipes[dst][m]); + pp->pp_pipes[dst][m] = -1; + } + } + } + } + + src = privsep_process; + ps->ps_pp = pp = &ps->ps_pipes[src][ps->ps_instance]; + + /* + * Listen on appropriate pipes + */ + for (i = 0; i < nproc; i++) { + dst = procs[i].p_id; + + if (src == dst) + fatal("proc_listen: cannot peer with oneself"); + + if ((ps->ps_ievs[dst] = calloc(ps->ps_instances[dst], + sizeof(struct imsgev))) == NULL) + fatal("proc_open"); + + for (n = 0; n < ps->ps_instances[dst]; n++) { + if (pp->pp_pipes[dst][n] == -1) + continue; + + imsg_init(&(ps->ps_ievs[dst][n].ibuf), + pp->pp_pipes[dst][n]); + ps->ps_ievs[dst][n].handler = proc_dispatch; + ps->ps_ievs[dst][n].events = EV_READ; + ps->ps_ievs[dst][n].proc = &procs[i]; + ps->ps_ievs[dst][n].data = &ps->ps_ievs[dst][n]; + procs[i].p_instance = n; + + event_set(&(ps->ps_ievs[dst][n].ev), + ps->ps_ievs[dst][n].ibuf.fd, + ps->ps_ievs[dst][n].events, + ps->ps_ievs[dst][n].handler, + ps->ps_ievs[dst][n].data); + event_add(&(ps->ps_ievs[dst][n].ev), NULL); + } + } +} + +void +proc_close(struct privsep *ps) +{ + u_int dst, n; + struct privsep_pipes *pp; + + if (ps == NULL) + return; + + pp = ps->ps_pp; + + for (dst = 0; dst < PROC_MAX; dst++) { + if (ps->ps_ievs[dst] == NULL) + continue; + + for (n = 0; n < ps->ps_instances[dst]; n++) { + if (pp->pp_pipes[dst][n] == -1) + continue; + + /* Cancel the fd, close and invalidate the fd */ + event_del(&(ps->ps_ievs[dst][n].ev)); + imsg_clear(&(ps->ps_ievs[dst][n].ibuf)); + close(pp->pp_pipes[dst][n]); + pp->pp_pipes[dst][n] = -1; + } + free(ps->ps_ievs[dst]); + } +} + +void +proc_shutdown(struct privsep_proc *p) +{ + struct privsep *ps = p->p_ps; + + if (p->p_id == PROC_CONTROL && ps) + control_cleanup(&ps->ps_csock); + + if (p->p_shutdown != NULL) + (*p->p_shutdown)(); + + proc_close(ps); + + log_info("%s exiting, pid %d", p->p_title, getpid()); + + _exit(0); +} + +void +proc_sig_handler(int sig, short event, void *arg) +{ + struct privsep_proc *p = arg; + + switch (sig) { + case SIGINT: + case SIGTERM: + proc_shutdown(p); + break; + case SIGCHLD: + case SIGHUP: + case SIGPIPE: + /* ignore */ + break; + default: + fatalx("proc_sig_handler: unexpected signal"); + /* NOTREACHED */ + } +} + +pid_t +proc_run(struct privsep *ps, struct privsep_proc *p, + struct privsep_proc *procs, u_int nproc, + void (*init)(struct privsep *, struct privsep_proc *, void *), void *arg) +{ + pid_t pid; + struct passwd *pw; + const char *root; + struct control_sock *rcs; + u_int n; + + if (ps->ps_noaction) + return (0); + + proc_open(ps, p, procs, nproc); + + /* Fork child handlers */ + switch (pid = fork()) { + case -1: + fatal("proc_run: cannot fork"); + case 0: + /* Set the process group of the current process */ + setpgrp(0, getpid()); + break; + default: + return (pid); + } + + pw = ps->ps_pw; + + if (p->p_id == PROC_CONTROL && ps->ps_instance == 0) { + if (control_init(ps, &ps->ps_csock) == -1) + fatalx(p->p_title); + TAILQ_FOREACH(rcs, &ps->ps_rcsocks, cs_entry) + if (control_init(ps, rcs) == -1) + fatalx(p->p_title); + } + + /* Change root directory */ + if (p->p_chroot != NULL) + root = p->p_chroot; + else + root = pw->pw_dir; + + if (chroot(root) == -1) + fatal("proc_run: chroot"); + if (chdir("/") == -1) + fatal("proc_run: chdir(\"/\")"); + + privsep_process = p->p_id; + + setproctitle("%s", p->p_title); + + 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("proc_run: cannot drop privileges"); + + /* Fork child handlers */ + for (n = 1; n < ps->ps_instances[p->p_id]; n++) { + if (fork() == 0) { + ps->ps_instance = p->p_instance = n; + break; + } + } + +#ifdef DEBUG + log_debug("%s: %s %d/%d, pid %d", __func__, p->p_title, + ps->ps_instance + 1, ps->ps_instances[p->p_id], getpid()); +#endif + + event_init(); + + signal_set(&ps->ps_evsigint, SIGINT, proc_sig_handler, p); + signal_set(&ps->ps_evsigterm, SIGTERM, proc_sig_handler, p); + signal_set(&ps->ps_evsigchld, SIGCHLD, proc_sig_handler, p); + signal_set(&ps->ps_evsighup, SIGHUP, proc_sig_handler, p); + signal_set(&ps->ps_evsigpipe, SIGPIPE, proc_sig_handler, p); + + signal_add(&ps->ps_evsigint, NULL); + signal_add(&ps->ps_evsigterm, NULL); + signal_add(&ps->ps_evsigchld, NULL); + signal_add(&ps->ps_evsighup, NULL); + signal_add(&ps->ps_evsigpipe, NULL); + + proc_listen(ps, procs, nproc); + + if (p->p_id == PROC_CONTROL && ps->ps_instance == 0) { + TAILQ_INIT(&ctl_conns); + if (control_listen(&ps->ps_csock) == -1) + fatalx(p->p_title); + TAILQ_FOREACH(rcs, &ps->ps_rcsocks, cs_entry) + if (control_listen(rcs) == -1) + fatalx(p->p_title); + } + + if (init != NULL) + init(ps, p, arg); + + event_dispatch(); + + proc_shutdown(p); + + return (0); +} + +void +proc_dispatch(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct privsep_proc *p = iev->proc; + struct privsep *ps = p->p_ps; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + int verbose; + const char *title; + + title = ps->ps_title[privsep_process]; + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1) + fatal(title); + if (n == 0) { + /* this pipe is dead, so remove the event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + return; + } + } + + if (event & EV_WRITE) { + if (msgbuf_write(&ibuf->w) <= 0 && errno != EAGAIN) + fatal(title); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal(title); + if (n == 0) + break; + +#if DEBUG > 1 + log_debug("%s: %s %d got imsg %d from %s %d", + __func__, title, ps->ps_instance + 1, + imsg.hdr.type, p->p_title, p->p_instance); +#endif + + /* + * Check the message with the program callback + */ + if ((p->p_cb)(fd, p, &imsg) == 0) { + /* Message was handled by the callback, continue */ + imsg_free(&imsg); + continue; + } + + /* + * Generic message handling + */ + switch (imsg.hdr.type) { + case IMSG_CTL_VERBOSE: + IMSG_SIZE_CHECK(&imsg, &verbose); + memcpy(&verbose, imsg.data, sizeof(verbose)); + log_verbose(verbose); + break; + default: + log_warnx("%s: %s %d got invalid imsg %d from %s %d", + __func__, title, ps->ps_instance + 1, + imsg.hdr.type, p->p_title, p->p_instance); + fatalx(title); + } + imsg_free(&imsg); + } + imsg_event_add(iev); +} + +/* + * imsg helper functions + */ + +void +imsg_event_add(struct imsgev *iev) +{ + if (iev->handler == NULL) { + imsg_flush(&iev->ibuf); + return; + } + + iev->events = EV_READ; + if (iev->ibuf.w.queued) + iev->events |= EV_WRITE; + + event_del(&iev->ev); + event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev->data); + event_add(&iev->ev, NULL); +} + +int +imsg_compose_event(struct imsgev *iev, u_int16_t type, u_int32_t peerid, + pid_t pid, int fd, void *data, u_int16_t datalen) +{ + int ret; + + if ((ret = imsg_compose(&iev->ibuf, type, peerid, + pid, fd, data, datalen)) == -1) + return (ret); + imsg_event_add(iev); + return (ret); +} + +int +imsg_composev_event(struct imsgev *iev, u_int16_t type, u_int32_t peerid, + pid_t pid, int fd, const struct iovec *iov, int iovcnt) +{ + int ret; + + if ((ret = imsg_composev(&iev->ibuf, type, peerid, + pid, fd, iov, iovcnt)) == -1) + return (ret); + imsg_event_add(iev); + return (ret); +} + +void +proc_range(struct privsep *ps, enum privsep_procid id, int *n, int *m) +{ + if (*n == -1) { + /* Use a range of all target instances */ + *n = 0; + *m = ps->ps_instances[id]; + } else { + /* Use only a single slot of the specified peer process */ + *m = *n + 1; + } +} + +int +proc_compose_imsg(struct privsep *ps, enum privsep_procid id, int n, + u_int16_t type, int fd, void *data, u_int16_t datalen) +{ + int m; + + proc_range(ps, id, &n, &m); + for (; n < m; n++) { + if (imsg_compose_event(&ps->ps_ievs[id][n], + type, -1, 0, fd, data, datalen) == -1) + return (-1); + } + + return (0); +} + +int +proc_composev_imsg(struct privsep *ps, enum privsep_procid id, int n, + u_int16_t type, int fd, const struct iovec *iov, int iovcnt) +{ + int m; + + proc_range(ps, id, &n, &m); + for (; n < m; n++) + if (imsg_composev_event(&ps->ps_ievs[id][n], + type, -1, 0, fd, iov, iovcnt) == -1) + return (-1); + + return (0); +} + +int +proc_forward_imsg(struct privsep *ps, struct imsg *imsg, + enum privsep_procid id, int n) +{ + return (proc_compose_imsg(ps, id, n, imsg->hdr.type, + imsg->fd, imsg->data, IMSG_DATA_SIZE(imsg))); +} + +struct imsgbuf * +proc_ibuf(struct privsep *ps, enum privsep_procid id, int n) +{ + int m; + + proc_range(ps, id, &n, &m); + return (&ps->ps_ievs[id][n].ibuf); +} + +struct imsgev * +proc_iev(struct privsep *ps, enum privsep_procid id, int n) +{ + int m; + + proc_range(ps, id, &n, &m); + return (&ps->ps_ievs[id][n]); +} diff --git a/server.c b/server.c new file mode 100644 index 0000000..65912d8 --- /dev/null +++ b/server.c @@ -0,0 +1,636 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2006 - 2014 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/time.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <sys/tree.h> +#include <sys/hash.h> + +#include <net/if.h> +#include <netinet/in_systm.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <stdio.h> +#include <err.h> +#include <pwd.h> +#include <event.h> +#include <fnmatch.h> + +#include <openssl/dh.h> +#include <openssl/ssl.h> + +#include "httpd.h" + +int server_dispatch_parent(int, struct privsep_proc *, + struct imsg *); +void server_shutdown(void); + +void server_init(struct privsep *, struct privsep_proc *p, void *); +void server_launch(void); +int server_socket(struct sockaddr_storage *, in_port_t, + struct server *, int, int); +int server_socket_listen(struct sockaddr_storage *, in_port_t, + struct server *); + +void server_accept(int, short, void *); +void server_input(struct client *); + +extern void bufferevent_read_pressure_cb(struct evbuffer *, size_t, + size_t, void *); + +volatile int server_clients; +volatile int server_inflight = 0; +u_int32_t server_cltid; + +static struct httpd *env = NULL; +int proc_id; + +static struct privsep_proc procs[] = { + { "parent", PROC_PARENT, server_dispatch_parent } +}; + +pid_t +server(struct privsep *ps, struct privsep_proc *p) +{ + pid_t pid; + env = ps->ps_env; + pid = proc_run(ps, p, procs, nitems(procs), server_init, NULL); +// server_http(env); + return (pid); +} + +void +server_shutdown(void) +{ + config_purge(env, CONFIG_ALL); + usleep(200); /* XXX server needs to shutdown last */ +} + +int +server_privinit(struct server *srv) +{ + log_debug("%s: adding server %s", __func__, srv->srv_conf.name); + + if ((srv->srv_s = server_socket_listen(&srv->srv_conf.ss, + srv->srv_conf.port, srv)) == -1) + return (-1); + + return (0); +} + +void +server_init(struct privsep *ps, struct privsep_proc *p, void *arg) +{ + if (config_init(ps->ps_env) == -1) + fatal("failed to initialize configuration"); + + /* Set to current prefork id */ + proc_id = p->p_instance; + + /* We use a custom shutdown callback */ + p->p_shutdown = server_shutdown; + + /* Unlimited file descriptors (use system limits) */ + socket_rlimit(-1); + +#if 0 + /* Schedule statistics timer */ + evtimer_set(&env->sc_statev, server_statistics, NULL); + bcopy(&env->sc_statinterval, &tv, sizeof(tv)); + evtimer_add(&env->sc_statev, &tv); +#endif +} + +void +server_launch(void) +{ + struct server *srv; + + TAILQ_FOREACH(srv, env->sc_servers, srv_entry) { +// server_http_init(srv); + + log_debug("%s: running server %s", __func__, + srv->srv_conf.name); + + event_set(&srv->srv_ev, srv->srv_s, EV_READ, + server_accept, srv); + event_add(&srv->srv_ev, NULL); + evtimer_set(&srv->srv_evt, server_accept, srv); + } +} + +int +server_socket_af(struct sockaddr_storage *ss, in_port_t port) +{ + switch (ss->ss_family) { + case AF_INET: + ((struct sockaddr_in *)ss)->sin_port = port; + ((struct sockaddr_in *)ss)->sin_len = + sizeof(struct sockaddr_in); + break; + case AF_INET6: + ((struct sockaddr_in6 *)ss)->sin6_port = port; + ((struct sockaddr_in6 *)ss)->sin6_len = + sizeof(struct sockaddr_in6); + break; + default: + return (-1); + } + + return (0); +} + +in_port_t +server_socket_getport(struct sockaddr_storage *ss) +{ + switch (ss->ss_family) { + case AF_INET: + return (((struct sockaddr_in *)ss)->sin_port); + case AF_INET6: + return (((struct sockaddr_in6 *)ss)->sin6_port); + default: + return (0); + } + + /* NOTREACHED */ + return (0); +} + +int +server_socket(struct sockaddr_storage *ss, in_port_t port, + struct server *srv, int fd, int reuseport) +{ + struct linger lng; + int s = -1, val; + + if (server_socket_af(ss, port) == -1) + goto bad; + + s = fd == -1 ? socket(ss->ss_family, SOCK_STREAM, IPPROTO_TCP) : fd; + if (s == -1) + goto bad; + + /* + * Socket options + */ + bzero(&lng, sizeof(lng)); + if (setsockopt(s, SOL_SOCKET, SO_LINGER, &lng, sizeof(lng)) == -1) + goto bad; + if (reuseport) { + val = 1; + if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &val, + sizeof(int)) == -1) + goto bad; + } + if (fcntl(s, F_SETFL, O_NONBLOCK) == -1) + goto bad; + if (srv->srv_tcpflags & TCPFLAG_BUFSIZ) { + val = srv->srv_tcpbufsiz; + if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, + &val, sizeof(val)) == -1) + goto bad; + val = srv->srv_tcpbufsiz; + if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, + &val, sizeof(val)) == -1) + goto bad; + } + + /* + * IP options + */ + if (srv->srv_tcpflags & TCPFLAG_IPTTL) { + val = (int)srv->srv_tcpipttl; + if (setsockopt(s, IPPROTO_IP, IP_TTL, + &val, sizeof(val)) == -1) + goto bad; + } + if (srv->srv_tcpflags & TCPFLAG_IPMINTTL) { + val = (int)srv->srv_tcpipminttl; + if (setsockopt(s, IPPROTO_IP, IP_MINTTL, + &val, sizeof(val)) == -1) + goto bad; + } + + /* + * TCP options + */ + if (srv->srv_tcpflags & (TCPFLAG_NODELAY|TCPFLAG_NNODELAY)) { + if (srv->srv_tcpflags & TCPFLAG_NNODELAY) + val = 0; + else + val = 1; + if (setsockopt(s, IPPROTO_TCP, TCP_NODELAY, + &val, sizeof(val)) == -1) + goto bad; + } + if (srv->srv_tcpflags & (TCPFLAG_SACK|TCPFLAG_NSACK)) { + if (srv->srv_tcpflags & TCPFLAG_NSACK) + val = 0; + else + val = 1; + if (setsockopt(s, IPPROTO_TCP, TCP_SACK_ENABLE, + &val, sizeof(val)) == -1) + goto bad; + } + + return (s); + + bad: + if (s != -1) + close(s); + return (-1); +} + +int +server_socket_listen(struct sockaddr_storage *ss, in_port_t port, + struct server *srv) +{ + int s; + + if ((s = server_socket(ss, port, srv, -1, 1)) == -1) + return (-1); + + if (bind(s, (struct sockaddr *)ss, ss->ss_len) == -1) + goto bad; + if (listen(s, srv->srv_tcpbacklog) == -1) + goto bad; + + return (s); + + bad: + close(s); + return (-1); +} + +void +server_input(struct client *clt) +{ + struct server *srv = clt->clt_server; + evbuffercb inrd = server_read; + evbuffercb inwr = server_write; + +#if 0 + if (server_httpdesc_init(&clt->clt_in) == -1) { + server_close(clt, + "failed to allocate http descriptor"); + return; + } +#endif + clt->clt_toread = TOREAD_HTTP_HEADER; +// inrd = server_read_http; + + /* + * Client <-> Server + */ + clt->clt_bev = bufferevent_new(clt->clt_s, inrd, inwr, + server_error, clt); + if (clt->clt_bev == NULL) { + server_close(clt, "failed to allocate input buffer event"); + return; + } + + bufferevent_settimeout(clt->clt_bev, + srv->srv_conf.timeout.tv_sec, srv->srv_conf.timeout.tv_sec); + bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE); +} + +void +server_write(struct bufferevent *bev, void *arg) +{ + struct client *clt = arg; + + getmonotime(&clt->clt_tv_last); + + if (clt->clt_done) + goto done; + return; + done: + server_close(clt, "done"); + return; +} + +void +server_dump(struct client *clt, const void *buf, size_t len) +{ + if (!len) + return; + + /* + * This function will dump the specified message directly + * to the underlying client, without waiting for success + * of non-blocking events etc. This is useful to print an + * error message before gracefully closing the client. + */ +#if 0 + if (cre->ssl != NULL) + (void)SSL_write(cre->ssl, buf, len); + else +#endif + (void)write(clt->clt_s, buf, len); +} + +void +server_read(struct bufferevent *bev, void *arg) +{ + struct client *clt = arg; + struct evbuffer *src = EVBUFFER_INPUT(bev); + + getmonotime(&clt->clt_tv_last); + + if (!EVBUFFER_LENGTH(src)) + return; + if (server_bufferevent_write_buffer(clt, src) == -1) + goto fail; + if (clt->clt_done) + goto done; + if (clt->clt_bev) + bufferevent_enable(clt->clt_bev, EV_READ); + return; + done: + server_close(clt, "done"); + return; + fail: + server_close(clt, strerror(errno)); +} + +void +server_error(struct bufferevent *bev, short error, void *arg) +{ + struct client *clt = arg; + + if (error & EVBUFFER_TIMEOUT) { + server_close(clt, "buffer event timeout"); + return; + } + if (error & EVBUFFER_ERROR && errno == EFBIG) { + bufferevent_enable(clt->clt_bev, EV_READ); + return; + } + if (error & (EVBUFFER_READ|EVBUFFER_WRITE|EVBUFFER_EOF)) { + bufferevent_disable(bev, EV_READ|EV_WRITE); + + clt->clt_done = 1; + + return; +// server_close(con, "done"); + } + server_close(clt, "buffer event error"); + return; +} + +void +server_accept(int fd, short event, void *arg) +{ + struct server *srv = arg; + struct client *clt = NULL; + socklen_t slen; + struct sockaddr_storage ss; + int s = -1; + + event_add(&srv->srv_ev, NULL); + if ((event & EV_TIMEOUT)) + return; + + slen = sizeof(ss); + if ((s = accept_reserve(fd, (struct sockaddr *)&ss, + &slen, FD_RESERVE, &server_inflight)) == -1) { + /* + * Pause accept if we are out of file descriptors, or + * libevent will haunt us here too. + */ + if (errno == ENFILE || errno == EMFILE) { + struct timeval evtpause = { 1, 0 }; + + event_del(&srv->srv_ev); + evtimer_add(&srv->srv_evt, &evtpause); + log_debug("%s: deferring connections", __func__); + } + return; + } + if (server_clients >= SERVER_MAX_CLIENTS) + goto err; + + if (fcntl(s, F_SETFL, O_NONBLOCK) == -1) + goto err; + + if ((clt = calloc(1, sizeof(*clt))) == NULL) + goto err; + + clt->clt_s = s; + clt->clt_toread = TOREAD_UNLIMITED; + clt->clt_server = srv; + clt->clt_id = ++server_cltid; + clt->clt_serverid = srv->srv_conf.id; + clt->clt_pid = getpid(); + switch (ss.ss_family) { + case AF_INET: + clt->clt_port = ((struct sockaddr_in *)&ss)->sin_port; + break; + case AF_INET6: + clt->clt_port = ((struct sockaddr_in6 *)&ss)->sin6_port; + break; + } + memcpy(&clt->clt_ss, &ss, sizeof(clt->clt_ss)); + + getmonotime(&clt->clt_tv_start); + bcopy(&clt->clt_tv_start, &clt->clt_tv_last, sizeof(clt->clt_tv_last)); + + server_clients++; + SPLAY_INSERT(client_tree, &srv->srv_clients, clt); + + /* Increment the per-relay client counter */ + //srv->srv_stats[proc_id].last++; + + /* Pre-allocate output buffer */ + clt->clt_output = evbuffer_new(); + if (clt->clt_output == NULL) { + server_close(clt, "failed to allocate output buffer"); + return; + } + + /* Pre-allocate log buffer */ + clt->clt_log = evbuffer_new(); + if (clt->clt_log == NULL) { + server_close(clt, "failed to allocate log buffer"); + return; + } + + server_input(clt); + return; + + err: + if (s != -1) { + close(s); + if (clt != NULL) + free(clt); + /* + * the client struct was not completly set up, but still + * counted as an inflight client. account for this. + */ + server_inflight--; + log_debug("%s: inflight decremented, now %d", + __func__, server_inflight); + } +} + +void +server_close(struct client *clt, const char *msg) +{ + char ibuf[128], obuf[128], *ptr = NULL; + struct server *srv = clt->clt_server; + + SPLAY_REMOVE(client_tree, &srv->srv_clients, clt); + + event_del(&clt->clt_ev); + if (clt->clt_bev != NULL) + bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE); + + if ((env->sc_opts & HTTPD_OPT_LOGUPDATE) && msg != NULL) { + bzero(&ibuf, sizeof(ibuf)); + bzero(&obuf, sizeof(obuf)); + (void)print_host(&clt->clt_ss, ibuf, sizeof(ibuf)); + (void)print_host(&srv->srv_ss, obuf, sizeof(obuf)); + if (EVBUFFER_LENGTH(clt->clt_log) && + evbuffer_add_printf(clt->clt_log, "\r\n") != -1) + ptr = evbuffer_readline(clt->clt_log); + log_info("server %s, " + "client %d (%d active), %s -> %s:%d, " + "%s%s%s", srv->srv_conf.name, clt->clt_id, server_clients, + ibuf, obuf, ntohs(clt->clt_port), msg, + ptr == NULL ? "" : ",", ptr == NULL ? "" : ptr); + if (ptr != NULL) + free(ptr); + } + + if (clt->clt_bev != NULL) + bufferevent_free(clt->clt_bev); + else if (clt->clt_output != NULL) + evbuffer_free(clt->clt_output); + if (clt->clt_s != -1) { + close(clt->clt_s); + if (/* XXX */ -1) { + /* + * the output was never connected, + * thus this was an inflight client. + */ + server_inflight--; + log_debug("%s: clients inflight decremented, now %d", + __func__, server_inflight); + } + } + + if (clt->clt_log != NULL) + evbuffer_free(clt->clt_log); + + free(clt); + server_clients--; +} + +int +server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg) +{ + switch (imsg->hdr.type) { +#if 0 + case IMSG_CFG_SERVER: + config_getserver(env, imsg); + break; +#endif + case IMSG_CFG_DONE: + config_getcfg(env, imsg); + break; + case IMSG_CTL_START: + server_launch(); + break; + case IMSG_CTL_RESET: + config_getreset(env, imsg); + break; + default: + return (-1); + } + + return (0); +} + +int +server_bufferevent_add(struct event *ev, int timeout) +{ + struct timeval tv, *ptv = NULL; + + if (timeout) { + timerclear(&tv); + tv.tv_sec = timeout; + ptv = &tv; + } + + return (event_add(ev, ptv)); +} + +int +server_bufferevent_print(struct client *clt, const char *str) +{ + if (clt->clt_bev == NULL) + return (evbuffer_add(clt->clt_output, str, strlen(str))); + return (bufferevent_write(clt->clt_bev, str, strlen(str))); +} + +int +server_bufferevent_write_buffer(struct client *clt, struct evbuffer *buf) +{ + if (clt->clt_bev == NULL) + return (evbuffer_add_buffer(clt->clt_output, buf)); + return (bufferevent_write_buffer(clt->clt_bev, buf)); +} + +int +server_bufferevent_write_chunk(struct client *clt, + struct evbuffer *buf, size_t size) +{ + int ret; + ret = server_bufferevent_write(clt, buf->buffer, size); + if (ret != -1) + evbuffer_drain(buf, size); + return (ret); +} + +int +server_bufferevent_write(struct client *clt, void *data, size_t size) +{ + if (clt->clt_bev == NULL) + return (evbuffer_add(clt->clt_output, data, size)); + return (bufferevent_write(clt->clt_bev, data, size)); +} + +int +server_client_cmp(struct client *a, struct client *b) +{ + return ((int)a->clt_id - b->clt_id); +} + +SPLAY_GENERATE(client_tree, client, clt_nodes, server_client_cmp); diff --git a/server_http.c b/server_http.c new file mode 100644 index 0000000..75e1521 --- /dev/null +++ b/server_http.c @@ -0,0 +1,1736 @@ +/* $OpenBSD: relay_http.c,v 1.25 2014/07/11 23:11:54 benno Exp $ */ + +/* + * Copyright (c) 2006 - 2014 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/time.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <sys/tree.h> +#include <sys/hash.h> + +#include <net/if.h> +#include <netinet/in_systm.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <stdio.h> +#include <err.h> +#include <pwd.h> +#include <event.h> +#include <fnmatch.h> + +#include <openssl/ssl.h> + +#include "relayd.h" +#include "http.h" + +static int _relay_lookup_url(struct ctl_relay_event *, char *, char *, + char *, struct kv *); +int relay_lookup_url(struct ctl_relay_event *, + const char *, struct kv *); +int relay_lookup_query(struct ctl_relay_event *, struct kv *); +int relay_lookup_cookie(struct ctl_relay_event *, const char *, + struct kv *); +void relay_read_httpcontent(struct bufferevent *, void *); +void relay_read_httpchunks(struct bufferevent *, void *); +char *relay_expand_http(struct ctl_relay_event *, char *, + char *, size_t); +int relay_writeheader_kv(struct ctl_relay_event *, struct kv *); +int relay_writeheader_http(struct ctl_relay_event *, + struct ctl_relay_event *); +int relay_writerequest_http(struct ctl_relay_event *, + struct ctl_relay_event *); +int relay_writeresponse_http(struct ctl_relay_event *, + struct ctl_relay_event *); +void relay_reset_http(struct ctl_relay_event *); +static int relay_httpmethod_cmp(const void *, const void *); +static int relay_httperror_cmp(const void *, const void *); +int relay_httpquery_test(struct ctl_relay_event *, + struct relay_rule *, struct kvlist *); +int relay_httpheader_test(struct ctl_relay_event *, + struct relay_rule *, struct kvlist *); +int relay_httppath_test(struct ctl_relay_event *, + struct relay_rule *, struct kvlist *); +int relay_httpurl_test(struct ctl_relay_event *, + struct relay_rule *, struct kvlist *); +int relay_httpcookie_test(struct ctl_relay_event *, + struct relay_rule *, struct kvlist *); +int relay_apply_actions(struct ctl_relay_event *, struct kvlist *); +int relay_match_actions(struct ctl_relay_event *, + struct relay_rule *, struct kvlist *, struct kvlist *); +void relay_httpdesc_free(struct http_descriptor *); + +static struct relayd *env = NULL; + +static struct http_method http_methods[] = HTTP_METHODS; +static struct http_error http_errors[] = HTTP_ERRORS; + +void +relay_http(struct relayd *x_env) +{ + if (x_env != NULL) + env = x_env; + + DPRINTF("%s: sorting lookup tables, pid %d", __func__, getpid()); + + /* Sort the HTTP lookup arrays */ + qsort(http_methods, sizeof(http_methods) / + sizeof(http_methods[0]) - 1, + sizeof(http_methods[0]), relay_httpmethod_cmp); + qsort(http_errors, sizeof(http_errors) / + sizeof(http_errors[0]) - 1, + sizeof(http_errors[0]), relay_httperror_cmp); +} + +void +relay_http_init(struct relay *rlay) +{ + rlay->rl_proto->close = relay_close_http; + + relay_http(NULL); + + /* Calculate skip step for the filter rules (may take a while) */ + relay_calc_skip_steps(&rlay->rl_proto->rules); +} + +int +relay_httpdesc_init(struct ctl_relay_event *cre) +{ + struct http_descriptor *desc; + + if ((desc = calloc(1, sizeof(*desc))) == NULL) + return (-1); + + RB_INIT(&desc->http_headers); + cre->desc = desc; + + return (0); +} + +void +relay_httpdesc_free(struct http_descriptor *desc) +{ + if (desc->http_path != NULL) { + free(desc->http_path); + desc->http_path = NULL; + } + if (desc->http_query != NULL) { + free(desc->http_query); + desc->http_query = NULL; + } + if (desc->http_version != NULL) { + free(desc->http_version); + desc->http_version = NULL; + } + if (desc->query_key != NULL) { + free(desc->query_key); + desc->query_key = NULL; + } + if (desc->query_val != NULL) { + free(desc->query_val); + desc->query_val = NULL; + } + kv_purge(&desc->http_headers); +} + +void +relay_read_http(struct bufferevent *bev, void *arg) +{ + struct ctl_relay_event *cre = arg; + struct http_descriptor *desc = cre->desc; + struct rsession *con = cre->con; + struct relay *rlay = con->se_relay; + struct protocol *proto = rlay->rl_proto; + struct evbuffer *src = EVBUFFER_INPUT(bev); + char *line = NULL, *key, *value; + int action; + const char *errstr; + size_t size, linelen; + struct kv *hdr = NULL; + + getmonotime(&con->se_tv_last); + + size = EVBUFFER_LENGTH(src); + DPRINTF("%s: session %d: size %lu, to read %lld", + __func__, con->se_id, size, cre->toread); + if (!size) { + if (cre->dir == RELAY_DIR_RESPONSE) + return; + cre->toread = TOREAD_HTTP_HEADER; + goto done; + } + + while (!cre->done && (line = evbuffer_readline(src)) != NULL) { + linelen = strlen(line); + + /* + * An empty line indicates the end of the request. + * libevent already stripped the \r\n for us. + */ + if (!linelen) { + cre->done = 1; + free(line); + break; + } + key = line; + + /* Limit the total header length minus \r\n */ + cre->headerlen += linelen; + if (cre->headerlen > RELAY_MAXHEADERLENGTH) { + free(line); + relay_abort_http(con, 413, "request too large", 0); + return; + } + + /* + * The first line is the GET/POST/PUT/... request, + * subsequent lines are HTTP headers. + */ + if (++cre->line == 1) + value = strchr(key, ' '); + else if (*key == ' ' || *key == '\t') + /* Multiline headers wrap with a space or tab */ + value = NULL; + else + value = strchr(key, ':'); + if (value == NULL) { + if (cre->line == 1) { + free(line); + relay_abort_http(con, 400, "malformed", 0); + return; + } + + /* Append line to the last header, if present */ + if (kv_extend(&desc->http_headers, + desc->http_lastheader, line) == NULL) { + free(line); + goto fail; + } + + free(line); + continue; + } + if (*value == ':') { + *value++ = '\0'; + value += strspn(value, " \t\r\n"); + } else { + *value++ = '\0'; + } + + DPRINTF("%s: session %d: header '%s: %s'", __func__, + con->se_id, key, value); + + /* + * Identify and handle specific HTTP request methods + */ + if (cre->line == 1 && cre->dir == RELAY_DIR_RESPONSE) { + desc->http_method = HTTP_METHOD_RESPONSE; + /* + * Decode response path and query + */ + desc->http_version = strdup(line); + if (desc->http_version == NULL) { + free(line); + goto fail; + } + desc->http_rescode = strdup(value); + if (desc->http_rescode == NULL) { + free(line); + goto fail; + } + desc->http_resmesg = strchr(desc->http_rescode, ' '); + if (desc->http_resmesg == NULL) { + free(line); + goto fail; + } + *desc->http_resmesg++ = '\0'; + if ((desc->http_resmesg = strdup(desc->http_resmesg)) + == NULL) { + free(line); + goto fail; + } + DPRINTF("http_version %s http_rescode %s " + "http_resmesg %s", desc->http_version, + desc->http_rescode, desc->http_resmesg); + goto lookup; + } else if (cre->line == 1 && cre->dir == RELAY_DIR_REQUEST) { + if ((desc->http_method = relay_httpmethod_byname(key)) + == HTTP_METHOD_NONE) + goto fail; + /* + * Decode request path and query + */ + desc->http_path = strdup(value); + if (desc->http_path == NULL) { + free(line); + goto fail; + } + desc->http_version = strchr(desc->http_path, ' '); + if (desc->http_version != NULL) + *desc->http_version++ = '\0'; + desc->http_query = strchr(desc->http_path, '?'); + if (desc->http_query != NULL) + *desc->http_query++ = '\0'; + + /* + * Have to allocate the strings because they could + * be changed independetly by the filters later. + */ + if (desc->http_version != NULL && + (desc->http_version = + strdup(desc->http_version)) == NULL) { + free(line); + goto fail; + } + if (desc->http_query != NULL && + (desc->http_query = + strdup(desc->http_query)) == NULL) { + free(line); + goto fail; + } + } else if (desc->http_method != HTTP_METHOD_NONE && + strcasecmp("Content-Length", key) == 0) { + if (desc->http_method == HTTP_METHOD_TRACE || + desc->http_method == HTTP_METHOD_CONNECT) { + /* + * These method should not have a body + * and thus no Content-Length header. + */ + relay_abort_http(con, 400, "malformed", 0); + goto abort; + } + + /* + * Need to read data from the client after the + * HTTP header. + * XXX What about non-standard clients not using + * the carriage return? And some browsers seem to + * include the line length in the content-length. + */ + cre->toread = strtonum(value, 0, LLONG_MAX, + &errstr); + if (errstr) { + relay_abort_http(con, 500, errstr, 0); + goto abort; + } + } + lookup: + if (strcasecmp("Transfer-Encoding", key) == 0 && + strcasecmp("chunked", value) == 0) + desc->http_chunked = 1; + + if (cre->line != 1) { + if ((hdr = kv_add(&desc->http_headers, key, + value)) == NULL) { + free(line); + goto fail; + } + desc->http_lastheader = hdr; + } + + free(line); + } + if (cre->done) { + if (desc->http_method == HTTP_METHOD_NONE) { + relay_abort_http(con, 406, "no method", 0); + return; + } + + action = relay_test(proto, cre); + if (action == RES_FAIL) { + relay_close(con, "filter rule failed"); + return; + } else if (action != RES_PASS) { + relay_abort_http(con, 403, "Forbidden", con->se_label); + return; + } + + switch (desc->http_method) { + case HTTP_METHOD_CONNECT: + /* Data stream */ + cre->toread = TOREAD_UNLIMITED; + bev->readcb = relay_read; + break; + case HTTP_METHOD_DELETE: + case HTTP_METHOD_GET: + case HTTP_METHOD_HEAD: + case HTTP_METHOD_OPTIONS: + cre->toread = 0; + break; + case HTTP_METHOD_POST: + case HTTP_METHOD_PUT: + case HTTP_METHOD_RESPONSE: + /* HTTP request payload */ + if (cre->toread > 0) + bev->readcb = relay_read_httpcontent; + + /* Single-pass HTTP body */ + if (cre->toread < 0) { + cre->toread = TOREAD_UNLIMITED; + bev->readcb = relay_read; + } + break; + default: + /* HTTP handler */ + cre->toread = TOREAD_HTTP_HEADER; + bev->readcb = relay_read_http; + break; + } + if (desc->http_chunked) { + /* Chunked transfer encoding */ + cre->toread = TOREAD_HTTP_CHUNK_LENGTH; + bev->readcb = relay_read_httpchunks; + } + + if (cre->dir == RELAY_DIR_REQUEST) { + if (relay_writerequest_http(cre->dst, cre) == -1) + goto fail; + } else { + if (relay_writeresponse_http(cre->dst, cre) == -1) + goto fail; + } + if (relay_bufferevent_print(cre->dst, "\r\n") == -1 || + relay_writeheader_http(cre->dst, cre) == -1 || + relay_bufferevent_print(cre->dst, "\r\n") == -1) + goto fail; + + relay_reset_http(cre); + done: + if (cre->dir == RELAY_DIR_REQUEST && cre->toread <= 0 && + cre->dst->bev == NULL) { + if (rlay->rl_conf.fwdmode == FWD_TRANS) { + relay_bindanyreq(con, 0, IPPROTO_TCP); + return; + } + if (relay_connect(con) == -1) + relay_abort_http(con, 502, "session failed", 0); + return; + } + } + if (con->se_done) { + relay_close(con, "last http read (done)"); + return; + } + if (EVBUFFER_LENGTH(src) && bev->readcb != relay_read_http) + bev->readcb(bev, arg); + bufferevent_enable(bev, EV_READ); + if (relay_splice(cre) == -1) + relay_close(con, strerror(errno)); + return; + fail: + relay_abort_http(con, 500, strerror(errno), 0); + return; + abort: + free(line); +} + +void +relay_read_httpcontent(struct bufferevent *bev, void *arg) +{ + struct ctl_relay_event *cre = arg; + struct rsession *con = cre->con; + struct evbuffer *src = EVBUFFER_INPUT(bev); + size_t size; + + getmonotime(&con->se_tv_last); + + size = EVBUFFER_LENGTH(src); + DPRINTF("%s: session %d: size %lu, to read %lld", __func__, + con->se_id, size, cre->toread); + if (!size) + return; + if (relay_spliceadjust(cre) == -1) + goto fail; + + if (cre->toread > 0) { + /* Read content data */ + if ((off_t)size > cre->toread) { + size = cre->toread; + if (relay_bufferevent_write_chunk(cre->dst, src, size) + == -1) + goto fail; + cre->toread = 0; + } else { + if (relay_bufferevent_write_buffer(cre->dst, src) == -1) + goto fail; + cre->toread -= size; + } + DPRINTF("%s: done, size %lu, to read %lld", __func__, + size, cre->toread); + } + if (cre->toread == 0) { + cre->toread = TOREAD_HTTP_HEADER; + bev->readcb = relay_read_http; + } + if (con->se_done) + goto done; + if (bev->readcb != relay_read_httpcontent) + bev->readcb(bev, arg); + bufferevent_enable(bev, EV_READ); + return; + done: + relay_close(con, "last http content read"); + return; + fail: + relay_close(con, strerror(errno)); +} + +void +relay_read_httpchunks(struct bufferevent *bev, void *arg) +{ + struct ctl_relay_event *cre = arg; + struct rsession *con = cre->con; + struct evbuffer *src = EVBUFFER_INPUT(bev); + char *line; + long long llval; + size_t size; + + getmonotime(&con->se_tv_last); + + size = EVBUFFER_LENGTH(src); + DPRINTF("%s: session %d: size %lu, to read %lld", __func__, + con->se_id, size, cre->toread); + if (!size) + return; + if (relay_spliceadjust(cre) == -1) + goto fail; + + if (cre->toread > 0) { + /* Read chunk data */ + if ((off_t)size > cre->toread) { + size = cre->toread; + if (relay_bufferevent_write_chunk(cre->dst, src, size) + == -1) + goto fail; + cre->toread = 0; + } else { + if (relay_bufferevent_write_buffer(cre->dst, src) == -1) + goto fail; + cre->toread -= size; + } + DPRINTF("%s: done, size %lu, to read %lld", __func__, + size, cre->toread); + } + switch (cre->toread) { + case TOREAD_HTTP_CHUNK_LENGTH: + line = evbuffer_readline(src); + if (line == NULL) { + /* Ignore empty line, continue */ + bufferevent_enable(bev, EV_READ); + return; + } + if (strlen(line) == 0) { + free(line); + goto next; + } + + /* + * Read prepended chunk size in hex, ignore the trailer. + * The returned signed value must not be negative. + */ + if (sscanf(line, "%llx", &llval) != 1 || llval < 0) { + free(line); + relay_close(con, "invalid chunk size"); + return; + } + + if (relay_bufferevent_print(cre->dst, line) == -1 || + relay_bufferevent_print(cre->dst, "\r\n") == -1) { + free(line); + goto fail; + } + free(line); + + if ((cre->toread = llval) == 0) { + DPRINTF("%s: last chunk", __func__); + cre->toread = TOREAD_HTTP_CHUNK_TRAILER; + } + break; + case TOREAD_HTTP_CHUNK_TRAILER: + /* Last chunk is 0 bytes followed by trailer and empty line */ + line = evbuffer_readline(src); + if (line == NULL) { + /* Ignore empty line, continue */ + bufferevent_enable(bev, EV_READ); + return; + } + if (relay_bufferevent_print(cre->dst, line) == -1 || + relay_bufferevent_print(cre->dst, "\r\n") == -1) { + free(line); + goto fail; + } + if (strlen(line) == 0) { + /* Switch to HTTP header mode */ + cre->toread = TOREAD_HTTP_HEADER; + bev->readcb = relay_read_http; + } + free(line); + break; + case 0: + /* Chunk is terminated by an empty newline */ + line = evbuffer_readline(src); + if (line != NULL) + free(line); + if (relay_bufferevent_print(cre->dst, "\r\n") == -1) + goto fail; + cre->toread = TOREAD_HTTP_CHUNK_LENGTH; + break; + } + + next: + if (con->se_done) + goto done; + if (EVBUFFER_LENGTH(src)) + bev->readcb(bev, arg); + bufferevent_enable(bev, EV_READ); + return; + + done: + relay_close(con, "last http chunk read (done)"); + return; + fail: + relay_close(con, strerror(errno)); +} + +void +relay_reset_http(struct ctl_relay_event *cre) +{ + struct http_descriptor *desc = cre->desc; + + relay_httpdesc_free(desc); + if (cre->buf != NULL) { + free(cre->buf); + cre->buf = NULL; + cre->buflen = 0; + } + desc->http_method = 0; + desc->http_chunked = 0; + cre->headerlen = 0; + cre->line = 0; + cre->done = 0; +} + +static int +_relay_lookup_url(struct ctl_relay_event *cre, char *host, char *path, + char *query, struct kv *kv) +{ + struct rsession *con = cre->con; + char *val, *md = NULL; + int ret = RES_FAIL; + const char *str = NULL; + + if (asprintf(&val, "%s%s%s%s", + host, path, + query == NULL ? "" : "?", + query == NULL ? "" : query) == -1) { + relay_abort_http(con, 500, "failed to allocate URL", 0); + return (RES_FAIL); + } + + switch (kv->kv_digest) { + case DIGEST_SHA1: + case DIGEST_MD5: + if ((md = digeststr(kv->kv_digest, + val, strlen(val), NULL)) == NULL) { + relay_abort_http(con, 500, + "failed to allocate digest", 0); + goto fail; + } + str = md; + break; + case DIGEST_NONE: + str = val; + break; + } + + DPRINTF("%s: session %d: %s, %s: %d", __func__, con->se_id, + str, kv->kv_key, strcasecmp(kv->kv_key, str)); + + if (strcasecmp(kv->kv_key, str) == 0) { + ret = RES_DROP; + goto fail; + } + + ret = RES_PASS; + fail: + if (md != NULL) + free(md); + free(val); + return (ret); +} + +int +relay_lookup_url(struct ctl_relay_event *cre, const char *host, struct kv *kv) +{ + struct rsession *con = cre->con; + struct http_descriptor *desc = (struct http_descriptor *)cre->desc; + int i, j, dots; + char *hi[RELAY_MAXLOOKUPLEVELS], *p, *pp, *c, ch; + char ph[MAXHOSTNAMELEN]; + int ret; + + if (desc->http_path == NULL) + return (RES_PASS); + + /* + * This is an URL lookup algorithm inspired by + * http://code.google.com/apis/safebrowsing/ + * developers_guide.html#PerformingLookups + */ + + DPRINTF("%s: session %d: host '%s', path '%s', query '%s'", + __func__, con->se_id, host, desc->http_path, + desc->http_query == NULL ? "" : desc->http_query); + + if (canonicalize_host(host, ph, sizeof(ph)) == NULL) { + relay_abort_http(con, 400, "invalid host name", 0); + return (RES_FAIL); + } + + bzero(hi, sizeof(hi)); + for (dots = -1, i = strlen(ph) - 1; i > 0; i--) { + if (ph[i] == '.' && ++dots) + hi[dots - 1] = &ph[i + 1]; + if (dots > (RELAY_MAXLOOKUPLEVELS - 2)) + break; + } + if (dots == -1) + dots = 0; + hi[dots] = ph; + + if ((pp = strdup(desc->http_path)) == NULL) { + relay_abort_http(con, 500, "failed to allocate path", 0); + return (RES_FAIL); + } + for (i = (RELAY_MAXLOOKUPLEVELS - 1); i >= 0; i--) { + if (hi[i] == NULL) + continue; + + /* 1. complete path with query */ + if (desc->http_query != NULL) + if ((ret = _relay_lookup_url(cre, hi[i], + pp, desc->http_query, kv)) != RES_PASS) + goto done; + + /* 2. complete path without query */ + if ((ret = _relay_lookup_url(cre, hi[i], + pp, NULL, kv)) != RES_PASS) + goto done; + + /* 3. traverse path */ + for (j = 0, p = strchr(pp, '/'); + p != NULL; p = strchr(p, '/'), j++) { + if (j > (RELAY_MAXLOOKUPLEVELS - 2) || *(++p) == '\0') + break; + c = &pp[p - pp]; + ch = *c; + *c = '\0'; + if ((ret = _relay_lookup_url(cre, hi[i], + pp, NULL, kv)) != RES_PASS) + goto done; + *c = ch; + } + } + + ret = RES_PASS; + done: + free(pp); + return (ret); +} + +int +relay_lookup_cookie(struct ctl_relay_event *cre, const char *str, + struct kv *kv) +{ + struct rsession *con = cre->con; + char *val, *ptr, *key, *value; + int ret; + + if ((val = strdup(str)) == NULL) { + relay_abort_http(con, 500, "failed to allocate cookie", 0); + return (RES_FAIL); + } + + for (ptr = val; ptr != NULL && strlen(ptr);) { + if (*ptr == ' ') + *ptr++ = '\0'; + key = ptr; + if ((ptr = strchr(ptr, ';')) != NULL) + *ptr++ = '\0'; + /* + * XXX We do not handle attributes + * ($Path, $Domain, or $Port) + */ + if (*key == '$') + continue; + + if ((value = + strchr(key, '=')) == NULL || + strlen(value) < 1) + continue; + *value++ = '\0'; + if (*value == '"') + *value++ = '\0'; + if (value[strlen(value) - 1] == '"') + value[strlen(value) - 1] = '\0'; + + DPRINTF("%s: session %d: %s = %s, %s = %s : %d", + __func__, con->se_id, + key, value, kv->kv_key, kv->kv_value, + strcasecmp(kv->kv_key, key)); + + if (strcasecmp(kv->kv_key, key) == 0 && + ((kv->kv_value == NULL) || + (fnmatch(kv->kv_value, value, + FNM_CASEFOLD) != FNM_NOMATCH))) { + ret = RES_DROP; + goto done; + } + } + + ret = RES_PASS; + + done: + free(val); + return (ret); +} + +int +relay_lookup_query(struct ctl_relay_event *cre, struct kv *kv) +{ + struct http_descriptor *desc = cre->desc; + struct kv *match = &desc->http_matchquery; + char *val, *ptr, *tmpkey = NULL, *tmpval = NULL; + int ret = -1; + + if (desc->http_query == NULL) + return (-1); + if ((val = strdup(desc->http_query)) == NULL) { + relay_abort_http(cre->con, 500, "failed to allocate query", 0); + return (-1); + } + + ptr = val; + while (ptr != NULL && strlen(ptr)) { + tmpkey = ptr; + if ((ptr = strchr(ptr, '&')) != NULL) + *ptr++ = '\0'; + if ((tmpval = strchr(tmpkey, '=')) == NULL || strlen(tmpval) + < 1) + continue; + *tmpval++ = '\0'; + + if (fnmatch(kv->kv_key, tmpkey, 0) != FNM_NOMATCH && + (kv->kv_value == NULL || fnmatch(kv->kv_value, tmpval, 0) + != FNM_NOMATCH)) + break; + else + tmpkey = NULL; + } + + if (tmpkey == NULL || tmpval == NULL) + goto done; + + match->kv_key = strdup(tmpkey); + if (match->kv_key == NULL) + goto done; + match->kv_value = strdup(tmpval); + if (match->kv_key == NULL) + goto done; + ret = 0; + + done: + free(val); + return (ret); +} + + +void +relay_abort_http(struct rsession *con, u_int code, const char *msg, + u_int16_t labelid) +{ + struct relay *rlay = con->se_relay; + struct bufferevent *bev = con->se_in.bev; + const char *httperr = NULL, *text = ""; + char *httpmsg; + time_t t; + struct tm *lt; + char tmbuf[32], hbuf[128]; + const char *style, *label = NULL; + + if ((httperr = relay_httperror_byid(code)) == NULL) + httperr = "Unknown Error"; + + if (labelid != 0) + label = label_id2name(labelid); + + /* In some cases this function may be called from generic places */ + if (rlay->rl_proto->type != RELAY_PROTO_HTTP || + (rlay->rl_proto->flags & F_RETURN) == 0) { + relay_close(con, msg); + return; + } + + if (bev == NULL) + goto done; + + /* Some system information */ + if (print_host(&rlay->rl_conf.ss, hbuf, sizeof(hbuf)) == NULL) + goto done; + + /* RFC 2616 "tolerates" asctime() */ + time(&t); + lt = localtime(&t); + tmbuf[0] = '\0'; + if (asctime_r(lt, tmbuf) != NULL) + tmbuf[strlen(tmbuf) - 1] = '\0'; /* skip final '\n' */ + + /* Do not send details of the Internal Server Error */ + if (code != 500) + text = msg; + + /* A CSS stylesheet allows minimal customization by the user */ + if ((style = rlay->rl_proto->style) == NULL) + style = "body { background-color: #a00000; color: white; }"; + + /* Generate simple HTTP+HTML error document */ + if (asprintf(&httpmsg, + "HTTP/1.0 %03d %s\r\n" + "Date: %s\r\n" + "Server: %s\r\n" + "Connection: close\r\n" + "Content-Type: text/html\r\n" + "\r\n" + "<!DOCTYPE HTML PUBLIC " + "\"-//W3C//DTD HTML 4.01 Transitional//EN\">\n" + "<html>\n" + "<head>\n" + "<title>%03d %s</title>\n" + "<style type=\"text/css\"><!--\n%s\n--></style>\n" + "</head>\n" + "<body>\n" + "<h1>%s</h1>\n" + "<div id='m'>%s</div>\n" + "<div id='l'>%s</div>\n" + "<hr><address>%s at %s port %d</address>\n" + "</body>\n" + "</html>\n", + code, httperr, tmbuf, RELAYD_SERVERNAME, + code, httperr, style, httperr, text, + label == NULL ? "" : label, + RELAYD_SERVERNAME, hbuf, ntohs(rlay->rl_conf.port)) == -1) + goto done; + + /* Dump the message without checking for success */ + relay_dump(&con->se_in, httpmsg, strlen(httpmsg)); + free(httpmsg); + + done: + if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) + relay_close(con, msg); + else { + relay_close(con, httpmsg); + free(httpmsg); + } +} + +void +relay_close_http(struct rsession *con) +{ + struct http_descriptor *desc[2] = { + con->se_in.desc, con->se_out.desc + }; + int i; + + for (i = 0; i < 2; i++) { + if (desc[i] == NULL) + continue; + relay_httpdesc_free(desc[i]); + free(desc[i]); + } +} + +char * +relay_expand_http(struct ctl_relay_event *cre, char *val, char *buf, + size_t len) +{ + struct rsession *con = cre->con; + struct relay *rlay = con->se_relay; + char ibuf[128]; + + if (strlcpy(buf, val, len) >= len) + return (NULL); + + if (strstr(val, "$REMOTE_") != NULL) { + if (strstr(val, "$REMOTE_ADDR") != NULL) { + if (print_host(&cre->ss, ibuf, sizeof(ibuf)) == NULL) + return (NULL); + if (expand_string(buf, len, + "$REMOTE_ADDR", ibuf) != 0) + return (NULL); + } + if (strstr(val, "$REMOTE_PORT") != NULL) { + snprintf(ibuf, sizeof(ibuf), "%u", ntohs(cre->port)); + if (expand_string(buf, len, + "$REMOTE_PORT", ibuf) != 0) + return (NULL); + } + } + if (strstr(val, "$SERVER_") != NULL) { + if (strstr(val, "$SERVER_ADDR") != NULL) { + if (print_host(&rlay->rl_conf.ss, + ibuf, sizeof(ibuf)) == NULL) + return (NULL); + if (expand_string(buf, len, + "$SERVER_ADDR", ibuf) != 0) + return (NULL); + } + if (strstr(val, "$SERVER_PORT") != NULL) { + snprintf(ibuf, sizeof(ibuf), "%u", + ntohs(rlay->rl_conf.port)); + if (expand_string(buf, len, + "$SERVER_PORT", ibuf) != 0) + return (NULL); + } + if (strstr(val, "$SERVER_NAME") != NULL) { + if (expand_string(buf, len, + "$SERVER_NAME", RELAYD_SERVERNAME) != 0) + return (NULL); + } + } + if (strstr(val, "$TIMEOUT") != NULL) { + snprintf(ibuf, sizeof(ibuf), "%lld", + (long long)rlay->rl_conf.timeout.tv_sec); + if (expand_string(buf, len, "$TIMEOUT", ibuf) != 0) + return (NULL); + } + + return (buf); +} + +int +relay_writerequest_http(struct ctl_relay_event *dst, + struct ctl_relay_event *cre) +{ + struct http_descriptor *desc = (struct http_descriptor *)cre->desc; + const char *name = NULL; + + if ((name = relay_httpmethod_byid(desc->http_method)) == NULL) + return (-1); + + if (relay_bufferevent_print(dst, name) == -1 || + relay_bufferevent_print(dst, " ") == -1 || + relay_bufferevent_print(dst, desc->http_path) == -1 || + (desc->http_query != NULL && + (relay_bufferevent_print(dst, "?") == -1 || + relay_bufferevent_print(dst, desc->http_query) == -1)) || + relay_bufferevent_print(dst, " ") == -1 || + relay_bufferevent_print(dst, desc->http_version) == -1) + return (-1); + + return (0); +} + +int +relay_writeresponse_http(struct ctl_relay_event *dst, + struct ctl_relay_event *cre) +{ + struct http_descriptor *desc = (struct http_descriptor *)cre->desc; + + DPRINTF("version: %s rescode: %s resmsg: %s", desc->http_version, + desc->http_rescode, desc->http_resmesg); + + if (relay_bufferevent_print(dst, desc->http_version) == -1 || + relay_bufferevent_print(dst, " ") == -1 || + relay_bufferevent_print(dst, desc->http_rescode) == -1 || + relay_bufferevent_print(dst, " ") == -1 || + relay_bufferevent_print(dst, desc->http_resmesg) == -1) + return (-1); + + return (0); +} + +int +relay_writeheader_kv(struct ctl_relay_event *dst, struct kv *hdr) +{ + char *ptr; + const char *key; + + if (hdr->kv_flags & KV_FLAG_INVALID) + return (0); + + /* The key might have been updated in the parent */ + if (hdr->kv_parent != NULL && hdr->kv_parent->kv_key != NULL) + key = hdr->kv_parent->kv_key; + else + key = hdr->kv_key; + + ptr = hdr->kv_value; + DPRINTF("%s: ptr %s", __func__, ptr); + if (relay_bufferevent_print(dst, key) == -1 || + (ptr != NULL && + (relay_bufferevent_print(dst, ": ") == -1 || + relay_bufferevent_print(dst, ptr) == -1 || + relay_bufferevent_print(dst, "\r\n") == -1))) + return (-1); + DPRINTF("%s: %s: %s", __func__, key, + hdr->kv_value == NULL ? "" : hdr->kv_value); + + return (0); +} + +int +relay_writeheader_http(struct ctl_relay_event *dst, struct ctl_relay_event + *cre) +{ + struct kv *hdr, *kv; + struct http_descriptor *desc = (struct http_descriptor *)cre->desc; + + RB_FOREACH(hdr, kvtree, &desc->http_headers) { + if (relay_writeheader_kv(dst, hdr) == -1) + return (-1); + TAILQ_FOREACH(kv, &hdr->kv_children, kv_entry) { + if (relay_writeheader_kv(dst, kv) == -1) + return (-1); + } + } + + return (0); +} + +enum httpmethod +relay_httpmethod_byname(const char *name) +{ + enum httpmethod id = HTTP_METHOD_NONE; + struct http_method method, *res = NULL; + + /* Set up key */ + method.method_name = name; + + /* + * RFC 2616 section 5.1.1 says that the method is case + * sensitive so we don't do a strcasecmp here. + */ + if ((res = bsearch(&method, http_methods, + sizeof(http_methods) / sizeof(http_methods[0]) - 1, + sizeof(http_methods[0]), relay_httpmethod_cmp)) != NULL) + id = res->method_id; + + return (id); +} + +const char * +relay_httpmethod_byid(u_int id) +{ + const char *name = NULL; + int i; + + for (i = 0; http_methods[i].method_name != NULL; i++) { + if (http_methods[i].method_id == id) { + name = http_methods[i].method_name; + break; + } + } + + return (name); +} + +static int +relay_httpmethod_cmp(const void *a, const void *b) +{ + const struct http_method *ma = a; + const struct http_method *mb = b; + return (strcmp(ma->method_name, mb->method_name)); +} + +const char * +relay_httperror_byid(u_int id) +{ + struct http_error error, *res = NULL; + + /* Set up key */ + error.error_code = (int)id; + + res = bsearch(&error, http_errors, + sizeof(http_errors) / sizeof(http_errors[0]) - 1, + sizeof(http_errors[0]), relay_httperror_cmp); + + return (res->error_name); +} + +static int +relay_httperror_cmp(const void *a, const void *b) +{ + const struct http_error *ea = a; + const struct http_error *eb = b; + return (ea->error_code - eb->error_code); +} + +int +relay_httpquery_test(struct ctl_relay_event *cre, struct relay_rule *rule, + struct kvlist *actions) +{ + struct http_descriptor *desc = cre->desc; + struct kv *match = &desc->http_matchquery; + struct kv *kv = &rule->rule_kv[KEY_TYPE_QUERY]; + + if (cre->dir == RELAY_DIR_RESPONSE || kv->kv_type != KEY_TYPE_QUERY) + return (0); + else if (kv->kv_key == NULL) + return (0); + else if (relay_lookup_query(cre, kv)) + return (-1); + + relay_match(actions, kv, match, NULL); + + return (0); +} + +int +relay_httpheader_test(struct ctl_relay_event *cre, struct relay_rule *rule, + struct kvlist *actions) +{ + struct http_descriptor *desc = cre->desc; + struct kv *kv = &rule->rule_kv[KEY_TYPE_HEADER]; + struct kv *match; + + if (kv->kv_type != KEY_TYPE_HEADER) + return (0); + + match = kv_find(&desc->http_headers, kv); + + if (kv->kv_option == KEY_OPTION_APPEND || + kv->kv_option == KEY_OPTION_SET) { + /* header can be NULL and will be added later */ + } else if (match == NULL) { + /* Fail if header doesn't exist */ + return (-1); + } + + relay_match(actions, kv, match, &desc->http_headers); + + return (0); +} + +int +relay_httppath_test(struct ctl_relay_event *cre, struct relay_rule *rule, + struct kvlist *actions) +{ + struct http_descriptor *desc = cre->desc; + struct kv *kv = &rule->rule_kv[KEY_TYPE_PATH]; + struct kv *match = &desc->http_pathquery; + const char *query; + + if (cre->dir == RELAY_DIR_RESPONSE || kv->kv_type != KEY_TYPE_PATH) + return (0); + else if (kv->kv_key == NULL) + return (0); + else if (fnmatch(kv->kv_key, desc->http_path, 0) == FNM_NOMATCH) + return (-1); + else if (kv->kv_value != NULL && kv->kv_option == KEY_OPTION_NONE) { + query = desc->http_query == NULL ? "" : desc->http_query; + if (fnmatch(kv->kv_value, query, FNM_CASEFOLD) == FNM_NOMATCH) + return (-1); + } + + relay_match(actions, kv, match, NULL); + + return (0); +} + +int +relay_httpurl_test(struct ctl_relay_event *cre, struct relay_rule *rule, + struct kvlist *actions) +{ + struct http_descriptor *desc = cre->desc; + struct kv *host, key; + struct kv *kv = &rule->rule_kv[KEY_TYPE_URL]; + struct kv *match = &desc->http_pathquery; + + if (cre->dir == RELAY_DIR_RESPONSE || kv->kv_type != KEY_TYPE_URL || + kv->kv_key == NULL) + return (0); + + key.kv_key = "Host"; + host = kv_find(&desc->http_headers, &key); + + if (host == NULL || host->kv_value == NULL) + return (0); + else if (rule->rule_action != RULE_ACTION_BLOCK && + kv->kv_option == KEY_OPTION_LOG && + fnmatch(kv->kv_key, match->kv_key, FNM_CASEFOLD) != FNM_NOMATCH) { + /* fnmatch url only for logging */ + } else if (relay_lookup_url(cre, host->kv_value, kv) != 0) + return (-1); + + relay_match(actions, kv, match, NULL); + + return (0); +} + +int +relay_httpcookie_test(struct ctl_relay_event *cre, struct relay_rule *rule, + struct kvlist *actions) +{ + struct http_descriptor *desc = cre->desc; + struct kv *kv = &rule->rule_kv[KEY_TYPE_COOKIE], key; + struct kv *match = NULL; + + if (kv->kv_type != KEY_TYPE_COOKIE) + return (0); + + switch (cre->dir) { + case RELAY_DIR_REQUEST: + key.kv_key = "Cookie"; + break; + case RELAY_DIR_RESPONSE: + key.kv_key = "Set-Cookie"; + break; + default: + return (0); + /* NOTREACHED */ + break; + } + + if (kv->kv_option == KEY_OPTION_APPEND || + kv->kv_option == KEY_OPTION_SET) { + /* no cookie, can be NULL and will be added later */ + } else { + match = kv_find(&desc->http_headers, &key); + if (match == NULL) + return (-1); + if (kv->kv_key == NULL || match->kv_value == NULL) + return (0); + else if (relay_lookup_cookie(cre, match->kv_value, kv) != 0) + return (-1); + } + + relay_match(actions, kv, match, &desc->http_headers); + + return (0); +} + +int +relay_match_actions(struct ctl_relay_event *cre, struct relay_rule *rule, + struct kvlist *matches, struct kvlist *actions) +{ + struct rsession *con = cre->con; + struct kv *kv; + + /* + * Apply the following options instantly (action per match). + */ + if (rule->rule_table != NULL) + con->se_table = rule->rule_table; + + if (rule->rule_tag != 0) + con->se_tag = rule->rule_tag == -1 ? 0 : rule->rule_tag; + + if (rule->rule_label != 0) + con->se_label = rule->rule_label == -1 ? 0 : rule->rule_label; + + /* + * Apply the remaining options once after evaluation. + */ + if (matches == NULL) { + /* 'pass' or 'block' rule */ + TAILQ_FOREACH(kv, &rule->rule_kvlist, kv_rule_entry) { + TAILQ_INSERT_TAIL(actions, kv, kv_action_entry); + TAILQ_REMOVE(&rule->rule_kvlist, kv, kv_rule_entry); + } + } else { + /* 'match' rule */ + TAILQ_FOREACH(kv, matches, kv_match_entry) { + TAILQ_INSERT_TAIL(actions, kv, kv_action_entry); + } + } + + return (0); +} + +int +relay_apply_actions(struct ctl_relay_event *cre, struct kvlist *actions) +{ + struct rsession *con = cre->con; + struct http_descriptor *desc = cre->desc; + struct kv *host = NULL; + const char *value; + struct kv *kv, *match, *kp, *mp, kvcopy, matchcopy, key; + int addkv, ret; + char buf[IBUF_READ_SIZE], *ptr; + + memset(&kvcopy, 0, sizeof(kvcopy)); + memset(&matchcopy, 0, sizeof(matchcopy)); + + ret = -1; + kp = mp = NULL; + TAILQ_FOREACH(kv, actions, kv_action_entry) { + kp = NULL; + match = kv->kv_match; + addkv = 0; + + /* + * Although marked as deleted, give a chance to non-critical + * actions, ie. log, to be performed + */ + if (match != NULL && (match->kv_flags & KV_FLAG_INVALID)) + goto matchdel; + + switch (kv->kv_option) { + case KEY_OPTION_APPEND: + case KEY_OPTION_SET: + switch (kv->kv_type) { + case KEY_TYPE_PATH: + if (kv->kv_option == KEY_OPTION_APPEND) { + if (kv_setkey(match, "%s%s", + match->kv_key, kv->kv_key) == -1) + goto fail; + } else { + if (kv_setkey(match, "%s", + kv->kv_value) == -1) + goto fail; + } + break; + case KEY_TYPE_COOKIE: + kp = &kvcopy; + if (kv_inherit(kp, kv) == NULL) + goto fail; + if (kv_set(kp, "%s=%s;", kp->kv_key, + kp->kv_value) == -1) + goto fail; + if (kv_setkey(kp, "%s", cre->dir == + RELAY_DIR_REQUEST ? + "Cookie" : "Set-Cookie") == -1) + goto fail; + /* FALLTHROUGH cookie is a header */ + case KEY_TYPE_HEADER: + if (match == NULL) { + addkv = 1; + break; + } + if (match->kv_value == NULL || + kv->kv_option == KEY_OPTION_SET) { + if (kv_set(match, "%s", + kv->kv_value) == -1) + goto fail; + } else { + if (kv_setkey(match, "%s,%s", + match->kv_key, kv->kv_key) == -1) + goto fail; + } + break; + default: + /* query, url not supported */ + break; + } + break; + case KEY_OPTION_REMOVE: + switch (kv->kv_type) { + case KEY_TYPE_PATH: + if (kv_setkey(match, "/") == -1) + goto fail; + break; + case KEY_TYPE_COOKIE: + case KEY_TYPE_HEADER: + if (kv->kv_matchtree != NULL) + match->kv_flags |= KV_FLAG_INVALID; + else + kv_free(match); + match = kv->kv_match = NULL; + break; + default: + /* query and url not supported */ + break; + } + break; + case KEY_OPTION_HASH: + switch (kv->kv_type) { + case KEY_TYPE_PATH: + value = match->kv_key; + break; + default: + value = match->kv_value; + break; + } + if (!con->se_hashkeyset) + con->se_hashkey = HASHINIT; + con->se_hashkey = hash32_str(value, con->se_hashkey); + con->se_hashkeyset = 1; + log_debug("%s: hashkey 0x%04x", __func__, + con->se_hashkey); + break; + case KEY_OPTION_LOG: + /* perform this later */ + break; + default: + fatalx("relay_action: invalid action"); + /* NOTREACHED */ + } + + /* from now on, reads from kp writes to kv */ + if (kp == NULL) + kp = kv; + if (addkv && kv->kv_matchtree != NULL) { + /* Add new entry to the list (eg. new HTTP header) */ + if ((match = kv_add(kv->kv_matchtree, kp->kv_key, + kp->kv_value)) == NULL) + goto fail; + match->kv_option = kp->kv_option; + match->kv_type = kp->kv_type; + kv->kv_match = match; + } + if (match != NULL && kp->kv_flags & KV_FLAG_MACRO) { + bzero(buf, sizeof(buf)); + if ((ptr = relay_expand_http(cre, kp->kv_value, buf, + sizeof(buf))) == NULL) + goto fail; + if (kv_set(match, ptr) == -1) + goto fail; + } + + matchdel: + switch(kv->kv_option) { + case KEY_OPTION_LOG: + if (match == NULL) + break; + mp = &matchcopy; + if (kv_inherit(mp, match) == NULL) + goto fail; + if (mp->kv_flags & KV_FLAG_INVALID) { + if (kv_set(mp, "%s (removed)", + mp->kv_value) == -1) + goto fail; + } + switch(kv->kv_type) { + case KEY_TYPE_URL: + key.kv_key = "Host"; + host = kv_find(&desc->http_headers, &key); + switch (kv->kv_digest) { + case DIGEST_NONE: + if (host == NULL || + host->kv_value == NULL) + break; + if (kv_setkey(mp, "%s%s", + host->kv_value, mp->kv_key) == + -1) + goto fail; + break; + default: + if (kv_setkey(mp, "%s", kv->kv_key) + == -1) + goto fail; + break; + } + break; + default: + break; + } + if (kv_log(con->se_log, mp, con->se_label) == -1) + goto fail; + break; + default: + break; + } + + /* actions applied, cleanup kv */ + kv->kv_match = NULL; + kv->kv_matchtree = NULL; + TAILQ_REMOVE(actions, kv, kv_match_entry); + + kv_free(&kvcopy); + kv_free(&matchcopy); + } + + ret = 0; + fail: + kv_free(&kvcopy); + kv_free(&matchcopy); + + return (ret); +} + +#define RELAY_GET_SKIP_STEP(i) \ + do { \ + r = r->rule_skip[i]; \ + DPRINTF("%s:%d: skip %d rules", __func__, __LINE__, i); \ + } while (0) + +#define RELAY_GET_NEXT_STEP \ + do { \ + DPRINTF("%s:%d: next rule", __func__, __LINE__); \ + goto nextrule; \ + } while (0) + +int +relay_test(struct protocol *proto, struct ctl_relay_event *cre) +{ + struct rsession *con; + struct http_descriptor *desc = cre->desc; + struct relay_rule *r = NULL, *rule = NULL; + u_int cnt = 0; + u_int action = RES_PASS; + struct kvlist actions, matches; + struct kv *kv; + + con = cre->con; + TAILQ_INIT(&actions); + + r = TAILQ_FIRST(&proto->rules); + while (r != NULL) { + cnt++; + TAILQ_INIT(&matches); + TAILQ_INIT(&r->rule_kvlist); + if (r->rule_dir && r->rule_dir != cre->dir) + RELAY_GET_SKIP_STEP(RULE_SKIP_DIR); + else if (proto->type != r->rule_proto) + RELAY_GET_SKIP_STEP(RULE_SKIP_PROTO); + else if (r->rule_af != AF_UNSPEC && + (cre->ss.ss_family != r->rule_af || + cre->dst->ss.ss_family != r->rule_af)) + RELAY_GET_SKIP_STEP(RULE_SKIP_AF); + else if (RELAY_ADDR_CMP(&r->rule_src, &cre->ss) != 0) + RELAY_GET_SKIP_STEP(RULE_SKIP_SRC); + else if (RELAY_ADDR_CMP(&r->rule_dst, &cre->dst->ss) != 0) + RELAY_GET_SKIP_STEP(RULE_SKIP_DST); + else if (r->rule_method != HTTP_METHOD_NONE && + (desc->http_method == HTTP_METHOD_RESPONSE || + desc->http_method != r->rule_method)) + RELAY_GET_SKIP_STEP(RULE_SKIP_METHOD); + else if (r->rule_tagged && con->se_tag != r->rule_tagged) + RELAY_GET_NEXT_STEP; + else if (relay_httpheader_test(cre, r, &matches) != 0) + RELAY_GET_NEXT_STEP; + else if (relay_httpquery_test(cre, r, &matches) != 0) + RELAY_GET_NEXT_STEP; + else if (relay_httppath_test(cre, r, &matches) != 0) + RELAY_GET_NEXT_STEP; + else if (relay_httpurl_test(cre, r, &matches) != 0) + RELAY_GET_NEXT_STEP; + else if (relay_httpcookie_test(cre, r, &matches) != 0) + RELAY_GET_NEXT_STEP; + else { + DPRINTF("%s: session %d: matched rule %d", + __func__, con->se_id, r->rule_id); + + if (r->rule_action == RULE_ACTION_MATCH) { + if (relay_match_actions(cre, r, &matches, + &actions) != 0) { + /* Something bad happened, drop */ + action = RES_DROP; + break; + } + RELAY_GET_NEXT_STEP; + } else if (r->rule_action == RULE_ACTION_BLOCK) + action = RES_DROP; + else if (r->rule_action == RULE_ACTION_PASS) + action = RES_PASS; + + /* Rule matched */ + rule = r; + + /* Temporarily save actions */ + TAILQ_FOREACH(kv, &matches, kv_match_entry) { + TAILQ_INSERT_TAIL(&rule->rule_kvlist, + kv, kv_rule_entry); + } + + if (rule->rule_flags & RULE_FLAG_QUICK) + break; + + nextrule: + /* Continue to find last matching policy */ + r = TAILQ_NEXT(r, rule_entry); + } + } + + if (rule != NULL && + relay_match_actions(cre, rule, NULL, &actions) != 0) { + /* Something bad happened, drop */ + action = RES_DROP; + } + + if (relay_apply_actions(cre, &actions) != 0) { + /* Something bad happened, drop */ + action = RES_DROP; + } + + DPRINTF("%s: session %d: action %d", __func__, + con->se_id, action); + + return (action); +} + +#define RELAY_SET_SKIP_STEPS(i) \ + do { \ + while (head[i] != cur) { \ + head[i]->rule_skip[i] = cur; \ + head[i] = TAILQ_NEXT(head[i], rule_entry); \ + } \ + } while (0) + +/* This code is derived from pf_calc_skip_steps() from pf.c */ +void +relay_calc_skip_steps(struct relay_rules *rules) +{ + struct relay_rule *head[RULE_SKIP_COUNT], *cur, *prev; + int i; + + cur = TAILQ_FIRST(rules); + prev = cur; + for (i = 0; i < RULE_SKIP_COUNT; ++i) + head[i] = cur; + while (cur != NULL) { + if (cur->rule_dir != prev->rule_dir) + RELAY_SET_SKIP_STEPS(RULE_SKIP_DIR); + else if (cur->rule_proto != prev->rule_proto) + RELAY_SET_SKIP_STEPS(RULE_SKIP_PROTO); + else if (cur->rule_af != prev->rule_af) + RELAY_SET_SKIP_STEPS(RULE_SKIP_AF); + else if (RELAY_ADDR_NEQ(&cur->rule_src, &prev->rule_src)) + RELAY_SET_SKIP_STEPS(RULE_SKIP_SRC); + else if (RELAY_ADDR_NEQ(&cur->rule_dst, &prev->rule_dst)) + RELAY_SET_SKIP_STEPS(RULE_SKIP_DST); + else if (cur->rule_method != prev->rule_method) + RELAY_SET_SKIP_STEPS(RULE_SKIP_METHOD); + + prev = cur; + cur = TAILQ_NEXT(cur, rule_entry); + } + for (i = 0; i < RULE_SKIP_COUNT; ++i) + RELAY_SET_SKIP_STEPS(i); +} + +void +relay_match(struct kvlist *actions, struct kv *kv, struct kv *match, + struct kvtree *matchtree) +{ + if (kv->kv_option != KEY_OPTION_NONE) { + kv->kv_match = match; + kv->kv_matchtree = matchtree; + TAILQ_INSERT_TAIL(actions, kv, kv_match_entry); + } +} |