From: Rafael Sadowski Subject: Re: httpd: add custom HTTP header support #2 To: tech@openbsd.org, kirill@openbsd.org Date: Mon, 20 Jul 2026 08:44:39 +0200 On Thu Jul 16, 2026 at 09:46:47AM +0200, Claudio Jeker wrote: > On Thu, Jul 16, 2026 at 09:36:47AM +0200, Rafael Sadowski wrote: > > On Thu Jul 16, 2026 at 08:50:21AM +0200, Claudio Jeker wrote: > > > On Thu, Jul 16, 2026 at 07:42:36AM +0200, Rafael Sadowski wrote: > > > > On Wed Jul 15, 2026 at 08:56:27AM +0200, Claudio Jeker wrote: > > > > > On Wed, Jul 15, 2026 at 08:08:51AM +0200, Rafael Sadowski wrote: > > > > > > On Mon Jul 13, 2026 at 02:16:58AM +0200, Kirill A. Korinsky wrote: > > > > > > > On Sat, 11 Jul 2026 16:12:46 +0200, > > > > > > > Rafael Sadowski wrote: > > > > > > > > > > > > > > > > I think this is a solid first version for that feature. We'll certainly > > > > > > > > find out about more edge cases through user feedback ... or maybe not ;) > > > > > > > > > > > > > > > > > > > > > > Here first pass for review. I haven't spent much time on this. I may be wrong. > > > > > > > > > > > > > > > + TAILQ_FOREACH(hdr, &parent_srv->srv_conf.headers, entry) { > > > > > > > > + if (header_exists(srv_conf, hdr->name)) { > > > > > > > > + DPRINTF("%s: skipping header \"%s\" from parent " > > > > > > > > + "\"%s\", overridden in location \"%s\"", > > > > > > > > + __func__, hdr->name, > > > > > > > > + parent_srv->srv_conf.name, srv_conf->location); > > > > > > > > + continue; > > > > > > > > + } > > > > > > > > > > > > > > What happens with duplicated headers? Multiple Set-Cookie is good example. > > > > > > > > > > > > Good catch! > > > > > > > > > > > > Fixed by collecting the inherited headers in a temporary list and > > > > > > TAILQ_CONCAT()ing them afterwards, so config_header_exists() only ever sees > > > > > > headers defined in the location itself. > > > > > > > > > > > > The semantics are unchanged otherwise: if a location mentions a header name at > > > > > > all, it takes over that name completely and none of the parent's headers > > > > > > with that name are inherited. So a "header add "Set-Cookie"" in a > > > > > > location replaces the whole parent group rather than adding to it. The > > > > > > users can use remove if they want to drop individual inherited headers. > > > > > > Documented in httpd.conf(5). > > > > > > > > > > > > > > > > > > > > > @@ -1050,6 +1063,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > > > > > > > > free(hstsheader); > > > > > > > > free(clenheader); > > > > > > > > free(bannerheader); > > > > > > > > + free(customheaders); > > > > > > > > > > > > > > I think this code is reached only on error, and it makes customheaders leaks > > > > > > > on sucess. > > > > > > > > > > > > > > And next things which I not yet sure. I think that roughly 25 directives > > > > > > > exceed the 16K imsg limit because every one is fixed 664 byte structure nad > > > > > > > it is packed into one message. > > > > > > > > > > > > So true! That's what happens when you follow the code from fastcgi. > > > > > > FastCGI has the same problem. It's actually even worse: > > > > > > > > > > With imsg_set_maxsize(3) you can alter the limit. e.g. bgpd uses a 128k > > > > > imsg size. You can go quite large but keep in mind that the full message > > > > > needs to a) fit in memory and b) needs to be transferred before other > > > > > messages can be processed (the imsgq is a fifo). > > > > > > > > > > > #define HTTPD_FCGI_NAME_MAX 511 > > > > > > #define HTTPD_FCGI_VAL_MAX 511 > > > > > > > > > > > > I fixed the problem by sending the headers one after the other. > > > > > > > > > > Isn't 511 bytes for a header a bit short? e.g. some cookies and JWT tokens > > > > > can be rather large. > > > > > > > > Yes that's true. I upper the limits for header to 256/8192 and not to > > > > touch the IMSG limit for the time being. > > > > > > > > > > > > > > The imsg passing in config_setserver_headers() sends the tailq pointers > > > > > which breaks the fork/exec ASLR protection. I would suggest to use encode > > > > > the headers differently and not use the struct for passing. With that also > > > > > the fixed size string buffers can be changed to a length + str value > > > > > encoding. > > > > > > > > > > You can probably shoehorn this into proc_composev, on the receive side > > > > > just use imsg_get_buf() and imsg_get_strbuf() to pull the data out. > > > > > > > > > > > > > Ohh! Thanks, fixed like this: > > > > > > > > config_setserver_headers() now sends a small fixed struct header_imsg > > > > (id, flags, and the name/value lengths) followed by the raw name and > > > > value bytes via proc_composev() > > > > > > > > As you suggested, the length encoding also let me drop the fixed > > > > name[]/value[] buffers in custom_header. > > > > > > > > If you only interested in review this part, you can find it here > > > > https://rsadowski.gothub.org/?action=diff&commit=daa3667f40735c225a6ba94ebd6bb7bd90a115bc&headref=custom-http-header-support&path=httpd.git > > > > > > > > If we can agree on that, I can and will implement ("fix") it for FastCGI > > > > as well. > > > > > > > > Here is the full diff and another iteration with the following changes: > > > > > > > > httpd: apply "add ... always" headers to internal error responses > > > > > > > > Bring back get_always_custom_headers() to append "add ... always" > > > > headers to those responses. Only "add" is handled here. "set" and > > > > "remove" are ignored for now, so "set ... always" is dropped from the > > > > grammar. Framing headers are already rejected at parse time, so a raw > > > > append cannot desync the response, only produce a duplicate header. > > > > > > > > This is a stopgap. A later change should fold this into > > > > server_custom_headers() so all header operations work the same way on > > > > every response. > > > > > > > > That should be "everything", > > > > > > Looks good to me. Two minor comments. > > > > > > - why is always only supported for add? > > > Why can't I remove or set headers in error conditions? > > > > Because the error/abort path doesn't build its response through the > > kvtree like normal responses do. It formats the headers as a raw string. > > In that path "add" is the only operation that can be applied "safely". > > "set" and "remove" would need to modify the built-in headers. > > > > Rather than half-implementing set/remove on a second code path, I > > limited "always" to "add" for now and marked it XXX. The plan is to > > convert server_abort_http() to build its headers via the kvtree too, > > then all three operations work the same on every response and the > > restriction goes away. > > Why can't we use the normal path here? Sure if httpd is out of memory > kvtree ops will fail but so will most bufferevent calls etc. > One could still fall back to static headers then... > > Anyway, you answered my question and the reason is the usual httpd horror. I've already sent this diff to kirill@, and here is the rebase following the latest commits. This diff fix small typos/minor issues and impl. the error path: commit 0da57a19c29af6ea328366dec5f7c699ce763ce8 Author: Rafael Sadowski Date: Thu Jul 16 15:53:03 2026 +0200 add always option for header set and remove This adds support for the "always" keyword in header set and remove directives. refactor the internal header generation to use a kvtree so that custom headers are correctly included in the error/abort path. That should cover everything. And now I'm looking for an OK. diff --git a/config.c b/config.c index 552199f..54d5dd8 100644 --- a/config.c +++ b/config.c @@ -197,6 +197,7 @@ clear_config_server_ptrs(struct server_config *cfg) /* clear TAILQ_HEAD */ memset(&cfg->fcgiparams, 0, sizeof(cfg->fcgiparams)); + memset(&cfg->headers, 0, sizeof(cfg->headers)); /* clear TAILQ_ENTRY */ memset(&cfg->entry, 0, sizeof(cfg->entry)); @@ -289,6 +290,11 @@ config_setserver(struct httpd *env, struct server *srv) if (config_setserver_fcgiparams(env, srv) != 0) return (-1); + /* Configure custom headers if necessary. */ + config_inherit_headers(env, srv); + if (config_setserver_headers(env, srv) == -1) + return (-1); + /* Close server socket early to prevent fd exhaustion in the parent. */ if (srv->srv_s != -1) { close(srv->srv_s); @@ -436,6 +442,129 @@ config_setserver_fcgiparams(struct httpd *env, struct server *srv) return (0); } +int +config_getserver_headers(struct httpd *env, struct imsg *imsg) +{ + struct server_config *srv_conf; + struct custom_header *hdr; + struct header_imsg hmsg; + struct ibuf ibuf; + + if (imsg_get_ibuf(imsg, &ibuf) == -1 || + ibuf_get(&ibuf, &hmsg, sizeof(hmsg)) == -1) { + log_debug("%s: invalid message", __func__); + return (-1); + } + + if ((srv_conf = serverconfig_byid(hmsg.id)) == NULL) { + log_debug("%s: invalid config id", __func__); + return (-1); + } + + if (hmsg.namelen > HTTPD_HEADER_NAME_MAX - 1 || + hmsg.vallen > HTTPD_HEADER_VAL_MAX - 1) { + log_debug("%s: header too long", __func__); + return (-1); + } + + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) + fatal("headers out of memory"); + + hdr->name = ibuf_get_string(&ibuf, hmsg.namelen); + hdr->value = ibuf_get_string(&ibuf, hmsg.vallen); + if (hdr->name == NULL || hdr->value == NULL) { + free(hdr->name); + free(hdr->value); + free(hdr); + return (-1); + } + hdr->flags = hmsg.flags; + + TAILQ_INSERT_TAIL(&srv_conf->headers, hdr, entry); + print_custom_header(__func__, hdr); + return (0); +} + +/* + * Inherit headers from parent server, skipping those + * already defined in the location. + */ +void +config_inherit_headers(struct httpd *env, struct server *srv) +{ + struct server *parent_srv; + struct server_config *srv_conf = &srv->srv_conf; + struct custom_header *hdr, *nhdr; + struct server_headers inherited; + + if (!(srv_conf->flags & SRVFLAG_LOCATION)) + return; + + /* Find parent server by parent_id */ + TAILQ_FOREACH(parent_srv, env->sc_servers, srv_entry) { + if (parent_srv->srv_conf.id == srv_conf->parent_id) + break; + } + + if (parent_srv == NULL) + return; + + TAILQ_INIT(&inherited); + + TAILQ_FOREACH(hdr, &parent_srv->srv_conf.headers, entry) { + if (header_exists(srv_conf, hdr->name)) { + DPRINTF("%s: skipping header \"%s\" from parent " + "\"%s\", overridden in location \"%s\"", + __func__, hdr->name, + parent_srv->srv_conf.name, srv_conf->location); + continue; + } + nhdr = header_dup(hdr); + TAILQ_INSERT_TAIL(&inherited, nhdr, entry); + DPRINTF("%s: inheriting header \"%s\" from parent \"%s\" " + "to location \"%s\"", __func__, hdr->name, + parent_srv->srv_conf.name, srv_conf->location); + } + + TAILQ_CONCAT(&srv_conf->headers, &inherited, entry); +} + +int +config_setserver_headers(struct httpd *env, struct server *srv) +{ + struct privsep *ps = env->sc_ps; + struct server_config *srv_conf = &srv->srv_conf; + struct custom_header *hdr; + struct header_imsg hmsg; + struct iovec iov[3]; + + DPRINTF("%s: sending headers for \"%s[%u]\" to %s fd %d", __func__, + srv_conf->name, srv_conf->id, ps->ps_title[PROC_SERVER], + srv->srv_s); + + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { + hmsg.id = srv_conf->id; + hmsg.flags = hdr->flags; + hmsg.namelen = strlen(hdr->name); + hmsg.vallen = strlen(hdr->value); + + iov[0].iov_base = &hmsg; + iov[0].iov_len = sizeof(hmsg); + iov[1].iov_base = hdr->name; + iov[1].iov_len = hmsg.namelen; + iov[2].iov_base = hdr->value; + iov[2].iov_len = hmsg.vallen; + + if (proc_composev(ps, PROC_SERVER, IMSG_CFG_HEADERS, + iov, 3) != 0) { + log_warn("%s: failed to compose IMSG_CFG_HEADERS " + "for `%s'", __func__, srv_conf->name); + return (-1); + } + } + return (0); +} + int config_setserver_tls(struct httpd *env, struct server *srv) { @@ -720,6 +849,8 @@ config_getserver(struct httpd *env, struct imsg *imsg) memcpy(&srv->srv_conf, &srv_conf, sizeof(srv->srv_conf)); srv->srv_s = fd; + TAILQ_INIT(&srv->srv_conf.headers); + if (config_getserver_auth(env, &srv->srv_conf) != 0) goto fail; diff --git a/httpd.c b/httpd.c index fd98a7b..35d5b77 100644 --- a/httpd.c +++ b/httpd.c @@ -922,7 +922,7 @@ accept_reserve(int sockfd, struct sockaddr *addr, socklen_t *addrlen, } struct kv * -kv_add(struct kvtree *keys, char *key, char *value) +kv_add(struct kvtree *keys, const char *key, const char *value) { struct kv *kv, *oldkv; @@ -1239,3 +1239,44 @@ getmonotime(struct timeval *tv) TIMESPEC_TO_TIMEVAL(tv, &ts); } + +void +print_custom_header(const char *i, const struct custom_header *hdr) +{ + if (hdr == NULL) { + DPRINTF("%s: hdr=NULL", i); + return; + } + DPRINTF("%s: hdr (%s%s%s%s) %s: %s", i, + (hdr->flags & HEADER_REMOVE) ? "remove " : "", + (hdr->flags & HEADER_ADD) ? "add " : "", + (hdr->flags & HEADER_SET) ? "set " : "", + (hdr->flags & HEADER_ALWAYS) ? "always" : "", + hdr->name, hdr->value); +} + +int +header_exists(struct server_config *srv_conf, const char *name) +{ + struct custom_header *hdr; + + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { + if (strcasecmp(hdr->name, name) == 0) + return (1); + } + return (0); +} + +struct custom_header * +header_dup(const struct custom_header *src) +{ + struct custom_header *h; + + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal("out of memory"); + if ((h->name = strdup(src->name)) == NULL || + (h->value = strdup(src->value)) == NULL) + fatal("out of memory"); + h->flags = src->flags; + return (h); +} diff --git a/httpd.conf.5 b/httpd.conf.5 index caf55a4..6a62dcb 100644 --- a/httpd.conf.5 +++ b/httpd.conf.5 @@ -485,6 +485,56 @@ Enable static gzip compression to save bandwidth. If gzip encoding is accepted and if the requested file exists with an additional .gz suffix, use the compressed file instead and deliver it with content encoding gzip. +.It Ic header Ar option +Manipulate HTTP response headers. +Multiple +.Ic header +statements may be specified. +Valid options are: +.Bl -tag -width Ds +.It Ic set Ar name Ar value Op Ic always +Set a custom HTTP response header with the specified +.Ar name +and +.Ar value . +If a header with the same +.Ar name +is already present in the response, its value will be replaced. +The header is added to successful responses +(2xx and 3xx status codes) by default. +.It Ic add Ar name Ar value Op Ic always +Add a custom HTTP response header with the specified +.Ar name +and +.Ar value . +Unlike +.Ic set , +this option appends the header even if one with the same +.Ar name +already exists, allowing for multiple headers with the same name. +The header is added to successful responses +(2xx and 3xx status codes) by default. +.It Ic remove Ar name Op Ic always +Suppress the HTTP response header with the specified +.Ar name . +This can be used to remove headers added by default, such as +.Qq Last-Modified , +as well as headers inherited from a parent server configuration. +.El +.Pp +If +.Ic always +is specified, the header is also added to error responses (4xx and +5xx). +.Pp +Header names are limited to 256 characters and values to 8192 characters. +Headers defined in a +.Ic location +block are inherited from the +.Ic server +context and override defined headers with the same name. +If you do not wish to inherit these, you can remove them again with +.Ic remove . .It Ic hsts Oo Ar option Oc Enable HTTP Strict Transport Security. Valid options are: @@ -917,6 +967,32 @@ server "example.com" { } } .Ed +.Pp +Custom HTTP headers can be added to responses for security or compatibility: +.Bd -literal -offset indent +server "example.com" { + listen on * tls port 443 + + header set "X-Powered-By" "OpenBSD httpd" + + header set "X-Content-Type-Options" "nosniff" always + header set "X-Frame-Options" "DENY" always + + header add "Set-Cookie" "id=23; HttpOnly; Secure" + + # Remove default Last-Modified header + header remove "Last-Modified" + + location "/api/*" { + # This location defines its own headers + header add "Access-Control-Allow-Origin" "*" + # Override from server context + header set "Set-Cookie" "id=5; HttpOnly; Secure" + # Remove from server context + header remove "X-Frame-Options" + } +} +.Ed .Sh SEE ALSO .Xr htpasswd 1 , .Xr glob 7 , diff --git a/httpd.h b/httpd.h index 46c60d6..8054536 100644 --- a/httpd.h +++ b/httpd.h @@ -67,6 +67,8 @@ #define HTTPD_TLS_ECDHE_CURVES "default" #define HTTPD_FCGI_NAME_MAX 511 #define HTTPD_FCGI_VAL_MAX 511 +#define HTTPD_HEADER_NAME_MAX 256 +#define HTTPD_HEADER_VAL_MAX 8192 #define FD_RESERVE 5 #define SERVER_MAX_CLIENTS 1024 @@ -187,6 +189,7 @@ enum imsg_type { IMSG_CFG_MEDIA, IMSG_CFG_AUTH, IMSG_CFG_FCGI, + IMSG_CFG_HEADERS, IMSG_CFG_DONE, IMSG_LOG_ACCESS, IMSG_LOG_ERROR, @@ -410,6 +413,18 @@ enum log_format { LOG_FORMAT_FORWARDED }; +#define HEADER_REMOVE 0x01 +#define HEADER_ADD 0x02 +#define HEADER_SET 0x04 +#define HEADER_ALWAYS 0x08 + +struct header_imsg { + uint32_t id; /* server conf id */ + uint32_t flags; + uint16_t namelen; + uint16_t vallen; +}; + struct log_file { char log_name[PATH_MAX]; int log_fd; @@ -448,6 +463,15 @@ struct fastcgi_param { }; TAILQ_HEAD(server_fcgiparams, fastcgi_param); +struct custom_header { + char *name; + char *value; + uint32_t flags; + + TAILQ_ENTRY(custom_header) entry; +}; +TAILQ_HEAD(server_headers, custom_header); + struct server_config { uint32_t id; uint32_t parent_id; @@ -519,6 +543,7 @@ struct server_config { struct server_fcgiparams fcgiparams; int fcgistrip; int fcgiallowchunked; + struct server_headers headers; char errdocroot[HTTPD_ERRDOCROOT_MAX]; TAILQ_ENTRY(server_config) entry; @@ -642,6 +667,8 @@ void server_http(void); int server_httpdesc_init(struct client *); void server_read_http(struct bufferevent *, void *); void server_abort_http(struct client *, unsigned int, const char *); +int server_custom_headers(struct server_config *, struct kvtree *, + unsigned int); unsigned int server_httpmethod_byname(const char *); const char @@ -698,7 +725,7 @@ struct in6_addr *prefixlen2mask6(uint8_t, uint32_t *); uint32_t prefixlen2mask(uint8_t); int accept_reserve(int, struct sockaddr *, socklen_t *, int, volatile int *); -struct kv *kv_add(struct kvtree *, char *, char *); +struct kv *kv_add(struct kvtree *, const char *, const char *); int kv_set(struct kv *, char *, ...) __attribute__((__format__ (printf, 2, 3))); int kv_setkey(struct kv *, char *, ...) @@ -727,6 +754,12 @@ const char *print_host(struct sockaddr_storage *, char *, size_t); const char *printb_flags(const uint64_t, const char *); void getmonotime(struct timeval *); +void print_custom_header(const char *, + const struct custom_header *); +int header_exists(struct server_config *, const char *); +struct custom_header + *header_dup(const struct custom_header *); + extern struct httpd *httpd_env; /* proc.c */ @@ -771,9 +804,12 @@ int config_getcfg(struct httpd *, struct imsg *); int config_setserver(struct httpd *, struct server *); int config_setserver_tls(struct httpd *, struct server *); int config_setserver_fcgiparams(struct httpd *, struct server *); +int config_setserver_headers(struct httpd *, struct server *); +void config_inherit_headers(struct httpd *, struct server *); int config_getserver(struct httpd *, struct imsg *); int config_getserver_tls(struct httpd *, struct imsg *); int config_getserver_fcgiparams(struct httpd *, struct imsg *); +int config_getserver_headers(struct httpd *, struct imsg *); int config_setmedia(struct httpd *, struct media_type *); int config_getmedia(struct httpd *, struct imsg *); int config_setauth(struct httpd *, struct auth *); diff --git a/parse.y b/parse.y index 65664e1..4b967ca 100644 --- a/parse.y +++ b/parse.y @@ -121,6 +121,7 @@ int getservice(char *); int is_if_in_group(const char *, const char *); int get_fastcgi_dest(struct server_config *, const char *, char *); void remove_locations(struct server_config *); +int header_name_forbidden(const char *); typedef struct { union { @@ -135,14 +136,14 @@ typedef struct { %} -%token ACCESS ALIAS AUTHENTICATE AUTO +%token ACCESS ADD ALIAS ALWAYS AUTHENTICATE AUTO %token BACKLOG BANNER BLOCK BODY BUFFER %token CA CERTIFICATE CHROOT CIPHERS CLIENT COMBINED COMMON CONNECTION CRL %token DEFAULT DHE DIRECTORY DROP %token ECDHE ERR ERRDOCS ERROR %token FCGI FORWARDED FOUND %token GZIPSTATIC -%token HSTS +%token HEADER HSTS %token INCLUDE INDEX IP %token KEY %token LIFETIME LISTEN LOCATION LOG LOGDIR @@ -150,15 +151,16 @@ typedef struct { %token NO NODELAY NOT %token OCSP ON OPTIONAL %token PARAM PASS PORT PREFORK PRELOAD PROTOCOLS -%token REQUEST REQUESTS RETURN REWRITE ROOT -%token SACK SERVER SOCKET STATIC_CACHE_CONTROL STRIP STYLE SUBDOMAINS SYSLOG +%token REMOVE REQUEST REQUESTS RETURN REWRITE ROOT +%token SACK SERVER SET SOCKET STATIC_CACHE_CONTROL STRIP STYLE SUBDOMAINS +%token SYSLOG %token TCP TICKET TIMEOUT TLS TYPE TYPES %token WITH %token STRING %token NUMBER %type port %type fcgiport -%type opttls optmatch optfound +%type optalways opttls optmatch optfound %type timeout %type numberstring optstring %type authopts @@ -336,6 +338,7 @@ server : SERVER optmatch STRING { SPLAY_INIT(&srv->srv_clients); TAILQ_INIT(&srv->srv_hosts); TAILQ_INIT(&srv_conf->fcgiparams); + TAILQ_INIT(&srv_conf->headers); TAILQ_INSERT_TAIL(&srv->srv_hosts, srv_conf, entry); } '{' optnl serveropts_l '}' { @@ -570,6 +573,7 @@ serveroptsl : LISTEN ON STRING opttls port { | root | directory | banner + | header | static_cache_control | logformat | fastcgi @@ -657,6 +661,7 @@ serveroptsl : LISTEN ON STRING opttls port { srv = s; srv_conf = &srv->srv_conf; SPLAY_INIT(&srv->srv_clients); + TAILQ_INIT(&srv_conf->headers); } '{' optnl serveropts_l '}' { struct server *s = NULL; uint64_t f; @@ -724,6 +729,117 @@ banner : BANNER { } ; +optalways : + /* empty */ { $$ = 0; } + | ALWAYS { $$ = 1; } + ; + +header : HEADER REMOVE STRING optalways { + struct custom_header *hdr; + + if (strlen($3) > HTTPD_HEADER_NAME_MAX - 1) { + yyerror("header name too long (max %d)", + HTTPD_HEADER_NAME_MAX - 1); + free($3); + YYERROR; + } + + if (header_name_forbidden($3)) { + free($3); + YYERROR; + } + + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) + fatal("out of memory"); + + if ((hdr->name = strdup($3)) == NULL || + (hdr->value = strdup("")) == NULL) /* never NULL */ + fatal("out of memory"); + free($3); + + hdr->flags = HEADER_REMOVE; + if ($4) + hdr->flags |= HEADER_ALWAYS; + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); + } + | HEADER ADD STRING STRING optalways { + struct custom_header *hdr; + + if (strlen($3) > HTTPD_HEADER_NAME_MAX - 1) { + yyerror("header name too long (max %d)", + HTTPD_HEADER_NAME_MAX - 1); + free($3); + free($4); + YYERROR; + } + if (header_name_forbidden($3)) { + free($3); + free($4); + YYERROR; + } + if (strlen($4) > HTTPD_HEADER_VAL_MAX - 1) { + yyerror("header value too long (max %d)", + HTTPD_HEADER_VAL_MAX - 1); + free($3); + free($4); + YYERROR; + } + + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) + fatal("out of memory"); + + if ((hdr->name = strdup($3)) == NULL || + (hdr->value = strdup($4)) == NULL) + fatal("out of memory"); + + free($3); + free($4); + + hdr->flags = HEADER_ADD; + if ($5) + hdr->flags |= HEADER_ALWAYS; + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); + } + | HEADER SET STRING STRING optalways { + struct custom_header *hdr; + + if (strlen($3) > HTTPD_HEADER_NAME_MAX - 1) { + yyerror("header name too long (max %d)", + HTTPD_HEADER_NAME_MAX - 1); + free($3); + free($4); + YYERROR; + } + if (header_name_forbidden($3)) { + free($3); + free($4); + YYERROR; + } + if (strlen($4) > HTTPD_HEADER_VAL_MAX - 1) { + yyerror("header value too long (max %d)", + HTTPD_HEADER_VAL_MAX - 1); + free($3); + free($4); + YYERROR; + } + + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) + fatal("out of memory"); + + if ((hdr->name = strdup($3)) == NULL || + (hdr->value = strdup($4)) == NULL) + fatal("out of memory"); + + free($3); + free($4); + + hdr->flags = HEADER_SET; + if ($5) + hdr->flags |= HEADER_ALWAYS; + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); + } + ; + optfound : /* empty */ { $$ = 0; } | FOUND { $$ = 1; } | NOT FOUND { $$ = -1; } @@ -1483,7 +1599,9 @@ lookup(char *s) /* this has to be sorted always */ static const struct keywords keywords[] = { { "access", ACCESS }, + { "add", ADD }, { "alias", ALIAS }, + { "always", ALWAYS }, { "authenticate", AUTHENTICATE}, { "auto", AUTO }, { "backlog", BACKLOG }, @@ -1511,6 +1629,7 @@ lookup(char *s) { "forwarded", FORWARDED }, { "found", FOUND }, { "gzip-static", GZIPSTATIC }, + { "header", HEADER }, { "hsts", HSTS }, { "include", INCLUDE }, { "index", INDEX }, @@ -1536,6 +1655,7 @@ lookup(char *s) { "prefork", PREFORK }, { "preload", PRELOAD }, { "protocols", PROTOCOLS }, + { "remove", REMOVE }, { "request", REQUEST }, { "requests", REQUESTS }, { "return", RETURN }, @@ -1543,6 +1663,7 @@ lookup(char *s) { "root", ROOT }, { "sack", SACK }, { "server", SERVER }, + { "set", SET }, { "socket", SOCKET }, { "static-cache-control", STATIC_CACHE_CONTROL }, { "strip", STRIP }, @@ -2330,17 +2451,44 @@ host(const char *s, struct addresslist *al, int max, return (host_dns(s, al, max, port, ifname, ipproto)); } +int +header_name_forbidden(const char *name) +{ + if (strcasecmp(name, "Content-Length") == 0 || + strcasecmp(name, "Transfer-Encoding") == 0 || + strcasecmp(name, "Connection") == 0 || + strcasecmp(name, "Date") == 0) { + yyerror("header \"%s\" is reserved and cannot be used", name); + return (1); + } + + if (strcasecmp(name, "Server") == 0) { + yyerror("header \"Server\" cannot be configured here, " + "use 'no banner' instead"); + return (1); + } + return (0); +} + struct server * server_inherit(struct server *src, struct server_config *alias, struct server_config *addr) { struct server *dst, *s, *dstl; + struct custom_header *hdr, *nhdr; if ((dst = calloc(1, sizeof(*dst))) == NULL) fatal("out of memory"); /* Copy the source server and assign a new Id */ memcpy(&dst->srv_conf, &src->srv_conf, sizeof(dst->srv_conf)); + + TAILQ_INIT(&dst->srv_conf.headers); + TAILQ_FOREACH(hdr, &src->srv_conf.headers, entry) { + nhdr = header_dup(hdr); + TAILQ_INSERT_TAIL(&dst->srv_conf.headers, nhdr, entry); + } + if ((dst->srv_conf.tls_cert_file = strdup(src->srv_conf.tls_cert_file)) == NULL) fatal("out of memory"); @@ -2430,6 +2578,14 @@ server_inherit(struct server *src, struct server_config *alias, fatal("out of memory"); memcpy(&dstl->srv_conf, &s->srv_conf, sizeof(dstl->srv_conf)); + + /* Copy custom headers from source location */ + TAILQ_INIT(&dstl->srv_conf.headers); + TAILQ_FOREACH(hdr, &s->srv_conf.headers, entry) { + nhdr = header_dup(hdr); + TAILQ_INSERT_TAIL(&dstl->srv_conf.headers, nhdr, entry); + } + strlcpy(dstl->srv_conf.name, alias->name, sizeof(dstl->srv_conf.name)); diff --git a/server.c b/server.c index e69c9b9..74d6410 100644 --- a/server.c +++ b/server.c @@ -472,6 +472,7 @@ void serverconfig_free(struct server_config *srv_conf) { struct fastcgi_param *param, *tparam; + struct custom_header *hdr, *thdr; free(srv_conf->return_uri); free(srv_conf->tls_ca_file); @@ -487,6 +488,12 @@ serverconfig_free(struct server_config *srv_conf) TAILQ_FOREACH_SAFE(param, &srv_conf->fcgiparams, entry, tparam) free(param); + + TAILQ_FOREACH_SAFE(hdr, &srv_conf->headers, entry, thdr) { + free(hdr->name); + free(hdr->value); + free(hdr); + } } void @@ -505,6 +512,7 @@ serverconfig_reset(struct server_config *srv_conf) srv_conf->tls_ocsp_staple = NULL; srv_conf->tls_ocsp_staple_file = NULL; TAILQ_INIT(&srv_conf->fcgiparams); + TAILQ_INIT(&srv_conf->headers); } struct server * @@ -1363,6 +1371,10 @@ server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg) if (config_getserver_fcgiparams(httpd_env, imsg) != 0) return (-1); break; + case IMSG_CFG_HEADERS: + if (config_getserver_headers(httpd_env, imsg) != 0) + return (-1); + break; case IMSG_CFG_DONE: if (config_getcfg(httpd_env, imsg) != 0) return (-1); diff --git a/server_fcgi.c b/server_fcgi.c index 15e5ee8..0daf55a 100644 --- a/server_fcgi.c +++ b/server_fcgi.c @@ -724,6 +724,9 @@ server_fcgi_header(struct client *clt, unsigned int code) return (-1); } + if (server_custom_headers(srv_conf, &resp->http_headers, code) == -1) + return (-1); + /* Date header is mandatory and should be added as late as possible */ key.kv_key = "Date"; if (kv_find(&resp->http_headers, &key) == NULL && diff --git a/server_http.c b/server_http.c index bf4301b..1c82ad1 100644 --- a/server_http.c +++ b/server_http.c @@ -51,6 +51,7 @@ void server_httpdesc_free(struct http_descriptor *); int server_http_authenticate(struct server_config *, struct client *); static int http_version_num(char *); +static int http_is_success(unsigned int code); char *server_expand_http(struct client *, const char *, char *, size_t); char *replace_var(char *, const char *, const char *); @@ -59,6 +60,7 @@ ssize_t server_create_builtin(struct server_config *, char **, unsigned int, const char *); char *server_create_errdoc(struct server_config *, unsigned int, const char *); +static char *get_custom_headers(struct kvtree *); static struct http_method http_methods[] = HTTP_METHODS; static struct http_error http_errors[] = HTTP_ERRORS; @@ -220,6 +222,12 @@ http_version_num(char *version) return (0); } +static int +http_is_success(unsigned int code) +{ + return (code >= 200 && code < 400); +} + void server_read_http(struct bufferevent *bev, void *arg) { @@ -885,14 +893,19 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) struct bufferevent *bev = clt->clt_bev; struct http_descriptor *desc = clt->clt_descreq; const char *httperr = NULL; - char *httpmsg, *body = NULL, *extraheader = NULL; + char *httpmsg, *body = NULL; char tmbuf[32], hbuf[128], *hstsheader = NULL; - char *clenheader = NULL; - char *bannerheader = NULL; + char *headers = NULL; char buf[IBUF_READ_SIZE]; char *escapedmsg = NULL; + char *basicrealm = NULL; + char hbodylen[32]; ssize_t bodylen; ssize_t httpmsglen; + struct kvtree http_headers; + int has_body, ret; + + RB_INIT(&http_headers); if (code == 0) { server_close(clt, "dropped"); @@ -912,9 +925,6 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) if (print_host(&srv_conf->ss, hbuf, sizeof(hbuf)) == NULL) goto done; - if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0) - goto done; - /* Do not send details of the Internal Server Error */ switch (code) { case 301: @@ -927,9 +937,8 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) memset(buf, 0, sizeof(buf)); if (server_expand_http(clt, msg, buf, sizeof(buf)) == NULL) goto done; - if (asprintf(&extraheader, "Location: %s\r\n", buf) == -1) { + if (kv_add(&http_headers, "Location", buf) == NULL) { code = 500; - extraheader = NULL; } msg = buf; break; @@ -938,23 +947,24 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) break; if (stravis(&escapedmsg, msg, VIS_DQ) == -1) { code = 500; - extraheader = NULL; - } else if (asprintf(&extraheader, - "WWW-Authenticate: Basic realm=\"%s\"\r\n", escapedmsg) - == -1) { + } else if (asprintf(&basicrealm, "Basic realm=\"%s\"", + escapedmsg) == -1) { + basicrealm = NULL; + code = 500; + } else if (kv_add(&http_headers, "WWW-Authenticate", + basicrealm) == NULL) { code = 500; - extraheader = NULL; } free(escapedmsg); + escapedmsg = NULL; + free(basicrealm); + basicrealm = NULL; break; case 416: if (msg == NULL) break; - if (asprintf(&extraheader, - "Content-Range: %s\r\n", msg) == -1) { + if (kv_add(&http_headers, "Content-Range", msg) == NULL) code = 500; - extraheader = NULL; - } break; default: /* @@ -975,8 +985,8 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) if (srv_conf->flags & SRVFLAG_SERVER_HSTS && srv_conf->flags & SRVFLAG_TLS) { - if (asprintf(&hstsheader, "Strict-Transport-Security: " - "max-age=%d%s%s\r\n", srv_conf->hsts_max_age, + if (asprintf(&hstsheader, "max-age=%d%s%s", + srv_conf->hsts_max_age, srv_conf->hsts_flags & HSTSFLAG_SUBDOMAINS ? "; includeSubDomains" : "", srv_conf->hsts_flags & HSTSFLAG_PRELOAD ? @@ -984,45 +994,52 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) hstsheader = NULL; goto done; } + + ret = kv_add(&http_headers, "Strict-Transport-Security", + hstsheader) == NULL; + free(hstsheader); + hstsheader = NULL; + if (ret) + goto done; } + has_body = !((code >= 100 && code < 200) || code == 204); + if (has_body) { + ret = snprintf(hbodylen, sizeof(hbodylen), "%zd", bodylen); + if (ret < 0 || (size_t)ret >= sizeof(hbodylen)) + goto done; - if ((code >= 100 && code < 200) || code == 204) - clenheader = NULL; - else { - if (asprintf(&clenheader, - "Content-Length: %zd\r\n", bodylen) == -1) { - clenheader = NULL; + if (kv_add(&http_headers, "Content-Length", hbodylen) == NULL) goto done; - } } /* If banner is suppressed, don't write a Server header */ if ((srv_conf->flags & SRVFLAG_NO_BANNER) == 0) - if (asprintf(&bannerheader, "Server: %s\r\n", - HTTPD_SERVERNAME) == -1) { - bannerheader = NULL; + if (kv_add(&http_headers, "Server", HTTPD_SERVERNAME) == NULL) goto done; - } + + if (kv_add(&http_headers, "Connection", "close") == NULL) + goto done; + + if (kv_add(&http_headers, "Content-Type", "text/html") == NULL) + goto done; + + /* Date header is mandatory and should be added as late as possible */ + if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 || + kv_add(&http_headers, "Date", tmbuf) == NULL) + goto done; + + if (server_custom_headers(srv_conf, &http_headers, code) == -1) + goto done; + + if ((headers = get_custom_headers(&http_headers)) == NULL) + goto done; /* Add basic HTTP headers */ if ((httpmsglen = asprintf(&httpmsg, - "HTTP/1.0 %03d %s\r\n" - "Date: %s\r\n" - "%s" - "Connection: close\r\n" - "Content-Type: text/html\r\n" - "%s" - "%s" - "%s" - "\r\n" - "%s", - code, httperr, tmbuf, - bannerheader == NULL ? "" : bannerheader, - clenheader == NULL ? "" : clenheader, - extraheader == NULL ? "" : extraheader, - hstsheader == NULL ? "" : hstsheader, - desc->http_method == HTTP_METHOD_HEAD || clenheader == NULL ? - "" : body)) == -1) + "HTTP/1.0 %03d %s\r\n%s\r\n%s", + code, httperr, headers, + desc->http_method == HTTP_METHOD_HEAD || !has_body ? "" : body)) + == -1) goto done; free(clt->clt_close_msg); @@ -1036,20 +1053,16 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) free(httpmsg); goto done; } + kv_purge(&http_headers); free(httpmsg); free(body); - free(extraheader); - free(hstsheader); - free(clenheader); - free(bannerheader); + free(headers); return; done: + kv_purge(&http_headers); free(body); - free(extraheader); - free(hstsheader); - free(clenheader); - free(bannerheader); + free(headers); if (msg == NULL) msg = "\"\""; if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) { @@ -1598,6 +1611,79 @@ server_locationaccesstest(struct server_config *srv_conf, const char *path) (ret == 0 && SRVFLAG_LOCATION_NOT_FOUND & srv_conf->flags)); } +int +server_custom_headers(struct server_config *srv_conf, struct kvtree *headers, + unsigned int code) +{ + struct custom_header *hdr; + struct kv *kv, search; + + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { + /* Only include headers not marked ALWAYS on success. */ + if (!(hdr->flags & HEADER_ALWAYS) && !http_is_success(code)) { + print_custom_header("skip", hdr); + continue; + } + + search.kv_key = hdr->name; + + /* deletes all existing headers of the same key */ + if (hdr->flags & HEADER_REMOVE) { + print_custom_header("remove", hdr); + while ((kv = kv_find(headers, &search)) != NULL) + kv_delete(headers, kv); + /* replaces all existing headers of the same name */ + } else if (hdr->flags & HEADER_SET) { + print_custom_header("set", hdr); + while ((kv = kv_find(headers, &search)) != NULL) + kv_delete(headers, kv); + + if (kv_add(headers, hdr->name, hdr->value) == NULL) + return (-1); + /* appends a new header without checking for duplicates */ + } else if (hdr->flags & HEADER_ADD) { + print_custom_header("add", hdr); + if (kv_add(headers, hdr->name, hdr->value) == NULL) + return (-1); + } + } + return (0); +} + +/* + * Convert a kvtree with headers (name, value) into a raw HTTP header block. + */ +char * +get_custom_headers(struct kvtree *headers) +{ + struct kv *hdr, *child; + char *nheaders = NULL, *tmp; + + RB_FOREACH(hdr, kvtree, headers) { + if (asprintf(&tmp, "%s%s: %s\r\n", + nheaders == NULL ? "" : nheaders, + hdr->kv_key, + hdr->kv_value == NULL ? "" : hdr->kv_value) == -1) + goto fail; + free(nheaders); + nheaders = tmp; + + TAILQ_FOREACH(child, &hdr->kv_children, kv_entry) { + if (asprintf(&tmp, "%s%s: %s\r\n", nheaders, + child->kv_key, + child->kv_value == NULL ? "" : child->kv_value) + == -1) + goto fail; + free(nheaders); + nheaders = tmp; + } + } + return (nheaders); + fail: + free(nheaders); + return (NULL); +} + int server_response_http(struct client *clt, unsigned int code, struct media_type *media, off_t size, time_t mtime) @@ -1675,6 +1761,9 @@ server_response_http(struct client *clt, unsigned int code, return (-1); } + if (server_custom_headers(srv_conf, &resp->http_headers, code) == -1) + return (-1); + /* Date header is mandatory and should be added as late as possible */ if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 || kv_add(&resp->http_headers, "Date", tmbuf) == NULL)