Download raw body.
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);
}
add support to httpd for serving static brotli encoded files