Download raw body.
httpd: add custom HTTP header support #2
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 <v.string> STRING
> %token <v.number> NUMBER
> %type <v.port> 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 ||
>
httpd: add custom HTTP header support #2