From cbced0bd4e14972a40a5dcfce5bb001f5654350f Mon Sep 17 00:00:00 2001 From: ian Date: Sun, 24 Oct 2021 16:01:04 +0000 Subject: [PATCH] Add httpd custom error page facility. Adapted by me from https://github.com/mpfr/httpd-plus. Improvements from & (earlier version) reads fine to tracey@; improvements & OK this version benno@, florian@. Thanks. --- usr.sbin/httpd/config.c | 9 +++- usr.sbin/httpd/httpd.conf.5 | 49 +++++++++++++++++++- usr.sbin/httpd/httpd.h | 11 ++++- usr.sbin/httpd/parse.y | 43 +++++++++++++++++- usr.sbin/httpd/server_http.c | 87 ++++++++++++++++++++++++++++++++++-- 5 files changed, 189 insertions(+), 10 deletions(-) diff --git a/usr.sbin/httpd/config.c b/usr.sbin/httpd/config.c index 804d58ea728..fe5c2ab9110 100644 --- a/usr.sbin/httpd/config.c +++ b/usr.sbin/httpd/config.c @@ -1,4 +1,4 @@ -/* $OpenBSD: config.c,v 1.61 2020/09/21 09:42:07 tobhe Exp $ */ +/* $OpenBSD: config.c,v 1.62 2021/10/24 16:01:04 ian Exp $ */ /* * Copyright (c) 2011 - 2015 Reyk Floeter @@ -51,6 +51,9 @@ config_init(struct httpd *env) CONFIG_SERVERS|CONFIG_MEDIA|CONFIG_AUTH; ps->ps_what[PROC_LOGGER] = CONFIG_SERVERS; + (void)strlcpy(env->sc_errdocroot, "", + sizeof(env->sc_errdocroot)); + /* Other configuration */ what = ps->ps_what[privsep_process]; @@ -585,6 +588,10 @@ config_getserver_config(struct httpd *env, struct server *srv, srv_conf->maxrequests = parent->maxrequests; srv_conf->maxrequestbody = parent->maxrequestbody; + srv_conf->flags |= parent->flags & SRVFLAG_ERRDOCS; + (void)strlcpy(srv_conf->errdocroot, parent->errdocroot, + sizeof(srv_conf->errdocroot)); + DPRINTF("%s: %s %d location \"%s\", " "parent \"%s[%u]\", flags: %s", __func__, ps->ps_title[privsep_process], ps->ps_instance, diff --git a/usr.sbin/httpd/httpd.conf.5 b/usr.sbin/httpd/httpd.conf.5 index 18d4358f81d..3c6a1b7d583 100644 --- a/usr.sbin/httpd/httpd.conf.5 +++ b/usr.sbin/httpd/httpd.conf.5 @@ -1,4 +1,4 @@ -.\" $OpenBSD: httpd.conf.5,v 1.118 2021/06/07 10:53:59 tb Exp $ +.\" $OpenBSD: httpd.conf.5,v 1.119 2021/10/24 16:01:04 ian Exp $ .\" .\" Copyright (c) 2014, 2015 Reyk Floeter .\" @@ -14,7 +14,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: June 7 2021 $ +.Dd $Mdocdate: October 24 2021 $ .Dt HTTPD.CONF 5 .Os .Sh NAME @@ -121,6 +121,44 @@ see the section below. If not specified, the default type is set to .Ar application/octet-stream . +.It Ic errdocs Ar directory +Let +.Xr httpd 8 +return custom error documents instead of the built-in ones. +.Pp +.Ar directory +is relative to the +.Ic chroot . +.Pp +Custom error documents are standalone +.Dq .html +files provided in one of the following ways: +.Bl -bullet -offset indent -compact +.It +As HTML files named after the 3-digit HTTP response code they are used +for, e.g., +.Pa 404.html . +.It +As a generic template file named +.Pa err.html +which is used for response codes no dedicated file is provided for. +.El +.Pp +In case the latter does not exist and there is no dedicated file available for +a certain response code, the built-in error document will be used as fallback. +.Pp +A custom error document may contain the following macros that will be expanded +at runtime: +.Pp +.Bl -tag -width $RESPONSE_CODE -offset indent -compact +.It Ic $HTTP_ERROR +The error message text. +.It Ic $RESPONSE_CODE +The 3-digit HTTP response code sent to the client. +.It Ic $SERVER_SOFTWARE +The server software name of +.Xr httpd 8 . +.El .It Ic logdir Ar directory Specifies the full path of the directory in which log files will be written. If not specified, it defaults to @@ -279,6 +317,12 @@ Disable the directory index. .Xr httpd 8 will neither display nor generate a directory index. .El +.It Oo Ic no Oc Ic errdocs Ar directory +Overrides or, if the +.Ic no +keyword is given, disables globally defined custom error documents for the +current +.Ic server . .It Oo Ic no Oc Ic fastcgi Oo Ar option Oc Enable FastCGI instead of serving files. Multiple options may be specified within curly braces. @@ -418,6 +462,7 @@ A location section may include most of the server configuration rules except .Ic alias , .Ic connection , +.Ic errdocs , .Ic hsts , .Ic listen on , .Ic location , diff --git a/usr.sbin/httpd/httpd.h b/usr.sbin/httpd/httpd.h index 4df7de216c2..6be274ccb7c 100644 --- a/usr.sbin/httpd/httpd.h +++ b/usr.sbin/httpd/httpd.h @@ -1,4 +1,4 @@ -/* $OpenBSD: httpd.h,v 1.157 2021/05/17 09:26:52 florian Exp $ */ +/* $OpenBSD: httpd.h,v 1.158 2021/10/24 16:01:04 ian Exp $ */ /* * Copyright (c) 2006 - 2015 Reyk Floeter @@ -48,6 +48,8 @@ #define HTTPD_USER "www" #define HTTPD_SERVERNAME "OpenBSD httpd" #define HTTPD_DOCROOT "/htdocs" +#define HTTPD_ERRDOCTEMPLATE "err" /* 3-char name */ +#define HTTPD_ERRDOCROOT_MAX (PATH_MAX - sizeof("000.html")) #define HTTPD_INDEX "index.html" #define HTTPD_FCGI_SOCKET "/run/slowcgi.sock" #define HTTPD_LOGROOT "/logs" @@ -378,6 +380,7 @@ SPLAY_HEAD(client_tree, client); #define SRVFLAG_NO_FCGI 0x00000080 #define SRVFLAG_LOG 0x00000100 #define SRVFLAG_NO_LOG 0x00000200 +#define SRVFLAG_ERRDOCS 0x00000400 #define SRVFLAG_SYSLOG 0x00000800 #define SRVFLAG_NO_SYSLOG 0x00001000 #define SRVFLAG_TLS 0x00002000 @@ -398,7 +401,7 @@ SPLAY_HEAD(client_tree, client); #define SRVFLAG_BITS \ "\10\01INDEX\02NO_INDEX\03AUTO_INDEX\04NO_AUTO_INDEX" \ - "\05ROOT\06LOCATION\07FCGI\10NO_FCGI\11LOG\12NO_LOG" \ + "\05ROOT\06LOCATION\07FCGI\10NO_FCGI\11LOG\12NO_LOG\13ERRDOCS" \ "\14SYSLOG\15NO_SYSLOG\16TLS\17ACCESS_LOG\20ERROR_LOG" \ "\21AUTH\22NO_AUTH\23BLOCK\24NO_BLOCK\25LOCATION_MATCH" \ "\26SERVER_MATCH\27SERVER_HSTS\30DEFAULT_TYPE\31PATH\32NO_PATH" \ @@ -542,6 +545,7 @@ struct server_config { struct server_fcgiparams fcgiparams; int fcgistrip; + char errdocroot[HTTPD_ERRDOCROOT_MAX]; TAILQ_ENTRY(server_config) entry; }; @@ -600,6 +604,9 @@ struct httpd { struct privsep *sc_ps; int sc_reload; + + int sc_custom_errdocs; + char sc_errdocroot[HTTPD_ERRDOCROOT_MAX]; }; #define HTTPD_OPT_VERBOSE 0x01 diff --git a/usr.sbin/httpd/parse.y b/usr.sbin/httpd/parse.y index 849f659f9fa..f2cf4b6c88c 100644 --- a/usr.sbin/httpd/parse.y +++ b/usr.sbin/httpd/parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: parse.y,v 1.126 2021/10/15 15:01:28 naddy Exp $ */ +/* $OpenBSD: parse.y,v 1.127 2021/10/24 16:01:04 ian Exp $ */ /* * Copyright (c) 2020 Matthias Pressfreund @@ -141,6 +141,7 @@ typedef struct { %token TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS DEFAULT PRELOAD REQUEST %token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS REWRITE %token CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT +%token ERRDOCS %token STRING %token NUMBER %type port @@ -212,6 +213,17 @@ main : PREFORK NUMBER { | CHROOT STRING { conf->sc_chroot = $2; } + | ERRDOCS STRING { + if ($2 != NULL && strlcpy(conf->sc_errdocroot, $2, + sizeof(conf->sc_errdocroot)) >= + sizeof(conf->sc_errdocroot)) { + yyerror("errdoc root path too long"); + free($2); + YYERROR; + } + free($2); + conf->sc_custom_errdocs = 1; + } | LOGDIR STRING { conf->sc_logdir = $2; } @@ -288,6 +300,12 @@ server : SERVER optmatch STRING { s->srv_conf.hsts_max_age = SERVER_HSTS_DEFAULT_AGE; + (void)strlcpy(s->srv_conf.errdocroot, + conf->sc_errdocroot, + sizeof(s->srv_conf.errdocroot)); + if (conf->sc_custom_errdocs) + s->srv_conf.flags |= SRVFLAG_ERRDOCS; + if (last_server_id == INT_MAX) { yyerror("too many servers defined"); free(s); @@ -477,6 +495,28 @@ serveroptsl : LISTEN ON STRING opttls port { TAILQ_INSERT_TAIL(&srv->srv_hosts, alias, entry); } + | ERRDOCS STRING { + if (parentsrv != NULL) { + yyerror("errdocs inside location"); + YYERROR; + } + if ($2 != NULL && strlcpy(srv->srv_conf.errdocroot, $2, + sizeof(srv->srv_conf.errdocroot)) >= + sizeof(srv->srv_conf.errdocroot)) { + yyerror("errdoc root path too long"); + free($2); + YYERROR; + } + free($2); + srv->srv_conf.flags |= SRVFLAG_ERRDOCS; + } + | NO ERRDOCS { + if (parentsrv != NULL) { + yyerror("errdocs inside location"); + YYERROR; + } + srv->srv_conf.flags &= ~SRVFLAG_ERRDOCS; + } | tcpip { if (parentsrv != NULL) { yyerror("tcp flags inside location"); @@ -1396,6 +1436,7 @@ lookup(char *s) { "directory", DIRECTORY }, { "drop", DROP }, { "ecdhe", ECDHE }, + { "errdocs", ERRDOCS }, { "error", ERR }, { "fastcgi", FCGI }, { "forwarded", FORWARDED }, diff --git a/usr.sbin/httpd/server_http.c b/usr.sbin/httpd/server_http.c index 696a03ddfaa..a2ec2bb663b 100644 --- a/usr.sbin/httpd/server_http.c +++ b/usr.sbin/httpd/server_http.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server_http.c,v 1.146 2021/10/23 15:30:28 benno Exp $ */ +/* $OpenBSD: server_http.c,v 1.147 2021/10/24 16:01:04 ian Exp $ */ /* * Copyright (c) 2020 Matthias Pressfreund @@ -49,9 +49,11 @@ static int server_httperror_cmp(const void *, const void *); void server_httpdesc_free(struct http_descriptor *); int server_http_authenticate(struct server_config *, struct client *); +int http_version_num(char *); char *server_expand_http(struct client *, const char *, char *, size_t); -int http_version_num(char *); +char *replace_var(char *, const char *, const char *); +char *read_errdoc(const char *, const char *); static struct http_method http_methods[] = HTTP_METHODS; static struct http_error http_errors[] = HTTP_ERRORS; @@ -885,7 +887,8 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) char *clenheader = NULL; char buf[IBUF_READ_SIZE]; char *escapedmsg = NULL; - int bodylen; + char cstr[5]; + ssize_t bodylen; if (code == 0) { server_close(clt, "dropped"); @@ -961,6 +964,23 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) free(escapedmsg); + if ((srv_conf->flags & SRVFLAG_ERRDOCS) == 0) + goto builtin; /* errdocs not enabled */ + if ((size_t)snprintf(cstr, sizeof(cstr), "%03u", code) >= sizeof(cstr)) + goto builtin; + + if ((body = read_errdoc(srv_conf->errdocroot, cstr)) == NULL && + (body = read_errdoc(srv_conf->errdocroot, HTTPD_ERRDOCTEMPLATE)) + == NULL) + goto builtin; + + body = replace_var(body, "$HTTP_ERROR", httperr); + body = replace_var(body, "$RESPONSE_CODE", cstr); + body = replace_var(body, "$SERVER_SOFTWARE", HTTPD_SERVERNAME); + bodylen = strlen(body); + goto send; + + builtin: /* A CSS stylesheet allows minimal customization by the user */ style = "body { background-color: white; color: black; font-family: " "'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', sans-serif; }\n" @@ -988,6 +1008,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) goto done; } + send: if (srv_conf->flags & SRVFLAG_SERVER_HSTS && srv_conf->flags & SRVFLAG_TLS) { if (asprintf(&hstsheader, "Strict-Transport-Security: " @@ -1005,7 +1026,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) clenheader = NULL; else { if (asprintf(&clenheader, - "Content-Length: %d\r\n", bodylen) == -1) { + "Content-Length: %zd\r\n", bodylen) == -1) { clenheader = NULL; goto done; } @@ -1729,6 +1750,64 @@ server_httperror_cmp(const void *a, const void *b) return (ea->error_code - eb->error_code); } +/* + * return -1 on failure, strlen() of read file otherwise. + * body is NULL on failure, contents of file with trailing \0 otherwise. + */ +char * +read_errdoc(const char *root, const char *file) +{ + struct stat sb; + char *path; + int fd; + char *ret = NULL; + + if (asprintf(&path, "%s/%s.html", root, file) == -1) + fatal("asprintf"); + if ((fd = open(path, O_RDONLY)) == -1) { + free(path); + log_warn("%s: open", __func__); + return (NULL); + } + free(path); + if (fstat(fd, &sb) < 0) { + log_warn("%s: stat", __func__); + return (NULL); + } + + if ((ret = calloc(1, sb.st_size + 1)) == NULL) + fatal("calloc"); + if (sb.st_size == 0) + return (ret); + if (read(fd, ret, sb.st_size) != sb.st_size) { + log_warn("%s: read", __func__); + close(fd); + free(ret); + ret = NULL; + return (ret); + } + close(fd); + + return (ret); +} + +char * +replace_var(char *str, const char *var, const char *repl) +{ + char *iv, *r; + size_t vlen; + + vlen = strlen(var); + while ((iv = strstr(str, var)) != NULL) { + *iv = '\0'; + if (asprintf(&r, "%s%s%s", str, repl, &iv[vlen]) == -1) + fatal("asprintf"); + free(str); + str = r; + } + return (str); +} + int server_log_http(struct client *clt, unsigned int code, size_t len) { -- 2.20.1