Index | Thread | Search

From:
Adam Mullins <alm@alm4x.com>
Subject:
Re: add support to httpd for serving static brotli encoded files
To:
Christian Schulte <cs@schulte.it>
Cc:
tech@openbsd.org, Lloyd <ng2d68@proton.me>, Crystal Kolipe <kolipe.c@exoticsilicon.com>
Date:
Wed, 25 Feb 2026 10:46:36 +0100

Download raw body.

Thread
  • Adam Mullins:

    add support to httpd for serving static brotli encoded files

On 2/24/26 8:46 AM, Christian Schulte wrote:
>
> Composing mail including a diff using thunderbird is impossible. A
> workflow to consider maybe...
>

So it seems. I'll try that workflow.

Here is another unified diff against -current. Compared to the first it:
- Reorders SRVFLAG_BITS as suggested by Lloyd and adds an entry for
  brotli-static.
- Edits comments, code, and whitespace to conform to style(9).
- Removes some obvious comments and trims the length of the others.
- Sorts variable declarations to conform; ie largest to smallest.
- Renames the helper function from server_file_encoded_path() to
  find_compressed_path(). I wanted to avoid 'encoded' even though
  that is the name of the HTTP header because it could be confused for
  URL encoding. I also dropped the server_file prefix, following the
  lead of some other internal functions in server_file.c.

Index: usr.sbin/httpd/httpd.conf.5
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/httpd.conf.5,v
diff -u -p -u -r1.129 httpd.conf.5
--- usr.sbin/httpd/httpd.conf.5	18 Jan 2026 16:38:02 -0000	1.129
+++ usr.sbin/httpd/httpd.conf.5	25 Feb 2026 09:14:50 -0000
@@ -464,6 +464,15 @@ Enable static gzip compression to save b
 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 brotli-static
+Enable static brotli compression to save bandwidth.
+.Pp
+If brotli encoding is accepted and if the requested file exists with
+a .br suffix and if and the client is connected with TLS, use the compressed
+file instead and deliver it with content encoding br.
+.Pp
+If both brotli-static and gzip-static are enabled, brotli is preferred
+if the above conditions are satisfied.
 .It Ic hsts Oo Ar option Oc
 Enable HTTP Strict Transport Security.
 Valid options are:
Index: usr.sbin/httpd/httpd.h
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/httpd.h,v
diff -u -p -u -r1.167 httpd.h
--- usr.sbin/httpd/httpd.h	28 Nov 2025 16:10:00 -0000	1.167
+++ usr.sbin/httpd/httpd.h	25 Feb 2026 09:14:50 -0000
@@ -390,6 +390,7 @@ SPLAY_HEAD(client_tree, client);
 #define SRVFLAG_NO_PATH_REWRITE	0x02000000
 #define SRVFLAG_GZIP_STATIC	0x04000000
 #define SRVFLAG_NO_BANNER	0x08000000
+#define SRVFLAG_BROTLI_STATIC	0x10000000
 #define SRVFLAG_LOCATION_FOUND	0x40000000
 #define SRVFLAG_LOCATION_NOT_FOUND 0x80000000
 
@@ -399,8 +400,8 @@ SPLAY_HEAD(client_tree, client);
 	"\14SYSLOG\15NO_SYSLOG\16TLS\17ACCESS_LOG\20ERROR_LOG"		\
 	"\21AUTH\22NO_AUTH\23BLOCK\24NO_BLOCK\25LOCATION_MATCH"		\
 	"\26SERVER_MATCH\27SERVER_HSTS\30DEFAULT_TYPE\31PATH_REWRITE"	\
-	"\32NO_PATH_REWRITE\34NO_BANNER\33GZIP_STATIC\37LOCATION_FOUND"	\
-	"\40LOCATION_NOT_FOUND"
+	"\32NO_PATH_REWRITE\33GZIP_STATIC\34NO_BANNER\35BROTLI_STATIC"	\
+	"\37LOCATION_FOUND\40LOCATION_NOT_FOUND"
 
 #define TCPFLAG_NODELAY		0x01
 #define TCPFLAG_NNODELAY	0x02
Index: usr.sbin/httpd/parse.y
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/parse.y,v
diff -u -p -u -r1.130 parse.y
--- usr.sbin/httpd/parse.y	28 Nov 2025 16:10:00 -0000	1.130
+++ usr.sbin/httpd/parse.y	25 Feb 2026 09:14:50 -0000
@@ -141,7 +141,7 @@ typedef struct {
 %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
+%token	ERRDOCS GZIPSTATIC BANNER BROTLISTATIC
 %token	<v.string>	STRING
 %token  <v.number>	NUMBER
 %type	<v.port>	port
@@ -561,6 +561,7 @@ serveroptsl	: LISTEN ON STRING opttls po
 		| fastcgi
 		| authenticate
 		| gzip_static
+		| brotli_static
 		| filter
 		| LOCATION optfound optmatch STRING	{
 			struct server		*s;
@@ -1249,6 +1250,14 @@ gzip_static	: NO GZIPSTATIC		{
 		}
 		;
 
+brotli_static	: NO BROTLISTATIC	{
+			srv->srv_conf.flags &= ~SRVFLAG_BROTLI_STATIC;
+		}
+		| BROTLISTATIC		{
+			srv->srv_conf.flags |= SRVFLAG_BROTLI_STATIC;
+		}
+		;
+
 tcpip		: TCP '{' optnl tcpflags_l '}'
 		| TCP tcpflags
 		;
@@ -1455,6 +1464,7 @@ lookup(char *s)
 		{ "block",		BLOCK },
 		{ "body",		BODY },
 		{ "buffer",		BUFFER },
+		{ "brotli-static",	BROTLISTATIC },
 		{ "ca",			CA },
 		{ "certificate",	CERTIFICATE },
 		{ "chroot",		CHROOT },
Index: usr.sbin/httpd/server_file.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server_file.c,v
diff -u -p -u -r1.80 server_file.c
--- usr.sbin/httpd/server_file.c	29 Apr 2024 16:17:46 -0000	1.80
+++ usr.sbin/httpd/server_file.c	25 Feb 2026 09:14:50 -0000
@@ -55,6 +55,8 @@ int		 server_file_method(struct client *
 int		 parse_range_spec(char *, size_t, struct range *);
 int		 parse_ranges(struct client *, char *, size_t);
 static int	 select_visible(const struct dirent *);
+static int	 find_compressed_path(const struct client *, const char *,
+		    int *, struct stat *);
 
 int
 server_file_access(struct httpd *env, struct client *clt,
@@ -168,44 +170,10 @@ server_file_access(struct httpd *env, st
 			    fd, &st, r->kv_value));
 	}
 
-	/* change path to path.gz if necessary. */
-	if (srv_conf->flags & SRVFLAG_GZIP_STATIC) {
-		struct http_descriptor	*req = clt->clt_descreq;
-		struct http_descriptor	*resp = clt->clt_descresp;
-		struct stat		 gzst;
-		int			 gzfd;
-		char			 gzpath[PATH_MAX];
-
-		/* check Accept-Encoding header */
-		key.kv_key = "Accept-Encoding";
-		r = kv_find(&req->http_headers, &key);
-
-		if (r != NULL && strstr(r->kv_value, "gzip") != NULL) {
-			/* append ".gz" to path and check existence */
-			ret = snprintf(gzpath, sizeof(gzpath), "%s.gz", path);
-			if (ret < 0 || (size_t)ret >= sizeof(gzpath)) {
-				close(fd);
-				return (500);
-			}
-
-			if ((gzfd = open(gzpath, O_RDONLY)) != -1) {
-				/* .gz must be a file, and not older */
-				if (fstat(gzfd, &gzst) != -1 &&
-				    S_ISREG(gzst.st_mode) &&
-				    timespeccmp(&gzst.st_mtim, &st.st_mtim,
-				    >=)) {
-					kv_add(&resp->http_headers,
-					    "Content-Encoding", "gzip");
-					/* Use original file timestamp */
-					gzst.st_mtim = st.st_mtim;
-					st = gzst;
-					close(fd);
-					fd = gzfd;
-				} else {
-					close(gzfd);
-				}
-			}
-		}
+	/* Point fd and st at path.br or .gz if appropriate. */
+	if ((ret = find_compressed_path(clt, path, &fd, &st)) != 0) {
+		close(fd);
+		return (ret);
 	}
 
 	return (server_file_request(env, clt, media, fd, &st));
@@ -823,4 +791,110 @@ parse_range_spec(char *str, size_t size,
 		return (0);
 
 	return (1);
+}
+
+static int
+find_compressed_path(const struct client *clt, const char *path, int *fd,
+    struct stat *st)
+{
+	struct server_config	*srv_conf = clt->clt_srv_conf;
+	struct http_descriptor	*req = clt->clt_descreq;
+	struct http_descriptor	*resp = clt->clt_descresp;
+	struct stat		 brst;
+	struct stat		 gzst;
+	struct kv		*r, key;
+	int			 ret;
+	int			 brfd = -1;
+	int			 gzfd = -1;
+	char			 brpath[PATH_MAX];
+	char			 gzpath[PATH_MAX];
+
+	key.kv_key = "Accept-Encoding";
+	r = kv_find(&req->http_headers, &key);
+
+	/*
+	 * Look for path.br if brotli-static is set,
+	 * and the client accepts brotli, and the connection is inside TLS.
+	 */
+	if ((srv_conf->flags & SRVFLAG_BROTLI_STATIC) &&
+	    r != NULL &&
+	    strstr(r->kv_value, "br") != NULL &&
+	    clt->clt_tls_ctx != NULL) {
+		/* Append .br... */
+		ret = snprintf(brpath, sizeof(brpath), "%s.br", path);
+		if (ret < 0 || (size_t)ret >= sizeof(brpath)) {
+			return (500);
+		}
+		/* ...and check existence. */
+		if ((brfd = open(brpath, O_RDONLY)) != -1) {
+			if (fstat(brfd, &brst) == -1) {
+				close(brfd);
+				return (500);
+			}
+		}
+	}
+
+	/* Likewise for path.gz, minus TLS requirement. */
+	if ((srv_conf->flags & SRVFLAG_GZIP_STATIC) &&
+	    r != NULL &&
+	    strstr(r->kv_value, "gzip") != NULL) {
+		ret = snprintf(gzpath, sizeof(gzpath), "%s.gz", path);
+		if (ret < 0 || (size_t)ret >= sizeof(gzpath)) {
+			/* brfd might be open here. */
+			if (brfd != -1)
+				close(brfd);
+			return (500);
+		}
+		if ((gzfd = open(gzpath, O_RDONLY)) != -1) {
+			if (fstat(gzfd, &gzst) == -1) {
+				/* brfd might be open here. */
+				if (brfd != -1)
+					close(brfd);
+				close(gzfd);
+				return (500);
+			}
+		}
+	}
+
+	/*
+	 * Serve path.br if it's not older than the base file,
+	 * and (if path.gz also exists, than .br is not older than .gz).
+	 *
+	 * Otherwise if path.gz is not older than path, serve it.
+	 */
+	if (brfd != -1 &&
+	    S_ISREG(brst.st_mode) &&
+	    timespeccmp(&brst.st_mtim, &st->st_mtim, >=) &&
+	    (gzfd == -1 || timespeccmp(&brst.st_mtim, &gzst.st_mtim, >=)))
+	{
+		kv_add(&resp->http_headers,
+		    "Content-Encoding", "br");
+		/* Use original file timestamp. */
+		brst.st_mtim = st->st_mtim;
+		*st = brst;
+		close(*fd);
+		*fd = brfd;
+		brfd = -1;
+	} else if (gzfd != -1 &&
+	    S_ISREG(gzst.st_mode) &&
+	    timespeccmp(&gzst.st_mtim, &st->st_mtim, >=))
+	{
+		kv_add(&resp->http_headers,
+		    "Content-Encoding", "gzip");
+		/* Use original file timestamp. */
+		gzst.st_mtim = st->st_mtim;
+		*st = gzst;
+		close(*fd);
+		*fd = gzfd;
+		gzfd = -1;
+	}
+
+	/*
+	 * brfd and gzfd could both be open here if they
+	 * exist but are older than the uncompressed version.
+	 */
+	if (brfd != -1) close(brfd);
+	if (gzfd != -1) close(gzfd);
+
+	return (0);
 }