Download raw body.
httpd: add custom HTTP header support #2
On Thu Mar 19, 2026 at 01:31:34AM +0100, Kirill A. Korinsky wrote:
> On Wed, 18 Mar 2026 17:48:54 +0100,
> Rafael Sadowski <rafael@sizeofvoid.org> 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?
> >
>
> Sorry for extreamly long response time.
>
> I think this is needed and I mostly OK with it a few things which I had
> noticed.
>
> 1. Order of headers is a bit unexpected. If I not mistaken it is reversed
> config order. Probably worth to respect the config order.
Good catch. The receiving side in config_getserver_headers() used
TAILQ_INSERT_HEAD, reversing the list once during the imsg transfer.
I changed to TAILQ_INSERT_TAIL, so headers now stay in config order
end-to-end (parse.y -> imsg -> server process): set/add/remove are
applied in the order they appear in httpd.conf, and repeated "add"
headers with the same name are emitted in config order. Note that the
on-wire order of *distinct* header names follows the existing kvtree
serialization, same as all other response headers.
One of my many test-cases:
chroot "/home/rsadowski/src/httpd-tests/01-basic-add/"
server "test" {
listen on 127.0.0.1 port 8888
#header set "Last-Modified" "FUCKUP"
#header add "X-Custom-Header" "TestValue"
#header add "X-Powered-By" "OpenBSD httpd"
#header add "X-Custom-Header" "TestValue" always
header add "X-Test" "first"
header add "X-Test" "second"
header set "X-Test" "final"
#header remove "Server"
root "/htdocs"
}
curl -s -I http://127.0.0.1:8888/ | grep X-Test
X-Test: final
>
> 2. Server generated replies, seems, to be excluded from new custom header
> logic. Which includes backend / FastCGI error responses.
FastCGI responses are not excluded: server_fcgi_header() runs
server_custom_headers() on every response delivered by the backend,
including backennd error statuses (headers marked "always" are applied
there). While looking into this I also replaced the nginx-style status
code whitelist with a plain 2xx/3xx range check, so codes like
202/203/205/226/300 are no longer silently skipped and the code matches
the man page.
Server-generated replies (httpd own error pages and "block return"
redirects) now include all headers marked "always", appended in
server_abort_http().
Full set/remove semantics on that path would require reworking
server_abort_http() to build its headers via a kvtree like the regular
paths; I'd prefer to do that in a follow-up diff rather than grow this
one further.
Other changes since the last version:
- serverconfig_byid() return value is now checked in
config_getserver_headers() (and the same missing check fixed in
config_getserver_fcgiparams() while there)
- TAILQ_INIT(&srv->srv_conf.headers) after the memcpy in
config_getserver(), so the server process never sees stale list
pointers
- get_always_custom_headers() is static now
- http_is_success() simplified to a range check
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 ;)
diff --git a/config.c b/config.c
index 146ed2c..360a009 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));
@@ -271,7 +272,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 "
@@ -279,7 +284,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);
}
@@ -365,7 +383,10 @@ config_getserver_fcgiparams(struct httpd *env, struct imsg *imsg)
p += sizeof(nc);
memcpy(&id, p, sizeof(id)); /* server conf id */
- srv_conf = serverconfig_byid(id);
+ if ((srv_conf = serverconfig_byid(id)) == NULL) {
+ log_debug("%s: invalid config id", __func__);
+ return (-1);
+ }
p += sizeof(id);
len += nc*sizeof(*fp);
@@ -443,6 +464,165 @@ 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 */
+ if ((srv_conf = serverconfig_byid(id)) == NULL) {
+ log_debug("%s: invalid config id", __func__);
+ return (-1);
+ }
+ 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_TAIL(&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)
{
@@ -727,6 +907,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.conf.5 b/httpd.conf.5
index caf55a4..f276ab4 100644
--- a/httpd.conf.5
+++ b/httpd.conf.5
@@ -485,6 +485,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:
@@ -917,6 +968,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 740721e..289b713 100644
--- a/httpd.h
+++ b/httpd.h
@@ -187,6 +187,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,
@@ -448,6 +449,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;
@@ -519,6 +533,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 +657,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
@@ -771,9 +788,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 d6bc335..394dc50 100644
--- a/parse.y
+++ b/parse.y
@@ -135,14 +135,14 @@ typedef struct {
%}
-%token ACCESS ALIAS AUTO BACKLOG BODY BUFFER CERTIFICATE CHROOT CIPHERS COMMON
-%token COMBINED CONNECTION DHE DIRECTORY ECDHE ERR FCGI INDEX IP KEY LIFETIME
-%token LISTEN LOCATION LOG LOGDIR MATCH MAXIMUM NO NODELAY OCSP ON PORT PREFORK
-%token PROTOCOLS REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE SYSLOG TCP TICKET
-%token TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS DEFAULT PRELOAD REQUEST
-%token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS REWRITE
-%token CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT
-%token ERRDOCS GZIPSTATIC BANNER STATIC_CACHE_CONTROL
+%token ACCESS ADD ALIAS ALWAYS AUTHENTICATE AUTO BACKLOG BANNER BLOCK BODY
+%token BUFFER CA CERTIFICATE CHROOT CIPHERS CLIENT COMBINED COMMON CONNECTION
+%token CRL DEFAULT DHE DIRECTORY DROP ECDHE ERR ERRDOCS ERROR FCGI FORWARDED
+%token FOUND GZIPSTATIC HEADER HSTS INCLUDE INDEX IP KEY LIFETIME LISTEN
+%token LOCATION LOG LOGDIR MATCH MAXAGE MAXIMUM NO NODELAY NOT OCSP ON OPTIONAL
+%token PARAM PASS PORT PREFORK PRELOAD PROTOCOLS REMOVE REQUEST REQUESTS RETURN
+%token REWRITE ROOT SACK SERVER SET SOCKET STATIC_CACHE_CONTROL STRIP STYLE
+%token SUBDOMAINS SYSLOG TCP TICKET TIMEOUT TLS TYPE TYPES WITH
%token <v.string> STRING
%token <v.number> NUMBER
%type <v.port> port
@@ -325,6 +325,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 '}' {
@@ -559,6 +560,7 @@ serveroptsl : LISTEN ON STRING opttls port {
| root
| directory
| banner
+ | header
| static_cache_control
| logformat
| fastcgi
@@ -646,6 +648,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;
@@ -713,6 +716,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; }
@@ -1472,7 +1616,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 },
@@ -1500,6 +1646,7 @@ lookup(char *s)
{ "forwarded", FORWARDED },
{ "found", FOUND },
{ "gzip-static", GZIPSTATIC },
+ { "header", HEADER },
{ "hsts", HSTS },
{ "include", INCLUDE },
{ "index", INDEX },
@@ -1525,6 +1672,7 @@ lookup(char *s)
{ "prefork", PREFORK },
{ "preload", PRELOAD },
{ "protocols", PROTOCOLS },
+ { "remove", REMOVE },
{ "request", REQUEST },
{ "requests", REQUESTS },
{ "return", RETURN },
@@ -1532,6 +1680,7 @@ lookup(char *s)
{ "root", ROOT },
{ "sack", SACK },
{ "server", SERVER },
+ { "set", SET },
{ "socket", SOCKET },
{ "static-cache-control", STATIC_CACHE_CONTROL },
{ "strip", STRIP },
@@ -2324,12 +2473,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");
@@ -2419,6 +2580,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/server.c b/server.c
index 0731730..832788e 100644
--- a/server.c
+++ b/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 *
@@ -1356,6 +1361,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/server_fcgi.c b/server_fcgi.c
index 15e5ee8..6ee84a8 100644
--- a/server_fcgi.c
+++ b/server_fcgi.c
@@ -724,6 +724,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/server_http.c b/server_http.c
index 9706c80..a88ca43 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_always_custom_headers(struct server_config *);
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)
{
@@ -889,6 +897,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;
+ char *customheaders = NULL;
char buf[IBUF_READ_SIZE];
char *escapedmsg = NULL;
ssize_t bodylen;
@@ -1004,6 +1013,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 ((httpmsglen = asprintf(&httpmsg,
"HTTP/1.0 %03d %s\r\n"
@@ -1014,6 +1025,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
"%s"
"%s"
"%s"
+ "%s"
"\r\n"
"%s",
code, httperr, tmbuf,
@@ -1021,6 +1033,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;
@@ -1050,6 +1063,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
free(hstsheader);
free(clenheader);
free(bannerheader);
+ free(customheaders);
if (msg == NULL)
msg = "\"\"";
if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) {
@@ -1598,6 +1612,72 @@ 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))
+ 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)
@@ -1674,6 +1754,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