From: Adam Mullins Subject: Re: add support to httpd for serving static brotli encoded files To: Christian Schulte Cc: tech@openbsd.org, Lloyd , Crystal Kolipe Date: Wed, 25 Feb 2026 10:46:36 +0100 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 STRING %token NUMBER %type 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); }