From: Rafael Sadowski Subject: Re: httpd: add custom HTTP header support #2 To: tech@openbsd.org Cc: "Anthony J. Bentley" , "Kirill A. Korinsky" , Stuart Henderson Date: Wed, 18 Mar 2026 17:48:54 +0100 On Wed Feb 11, 2026 at 12:59:27AM +0100, Rafael Sadowski wrote: > This is the second attempt to add custom HTTP header support to httpd. > > After receiving initial feedback and advice from Stuart, I changed and > improved the whole idea (This made it very powerful but also somewhat more > complex to test) and the syntax: > > Allow httpd.conf to set/add/remove custom HTTP response headers or > suppress existing ones. This enables httpd to add security headers, > custom metadata, or remove unwanted headers without modifying > app/fastcgi code. > > Three new directives are added: > > header set name value [always] > Set a custom HTTP response header with the specified name > and value. If a header with the same 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. > > header add name value [always] > Add a custom HTTP response header with the specified name > and value. Unlike set, this option appends the header > even if one with the same 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. > > header remove name > Suppress the HTTP response header with the specified > name. This can be used to remove headers added by > default, such as "Last-Modified", as well as headers > inherited from a parent server configuration. > > If always is specified, the header will be included in all > responses, including error pages (4xx and 5xx status codes). > > Header names are limited to 127 characters and values to 511 > characters. Headers defined in a location block are inherited > from the server context and override defined headers with the > same name. If you do not wish to inherit these, you can remove > them again with remove. > > Example configuration: > > server "example.com" { > listen on * tls port 443 > > header add "X-Frame-Options" "SAMEORIGIN" always > header add "X-Content-Type-Options" "nosniff" always > header set "X-Powered-By" "OpenBSD httpd" > > 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" > } > } > > The implementation follows the existing pattern form FastCGI parameters. > So I didn't have to invent anything new. Much of the code is from > existing patterns (so the potential bugs too). Just to give one example: > The entire IMSG part. > > The motivation was to have something similar to ngx_http_headers_module > without relayd coming into play. For this reason I follow the nginx pattern: > > "Adds the specified field to a response header provided that the > response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, > 307 (1.1.16, 1.0.13), or 308 (1.13.0). Parameter value can contain > variables." > -- https://nginx.org/en/docs/http/ngx_http_headers_module.html > > I tried to test all combinations (and sure I missed some) but feel > comfortable with them. My goal was not to modify current behavior. > > I would like to ask for more and more tests (big wild setups) and maybe > OKs. Is anyone interested? > > Rafael > > diff --git a/usr.sbin/httpd/config.c b/usr.sbin/httpd/config.c > index 300a5f2caca..5c10f4218f1 100644 > --- a/usr.sbin/httpd/config.c > +++ b/usr.sbin/httpd/config.c > @@ -196,6 +196,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)); > @@ -270,7 +271,11 @@ config_setserver(struct httpd *env, struct server *srv) > > /* Configure TLS if necessary. */ > config_setserver_tls(env, srv); > - } else { > + /* Configure custom headers if necessary. */ > + config_setserver_headers(env, srv); > + > + } else if (id == PROC_SERVER && > + (srv->srv_conf.flags & SRVFLAG_LOCATION)) { > if (proc_composev(ps, id, IMSG_CFG_SERVER, > iov, c) != 0) { > log_warn("%s: failed to compose " > @@ -278,7 +283,20 @@ config_setserver(struct httpd *env, struct server *srv) > __func__, srv->srv_conf.name); > return (-1); > } > + /* Configure FCGI parameters if necessary. */ > + config_setserver_fcgiparams(env, srv); > > + /* Configure custom headers if necessary. */ > + config_inherit_headers(env, srv); > + config_setserver_headers(env, srv); > + } else { > + if (proc_composev(ps, id, IMSG_CFG_SERVER, > + iov, c) != 0) { > + log_warn("%s: failed to compose " > + "IMSG_CFG_SERVER imsg for `%s'", > + __func__, srv->srv_conf.name); > + return (-1); > + } > /* Configure FCGI parameters if necessary. */ > config_setserver_fcgiparams(env, srv); > } > @@ -442,6 +460,162 @@ config_setserver_fcgiparams(struct httpd *env, struct server *srv) > return (0); > } > > +int > +config_getserver_headers(struct httpd *env, struct imsg *imsg) > +{ > + struct server *srv; > + struct server_config *srv_conf, *iconf; > + struct custom_header *hdr; > + uint32_t id; > + size_t c, nc, len; > + uint8_t *p = imsg->data; > + > + len = sizeof(nc) + sizeof(id); > + if (IMSG_DATA_SIZE(imsg) < len) { > + log_debug("%s: invalid message length", __func__); > + return (-1); > + } > + > + memcpy(&nc, p, sizeof(nc)); /* number of headers */ > + p += sizeof(nc); > + > + memcpy(&id, p, sizeof(id)); /* server conf id */ > + srv_conf = serverconfig_byid(id); > + p += sizeof(id); > + > + len += nc*sizeof(*hdr); > + if (IMSG_DATA_SIZE(imsg) < len) { > + log_debug("%s: invalid message length", __func__); > + return (-1); > + } > + > + /* Find associated server config */ > + TAILQ_FOREACH(srv, env->sc_servers, srv_entry) { > + if (srv->srv_conf.id == id) { > + srv_conf = &srv->srv_conf; > + break; > + } > + TAILQ_FOREACH(iconf, &srv->srv_hosts, entry) { > + if (iconf->id == id) { > + srv_conf = iconf; > + break; > + } > + } > + } > + > + /* Fetch custom headers */ > + for (c = 0; c < nc; c++) { > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatalx("headers out of memory"); > + memcpy(hdr, p, sizeof(*hdr)); > + TAILQ_INSERT_HEAD(&srv_conf->headers, hdr, entry); > + > + p += sizeof(*hdr); > + } > + > + return (0); > +} > + > +static 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); > +} > + > +/* > + * 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, *hdr_copy; > + > + 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_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; > + } > + > + if ((hdr_copy = calloc(1, sizeof(*hdr_copy))) == NULL) > + fatal("out of memory"); > + > + strlcpy(hdr_copy->name, hdr->name, sizeof(hdr_copy->name)); > + strlcpy(hdr_copy->value, hdr->value, sizeof(hdr_copy->value)); > + hdr_copy->flags = hdr->flags; > + > + TAILQ_INSERT_TAIL(&srv_conf->headers, hdr_copy, entry); > + DPRINTF("%s: inheriting header \"%s\" from parent \"%s\" " > + "to location \"%s\"", __func__, hdr->name, > + parent_srv->srv_conf.name, srv_conf->location); > + } > +} > + > +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 iovec *iov; > + size_t c = 0, nc = 0; > + > + 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); > + > + if (TAILQ_EMPTY(&srv_conf->headers)) /* nothing to do */ > + return (0); > + > + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { > + nc++; > + } > + if ((iov = calloc(nc + 2, sizeof(*iov))) == NULL) > + return (-1); > + > + iov[c].iov_base = &nc; /* number of headers */ > + iov[c++].iov_len = sizeof(nc); > + iov[c].iov_base = &srv_conf->id; /* server config id */ > + iov[c++].iov_len = sizeof(srv_conf->id); > + > + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { /* push headers */ > + iov[c].iov_base = hdr; > + iov[c++].iov_len = sizeof(*hdr); > + } > + if (proc_composev(ps, PROC_SERVER, IMSG_CFG_HEADERS, iov, c) != 0) { > + log_warn("%s: failed to compose IMSG_CFG_HEADERS imsg for " > + "`%s'", __func__, srv_conf->name); > + free(iov); > + return (-1); > + } > + free(iov); > + > + return (0); > +} > + > int > config_setserver_tls(struct httpd *env, struct server *srv) > { > diff --git a/usr.sbin/httpd/httpd.conf.5 b/usr.sbin/httpd/httpd.conf.5 > index 3053709a359..72d80628ac6 100644 > --- a/usr.sbin/httpd/httpd.conf.5 > +++ b/usr.sbin/httpd/httpd.conf.5 > @@ -464,6 +464,57 @@ 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 > +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 will be included in all responses, > +including error pages (4xx and 5xx status codes). > +.Pp > +Header names are limited to 127 characters and values to 511 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: > @@ -895,6 +946,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/usr.sbin/httpd/httpd.h b/usr.sbin/httpd/httpd.h > index baf11d1b523..096f68f02b0 100644 > --- a/usr.sbin/httpd/httpd.h > +++ b/usr.sbin/httpd/httpd.h > @@ -209,6 +209,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, > @@ -470,6 +471,19 @@ struct fastcgi_param { > }; > TAILQ_HEAD(server_fcgiparams, fastcgi_param); > > +struct custom_header { > + char name[128]; > + char value[512]; > + uint8_t flags; > +#define HEADER_REMOVE 0x01 > +#define HEADER_ADD 0x02 > +#define HEADER_SET 0x04 > +#define HEADER_ALWAYS 0x08 > + > + TAILQ_ENTRY(custom_header) entry; > +}; > +TAILQ_HEAD(server_headers, custom_header); > + > struct server_config { > uint32_t id; > uint32_t parent_id; > @@ -540,6 +554,7 @@ struct server_config { > > struct server_fcgiparams fcgiparams; > int fcgistrip; > + struct server_headers headers; > char errdocroot[HTTPD_ERRDOCROOT_MAX]; > > TAILQ_ENTRY(server_config) entry; > @@ -673,6 +688,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 *); > +void server_custom_headers(struct server_config *, struct kvtree *, > + unsigned int); > unsigned int > server_httpmethod_byname(const char *); > const char > @@ -825,9 +842,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/usr.sbin/httpd/parse.y b/usr.sbin/httpd/parse.y > index 326b249662f..d2d6282439f 100644 > --- a/usr.sbin/httpd/parse.y > +++ b/usr.sbin/httpd/parse.y > @@ -142,6 +142,7 @@ typedef struct { > %token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS REWRITE > %token CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT > %token ERRDOCS GZIPSTATIC BANNER > +%token HEADER ADD ALWAYS REMOVE SET > %token STRING > %token NUMBER > %type port > @@ -323,6 +324,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 '}' { > @@ -557,6 +559,7 @@ serveroptsl : LISTEN ON STRING opttls port { > | root > | directory > | banner > + | header > | logformat > | fastcgi > | authenticate > @@ -643,6 +646,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; > uint32_t f; > @@ -710,6 +714,147 @@ banner : BANNER { > } > ; > > +header : HEADER REMOVE STRING { > + struct custom_header *hdr; > + > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatal("out of memory"); > + > + if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >= > + sizeof(hdr->name)) { > + yyerror("header name truncated"); > + free($3); > + free(hdr); > + YYERROR; > + } > + if (strcmp("Server", hdr->name) == 0) { > + yyerror("'header remover Server' " > + "ignored, use 'no banner'"); > + free($3); > + free(hdr); > + YYERROR; > + } > + free($3); > + > + hdr->flags = HEADER_REMOVE; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + | HEADER ADD STRING STRING { > + struct custom_header *hdr; > + > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatal("out of memory"); > + > + if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >= > + sizeof(hdr->name)) { > + yyerror("header name truncated"); > + free($3); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($3); > + > + if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >= > + sizeof(hdr->value)) { > + yyerror("header value truncated"); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($4); > + > + hdr->flags = HEADER_ADD; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + | HEADER ADD STRING STRING ALWAYS { > + struct custom_header *hdr; > + > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatal("out of memory"); > + > + if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >= > + sizeof(hdr->name)) { > + yyerror("header name truncated"); > + free($3); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($3); > + > + if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >= > + sizeof(hdr->value)) { > + yyerror("header value truncated"); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($4); > + > + hdr->flags = HEADER_ADD; > + hdr->flags |= HEADER_ALWAYS; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + | HEADER SET STRING STRING { > + struct custom_header *hdr; > + > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatal("out of memory"); > + > + if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >= > + sizeof(hdr->name)) { > + yyerror("header name truncated"); > + free($3); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($3); > + > + if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >= > + sizeof(hdr->value)) { > + yyerror("header value truncated"); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($4); > + > + hdr->flags = HEADER_SET; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + | HEADER SET STRING STRING ALWAYS { > + struct custom_header *hdr; > + > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatal("out of memory"); > + > + if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >= > + sizeof(hdr->name)) { > + yyerror("header name truncated"); > + free($3); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($3); > + > + if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >= > + sizeof(hdr->value)) { > + yyerror("header value truncated"); > + free($4); > + free(hdr); > + YYERROR; > + } > + free($4); > + > + hdr->flags = HEADER_SET; > + hdr->flags |= HEADER_ALWAYS; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + ; > + > optfound : /* empty */ { $$ = 0; } > | FOUND { $$ = 1; } > | NOT FOUND { $$ = -1; } > @@ -1447,7 +1592,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 }, > @@ -1475,6 +1622,7 @@ lookup(char *s) > { "forwarded", FORWARDED }, > { "found", FOUND }, > { "gzip-static", GZIPSTATIC }, > + { "header", HEADER }, > { "hsts", HSTS }, > { "include", INCLUDE }, > { "index", INDEX }, > @@ -1500,6 +1648,7 @@ lookup(char *s) > { "prefork", PREFORK }, > { "preload", PRELOAD }, > { "protocols", PROTOCOLS }, > + { "remove", REMOVE }, > { "request", REQUEST }, > { "requests", REQUESTS }, > { "return", RETURN }, > @@ -1507,6 +1656,7 @@ lookup(char *s) > { "root", ROOT }, > { "sack", SACK }, > { "server", SERVER }, > + { "set", SET }, > { "socket", SOCKET }, > { "strip", STRIP }, > { "style", STYLE }, > @@ -2298,12 +2448,24 @@ 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) { > + if ((nhdr = calloc(1, sizeof(*nhdr))) == NULL) > + fatal("out of memory"); > + strlcpy(nhdr->name, hdr->name, sizeof(nhdr->name)); > + strlcpy(nhdr->value, hdr->value, sizeof(nhdr->value)); > + nhdr->flags = hdr->flags; > + 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"); > @@ -2393,6 +2555,18 @@ 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) { > + if ((nhdr = calloc(1, sizeof(*nhdr))) == NULL) > + fatal("out of memory"); > + strlcpy(nhdr->name, hdr->name, sizeof(nhdr->name)); > + strlcpy(nhdr->value, hdr->value, sizeof(nhdr->value)); > + nhdr->flags = hdr->flags; > + TAILQ_INSERT_TAIL(&dstl->srv_conf.headers, nhdr, entry); > + } > + > strlcpy(dstl->srv_conf.name, alias->name, > sizeof(dstl->srv_conf.name)); > > diff --git a/usr.sbin/httpd/server.c b/usr.sbin/httpd/server.c > index a38cf018d81..ed97f3f7e3c 100644 > --- a/usr.sbin/httpd/server.c > +++ b/usr.sbin/httpd/server.c > @@ -470,6 +470,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); > @@ -485,6 +486,9 @@ 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); > } > > void > @@ -503,6 +507,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 * > @@ -1366,6 +1371,9 @@ server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg) > case IMSG_CFG_FCGI: > config_getserver_fcgiparams(httpd_env, imsg); > break; > + case IMSG_CFG_HEADERS: > + config_getserver_headers(httpd_env, imsg); > + break; > case IMSG_CFG_DONE: > config_getcfg(httpd_env, imsg); > break; > diff --git a/usr.sbin/httpd/server_fcgi.c b/usr.sbin/httpd/server_fcgi.c > index c5f9917204c..02165ba3e32 100644 > --- a/usr.sbin/httpd/server_fcgi.c > +++ b/usr.sbin/httpd/server_fcgi.c > @@ -723,6 +723,8 @@ server_fcgi_header(struct client *clt, unsigned int code) > return (-1); > } > > + server_custom_headers(srv_conf, &resp->http_headers, code); > + > /* 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/usr.sbin/httpd/server_http.c b/usr.sbin/httpd/server_http.c > index afdb73f243f..96faa0fb4e5 100644 > --- a/usr.sbin/httpd/server_http.c > +++ b/usr.sbin/httpd/server_http.c > @@ -50,10 +50,12 @@ 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 *); > char *read_errdoc(const char *, const char *); > +char *get_always_custom_headers(struct server_config *); > > static struct http_method http_methods[] = HTTP_METHODS; > static struct http_error http_errors[] = HTTP_ERRORS; > @@ -219,6 +221,25 @@ http_version_num(char *version) > } > return (0); > } > +static int > +http_is_success(unsigned int code) > +{ > + switch (code) { > + case 200: > + case 201: > + case 204: > + case 206: > + case 301: > + case 302: > + case 303: > + case 304: > + case 307: > + case 308: > + return (0); > + default: > + return (1); > + } > +} > > void > server_read_http(struct bufferevent *bev, void *arg) > @@ -892,6 +913,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > char tmbuf[32], hbuf[128], *hstsheader = NULL; > char *clenheader = NULL; > char *bannerheader = NULL, *bannertoken = NULL; > + char *customheaders = NULL; > char buf[IBUF_READ_SIZE]; > char *escapedmsg = NULL; > char cstr[5]; > @@ -1060,6 +1082,8 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > goto done; > } > > + customheaders = get_always_custom_headers(srv_conf); > + > /* Add basic HTTP headers */ > if (asprintf(&httpmsg, > "HTTP/1.0 %03d %s\r\n" > @@ -1070,6 +1094,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > "%s" > "%s" > "%s" > + "%s" > "\r\n" > "%s", > code, httperr, tmbuf, > @@ -1077,6 +1102,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > clenheader == NULL ? "" : clenheader, > extraheader == NULL ? "" : extraheader, > hstsheader == NULL ? "" : hstsheader, > + customheaders == NULL ? "" : customheaders, > desc->http_method == HTTP_METHOD_HEAD || clenheader == NULL ? > "" : body) == -1) > goto done; > @@ -1092,6 +1118,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > free(clenheader); > free(bannerheader); > free(bannertoken); > + free(customheaders); > if (msg == NULL) > msg = "\"\""; > if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) { > @@ -1561,6 +1588,73 @@ server_locationaccesstest(struct server_config *srv_conf, const char *path) > (ret == 0 && SRVFLAG_LOCATION_NOT_FOUND & srv_conf->flags)); > } > > +void > +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) != 0) > + continue; > + > + search.kv_key = hdr->name; > + > + /* deletes all existing headers of the same key */ > + if (hdr->flags & HEADER_REMOVE) { > + 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) { > + while ((kv = kv_find(headers, &search)) != NULL) > + kv_delete(headers, kv); > + > + if (kv_add(headers, hdr->name, hdr->value) == NULL) > + return; > + /* appends a new header without checking for duplicates */ > + } else if (hdr->flags & HEADER_ADD) { > + if (kv_add(headers, hdr->name, hdr->value) == NULL) > + return; > + } > + } > +} > + > +/* > + * Build a raw custom HTTP header that only includes headers marked as always > + */ > +char * > +get_always_custom_headers(struct server_config *srv_conf) > +{ > + struct custom_header *hdr; > + char *headers = NULL; > + char *tmp = NULL; > + > + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { > + if (!(hdr->flags & HEADER_ALWAYS) > + || hdr->flags & HEADER_REMOVE) > + continue; > + > + if (headers == NULL) { > + if (asprintf(&headers, "%s: %s\r\n", hdr->name, > + hdr->value) == -1) { > + return (NULL); > + } > + } else { > + if (asprintf(&tmp, "%s%s: %s\r\n", headers, hdr->name, > + hdr->value) == -1) { > + free(headers); > + return (NULL); > + } > + free(headers); > + headers = tmp; > + } > + } > + return (headers); > +} > + > int > server_response_http(struct client *clt, unsigned int code, > struct media_type *media, off_t size, time_t mtime) > @@ -1627,6 +1721,7 @@ server_response_http(struct client *clt, unsigned int code, > "; preload" : "") == -1) > return (-1); > } > + server_custom_headers(srv_conf, &resp->http_headers, code); > > /* Date header is mandatory and should be added as late as possible */ > if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 || >