Index: httpd.conf.5 =================================================================== RCS file: /cvs/src/usr.sbin/httpd/httpd.conf.5,v diff -u -p -u -r1.127 httpd.conf.5 --- httpd.conf.5 8 Jul 2025 14:26:45 -0000 1.127 +++ httpd.conf.5 5 Mar 2026 17:01:19 -0000 @@ -442,6 +442,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: httpd.h =================================================================== RCS file: /cvs/src/usr.sbin/httpd/httpd.h,v diff -u -p -u -r1.165 httpd.h --- httpd.h 8 Oct 2024 05:28:11 -0000 1.165 +++ httpd.h 5 Mar 2026 17:01:19 -0000 @@ -389,6 +389,7 @@ SPLAY_HEAD(client_tree, client); #define SRVFLAG_PATH_REWRITE 0x01000000 #define SRVFLAG_NO_PATH_REWRITE 0x02000000 #define SRVFLAG_GZIP_STATIC 0x04000000 +#define SRVFLAG_BROTLI_STATIC 0x10000000 #define SRVFLAG_LOCATION_FOUND 0x40000000 #define SRVFLAG_LOCATION_NOT_FOUND 0x80000000 @@ -398,7 +399,7 @@ 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\32NO_PATH" \ - "\37LOCATION_FOUND\40LOCATION_NOT_FOUND" + "\35BROTLI_STATIC\37LOCATION_FOUND\40LOCATION_NOT_FOUND" #define TCPFLAG_NODELAY 0x01 #define TCPFLAG_NNODELAY 0x02 Index: parse.y =================================================================== RCS file: /cvs/src/usr.sbin/httpd/parse.y,v diff -u -p -r1.128 parse.y --- parse.y 27 Feb 2022 20:30:30 -0000 1.128 +++ parse.y 5 Mar 2026 17:18:45 -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 +%token ERRDOCS GZIPSTATIC BROTLISTATIC %token STRING %token NUMBER %type port @@ -554,6 +554,7 @@ serveroptsl : LISTEN ON STRING opttls po | fastcgi | authenticate | gzip_static + | brotli_static | filter | LOCATION optfound optmatch STRING { struct server *s; @@ -1226,6 +1227,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 ; @@ -1430,6 +1439,7 @@ lookup(char *s) { "backlog", BACKLOG }, { "block", BLOCK }, { "body", BODY }, + { "brotli-static", BROTLISTATIC }, { "buffer", BUFFER }, { "ca", CA }, { "certificate", CERTIFICATE }, Index: server_file.c =================================================================== RCS file: /cvs/src/usr.sbin/httpd/server_file.c,v diff -u -p -u -r1.80 server_file.c --- server_file.c 29 Apr 2024 16:17:46 -0000 1.80 +++ server_file.c 5 Mar 2026 17:01:19 -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, then .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); }