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 2 Sep 2026 16:55:23 -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.169 httpd.h --- usr.sbin/httpd/httpd.h 2 Mar 2026 19:24:58 -0000 1.169 +++ usr.sbin/httpd/httpd.h 2 Sep 2026 16:55:23 -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.131 parse.y --- usr.sbin/httpd/parse.y 2 Mar 2026 19:24:58 -0000 1.131 +++ usr.sbin/httpd/parse.y 2 Sep 2026 16:55:23 -0000 @@ -142,7 +142,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 @@ -562,6 +562,7 @@ serveroptsl : LISTEN ON STRING opttls po | fastcgi | authenticate | gzip_static + | brotli_static | filter | LOCATION optfound optmatch STRING { struct server *s; @@ -1250,6 +1251,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 ; @@ -1456,6 +1465,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 2 Sep 2026 16:55:23 -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)); @@ -468,7 +436,7 @@ server_partial_file_request(struct httpd } /* ignore hidden files starting with a dot */ -static int +static int select_visible(const struct dirent *dp) { if (dp->d_name[0] == '.' && @@ -579,7 +547,7 @@ server_file_index(struct httpd *env, str "%s/\n" " %s%s\n", strchr(escapeduri, ':') != NULL ? "./" : "", - escapeduri, escapedhtml, + escapeduri, escapedhtml, (long long)t, tmstr, "-") == -1) skip = 1; } else if (S_ISREG(subst.st_mode)) { @@ -590,7 +558,7 @@ server_file_index(struct httpd *env, str "%s\n", strchr(escapeduri, ':') != NULL ? "./" : "", escapeduri, escapedhtml, - (long long)t, tmstr, + (long long)t, tmstr, subst.st_size, human_size) == -1)) skip = 1; } @@ -823,4 +791,109 @@ 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 then the base file + * by pointing *fd at brfd. + * + * Else serve path.gz if it's not older then path. + */ + if (brfd != -1 && + S_ISREG(brst.st_mode) && + timespeccmp(&brst.st_mtim, &st->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 then the uncompressed version. + */ + if (brfd != -1) close(brfd); + if (gzfd != -1) close(gzfd); + + return (0); }