Index | Thread | Search

From:
Claudio Jeker <cjeker@diehard.n-r-g.com>
Subject:
Re: httpd: add custom HTTP header support #2
To:
Rafael Sadowski <rafael@sizeofvoid.org>
Cc:
tech@openbsd.org, "Anthony J. Bentley" <bentley@openbsd.org>, "Kirill A. Korinsky" <kirill@korins.ky>, Stuart Henderson <stu@spacehopper.org>
Date:
Wed, 18 Mar 2026 20:53:00 +0100

Download raw body.

Thread
On Wed, Mar 18, 2026 at 05:48:54PM +0100, Rafael Sadowski wrote:
> 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?

I think this is a sensible addition which is needed when you want to play
with content caching headers. My problem is that I have too much other
stuff on my plate to properly review this diff.
 
> > 
> > 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 ||
> > 
> 

-- 
:wq Claudio