Brotli: add support for Brotli compression. This commit adds initial versions of two modules: - ngx_brotli filter module - used to compress responses on-the-fly, - ngx_brotli static module - used to serve pre-compressed files. Signed-off-by: Piotr Sikora <piotrsikora@google.com>
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a9eabff --- /dev/null +++ b/CONTRIBUTING.md
@@ -0,0 +1,27 @@ +# Contributing + +Want to contribute? Great! First, read this page (including the small print at the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6003634 --- /dev/null +++ b/LICENSE
@@ -0,0 +1,27 @@ +/* + * Copyright (C) 2002-2015 Igor Sysoev + * Copyright (C) 2011-2015 Nginx, Inc. + * Copyright (C) 2015 Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */
diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbef5c9 --- /dev/null +++ b/README.md
@@ -0,0 +1,140 @@ +# ngx_brotli + +Brotli is a generic-purpose lossless compression algorithm that compresses data +using a combination of a modern variant of the LZ77 algorithm, Huffman coding +and 2nd order context modeling, with a compression ratio comparable to the best +currently available general-purpose compression methods. It is similar in speed +with deflate but offers more dense compression. + +ngx_brotli is a set of two nginx modules: + +- ngx_brotli filter module - used to compress responses on-the-fly, +- ngx_brotli static module - used to serve pre-compressed files. + +## Status + +Both Brotli library and nginx module are under active development. + +## Installation + + $ cd nginx-1.x.x + $ ./configure --add-module=/path/to/ngx_brotli + $ make && make install + +ngx_brotli filter module depends on +[libbroti](https://github.com/bagder/libbrotli), +which must be installed on the target system for it to work. + +Alternatively, ngx_brotli static module can be compiled into nginx by itself +with `NGX_BROTLI_STATIC_MODULE_ONLY=1` defined in the environment: + + $ cd nginx-1.x.x + $ export NGX_BROTLI_STATIC_MODULE_ONLY=1 + $ ./configure --add-module=/path/to/ngx_brotli + $ make && make install + +## Configuration directives + +### `brotli_static` + +- **syntax**: `brotli_static on|off|always` +- **default**: `off` +- **context**: `http`, `server`, `location` + +Enables or disables checking of the existence of pre-compressed files with`.br` +extension. With the `always` value, pre-compressed file is used in all cases, +without checking if the client supports it. + +### `brotli` + +- **syntax**: `brotli on|off` +- **default**: `off` +- **context**: `http`, `server`, `location`, `if` + +Enables or disables on-the-fly compression of responses. + +### `brotli_types` + +- **syntax**: `brotli_types <mime_type> [..]` +- **default**: `text/html` +- **context**: `http`, `server`, `location` + +Enables on-the-fly compression of responses for the specified MIME types +in addition to `text/html`. The special value `*` matches any MIME type. +Responses with the `text/html` MIME type are always compressed. + +### `brotli_buffers` + +- **syntax**: `brotli_buffers <number> <size>` +- **default**: `32 4k|16 8k` +- **context**: `http`, `server`, `location` + +Sets the `number` and `size` of buffers used to compress a response. +By default, the buffer size is equal to one memory page. +This is either `4k` or `8k`, depending on a platform. + +### `brotli_comp_level` + +- **syntax**: `brotli_comp_level <level>` +- **default**: `6` +- **context**: `http`, `server`, `location` + +Sets Brotli quality (compression) `level`. +Acceptable values are in the range from `0` to `11`. + +### `brotli_window` + +- **syntax**: `brotli_window <size>` +- **default**: `512k` +- **context**: `http`, `server`, `location` + +Sets Brotli window `size`. Acceptable values are `1k`, `2k`, `4k`, `8k`, `16k`, +`32k`, `64k`, `128k`, `256k`, `512k`, `1m`, `2m`, `4m`, `8m` and `16m`. + +### `brotli_min_length` + +- **syntax**: `brotli_min_length <length>` +- **default**: `20` +- **context**: `http`, `server`, `location` + +Sets the minimum `length` of a response that will be compressed. +The length is determined only from the `Content-Length` response header field. + +## Variables + +### `$brotli_ratio` + +Achieved compression ratio, computed as the ratio between the original +and compressed response sizes. + +## Contributing + +See [Contributing](CONTRIBUTING.md). + +## License + + Copyright (C) 2002-2015 Igor Sysoev + Copyright (C) 2011-2015 Nginx, Inc. + Copyright (C) 2015 Google Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE.
diff --git a/auto/feature b/auto/feature new file mode 100644 index 0000000..92294b7 --- /dev/null +++ b/auto/feature
@@ -0,0 +1,131 @@ + +# Copyright (C) Igor Sysoev +# Copyright (C) Nginx, Inc. + + +echo $ngx_n "checking for $ngx_feature ...$ngx_c" + +cat << END >> $NGX_AUTOCONF_ERR + +---------------------------------------- +checking for $ngx_feature + +END + +ngx_found=no + +if test -n "$ngx_feature_name"; then + ngx_have_feature=`echo $ngx_feature_name \ + | tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ` +fi + +if test -n "$ngx_feature_path"; then + for ngx_temp in $ngx_feature_path; do + ngx_feature_inc_path="$ngx_feature_inc_path -I $ngx_temp" + done +fi + +if [ "$ngx_feature_cpp" == "yes" ]; then + NGX_AUTOTEST_EXT=cc +else + NGX_AUTOTEST_EXT=c +fi + +cat << END > $NGX_AUTOTEST.$NGX_AUTOTEST_EXT + +#include <sys/types.h> +$NGX_INCLUDE_UNISTD_H +$ngx_feature_incs + +int main() { + $ngx_feature_test; + return 0; +} + +END + + +ngx_test="$CC $CC_TEST_FLAGS $CC_AUX_FLAGS $ngx_feature_inc_path \ + -o $NGX_AUTOTEST $NGX_AUTOTEST.$NGX_AUTOTEST_EXT \ + $NGX_TEST_LD_OPT $ngx_feature_libs" + +ngx_feature_inc_path= +ngx_feature_cpp= + +eval "/bin/sh -c \"$ngx_test\" >> $NGX_AUTOCONF_ERR 2>&1" + + +if [ -x $NGX_AUTOTEST ]; then + + case "$ngx_feature_run" in + + yes) + # /bin/sh is used to intercept "Killed" or "Abort trap" messages + if /bin/sh -c $NGX_AUTOTEST >> $NGX_AUTOCONF_ERR 2>&1; then + echo " found" + ngx_found=yes + + if test -n "$ngx_feature_name"; then + have=$ngx_have_feature . auto/have + fi + + else + echo " found but is not working" + fi + ;; + + value) + # /bin/sh is used to intercept "Killed" or "Abort trap" messages + if /bin/sh -c $NGX_AUTOTEST >> $NGX_AUTOCONF_ERR 2>&1; then + echo " found" + ngx_found=yes + + cat << END >> $NGX_AUTO_CONFIG_H + +#ifndef $ngx_feature_name +#define $ngx_feature_name `$NGX_AUTOTEST` +#endif + +END + else + echo " found but is not working" + fi + ;; + + bug) + # /bin/sh is used to intercept "Killed" or "Abort trap" messages + if /bin/sh -c $NGX_AUTOTEST >> $NGX_AUTOCONF_ERR 2>&1; then + echo " not found" + + else + echo " found" + ngx_found=yes + + if test -n "$ngx_feature_name"; then + have=$ngx_have_feature . auto/have + fi + fi + ;; + + *) + echo " found" + ngx_found=yes + + if test -n "$ngx_feature_name"; then + have=$ngx_have_feature . auto/have + fi + ;; + + esac + +else + echo " not found" + + echo "----------" >> $NGX_AUTOCONF_ERR + cat $NGX_AUTOTEST.$NGX_AUTOTEST_EXT >> $NGX_AUTOCONF_ERR + echo "----------" >> $NGX_AUTOCONF_ERR + echo $ngx_test >> $NGX_AUTOCONF_ERR + echo "----------" >> $NGX_AUTOCONF_ERR +fi + +rm -rf $NGX_AUTOTEST*
diff --git a/config b/config new file mode 100644 index 0000000..aea0e9f --- /dev/null +++ b/config
@@ -0,0 +1,114 @@ +# Copyright (C) 2015 Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +ngx_addon_name=ngx_brotli + +# +# static module (no dependencies) +# + +HTTP_MODULES="$HTTP_MODULES \ + ngx_http_brotli_static_module" + +NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ + $ngx_addon_dir/src/ngx_http_brotli_static_module.c" + +have=NGX_HTTP_GZIP . auto/have +have=NGX_HTTP_BROTLI_STATIC_MODULE . auto/have + +if [ -n "$NGX_BROTLI_STATIC_MODULE_ONLY" ]; then + return +fi + +# +# filter module (depends on Brotli library) +# + +ngx_feature_run=no +ngx_feature_cpp=yes +ngx_feature_incs="#include <brotli/enc/encode.h>" +ngx_feature_test="brotli::BrotliParams params; + params.quality = 11" + +# auto-discovery +ngx_feature="Brotli library" +ngx_feature_cpp=yes +ngx_feature_path= +ngx_feature_libs="-lbrotlienc -lstdc++" +. $ngx_addon_dir/auto/feature + +if [ $ngx_found = no ]; then + # FreeBSD, OpenBSD + ngx_feature="Brotli library in /usr/local/" + ngx_feature_cpp=yes + ngx_feature_path="/usr/local/include" + if [ $NGX_RPATH = YES ]; then + ngx_feature_libs="-R/usr/local/lib -L/usr/local/lib -lbrotlienc -lstdc++" + else + ngx_feature_libs="-L/usr/local/lib -lbrotlienc -lstdc++" + fi + . $ngx_addon_dir/auto/feature +fi + +if [ $ngx_found = no ]; then + # NetBSD + ngx_feature="Brotli library in /usr/pkg/" + ngx_feature_cpp=yes + ngx_feature_path="/usr/pkg/include" + if [ $NGX_RPATH = YES ]; then + ngx_feature_libs="-R/usr/pkg/lib -L/usr/pkg/lib -lbrotlienc -lstdc++" + else + ngx_feature_libs="-L/usr/pkg/lib -lbrotlienc -lstdc++" + fi + . $ngx_addon_dir/auto/feature +fi + +if [ $ngx_found = no ]; then + # MacPorts + ngx_feature="Brotli library in /opt/local/" + ngx_feature_cpp=yes + ngx_feature_path="/opt/local/include" + if [ $NGX_RPATH = YES ]; then + ngx_feature_libs="-R/opt/local/lib -L/opt/local/lib -lbrotlienc -lstdc++" + else + ngx_feature_libs="-L/opt/local/lib -lbrotlienc -lstdc++" + fi + . $ngx_addon_dir/auto/feature +fi + +if [ $ngx_found = no ]; then + echo "$0: error: ngx_brotli filter module requires Brotli library." + exit 1 +fi + +CORE_INCS="$CORE_INCS $ngx_feature_path" +CORE_LIBS="$CORE_LIBS $ngx_feature_libs" + +HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES \ + ngx_http_brotli_filter_module" + +NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ + $ngx_addon_dir/src/ngx_http_brotli_filter_module.cc" + +have=NGX_HTTP_BROTLI_FILTER_MODULE . auto/have
diff --git a/src/ngx_http_brotli_filter_module.cc b/src/ngx_http_brotli_filter_module.cc new file mode 100644 index 0000000..1202d75 --- /dev/null +++ b/src/ngx_http_brotli_filter_module.cc
@@ -0,0 +1,831 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) Nginx, Inc. + * Copyright (C) Google Inc. + */ + + +extern "C" { + #include <ngx_config.h> + #include <ngx_core.h> + #include <ngx_http.h> +} + +#include <brotli/enc/encode.h> + + +typedef struct { + ngx_flag_t enable; + + ngx_hash_t types; + + ngx_bufs_t bufs; + + ngx_int_t quality; + size_t win_bits; + ssize_t min_length; + + ngx_array_t *types_keys; +} ngx_http_brotli_conf_t; + + +typedef struct { + brotli::BrotliCompressor *compressor; + brotli::BrotliParams params; + + size_t brotli_ring; + size_t brotli_in; + u_char *brotli_out; + u_char *brotli_last; + + size_t bytes_in; + size_t bytes_out; + + ngx_chain_t *in; + ngx_chain_t *free; + ngx_chain_t *busy; + ngx_chain_t *out; + ngx_chain_t **last_out; + + ngx_buf_t *out_buf; + ngx_int_t bufs; + + unsigned done:1; + unsigned sent:1; + unsigned last:1; + unsigned flush:1; + unsigned nomem:1; +} ngx_http_brotli_ctx_t; + + +static void ngx_http_brotli_filter_params(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx); +static ngx_int_t ngx_http_brotli_filter_add_data(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx); +static ngx_int_t ngx_http_brotli_filter_process(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx); +static ngx_int_t ngx_http_brotli_filter_output(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx); +static ngx_int_t ngx_http_brotli_filter_get_buf(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx); + +static ngx_int_t ngx_http_brotli_ok(ngx_http_request_t *r); +static ngx_int_t ngx_http_brotli_accept_encoding(ngx_str_t *ae); + +static ngx_int_t ngx_http_brotli_add_variables(ngx_conf_t *cf); +static ngx_int_t ngx_http_brotli_ratio_variable(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); + +static void *ngx_http_brotli_create_conf(ngx_conf_t *cf); +static char *ngx_http_brotli_merge_conf(ngx_conf_t *cf, + void *parent, void *child); +static ngx_int_t ngx_http_brotli_filter_init(ngx_conf_t *cf); + +static char *ngx_http_brotli_window(ngx_conf_t *cf, void *post, void *data); + + +static ngx_conf_num_bounds_t ngx_http_brotli_comp_level_bounds = { + ngx_conf_check_num_bounds, 0, 11 +}; + +static ngx_conf_post_handler_pt ngx_http_brotli_window_p = + ngx_http_brotli_window; + + +static ngx_command_t ngx_http_brotli_filter_commands[] = { + + { ngx_string("brotli"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF + |NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_conf_t, enable), + NULL }, + + { ngx_string("brotli_buffers"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2, + ngx_conf_set_bufs_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_conf_t, bufs), + NULL }, + + { ngx_string("brotli_types"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, + ngx_http_types_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_conf_t, types_keys), + &ngx_http_html_default_types[0] }, + + { ngx_string("brotli_comp_level"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_conf_t, quality), + &ngx_http_brotli_comp_level_bounds }, + + { ngx_string("brotli_window"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_conf_t, win_bits), + &ngx_http_brotli_window_p }, + + { ngx_string("brotli_min_length"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_conf_t, min_length), + NULL }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_http_brotli_filter_module_ctx = { + ngx_http_brotli_add_variables, /* preconfiguration */ + ngx_http_brotli_filter_init, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_http_brotli_create_conf, /* create location configuration */ + ngx_http_brotli_merge_conf /* merge location configuration */ +}; + + +ngx_module_t ngx_http_brotli_filter_module = { + NGX_MODULE_V1, + &ngx_http_brotli_filter_module_ctx, /* module context */ + ngx_http_brotli_filter_commands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static ngx_str_t ngx_http_brotli_ratio = ngx_string("brotli_ratio"); + +static ngx_http_output_header_filter_pt ngx_http_next_header_filter; +static ngx_http_output_body_filter_pt ngx_http_next_body_filter; + + +static ngx_int_t +ngx_http_brotli_header_filter(ngx_http_request_t *r) +{ + ngx_table_elt_t *h; + ngx_http_brotli_ctx_t *ctx; + ngx_http_brotli_conf_t *conf; + + conf = reinterpret_cast<ngx_http_brotli_conf_t *>( + ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module)); + + if (!conf->enable + || (r->headers_out.status != NGX_HTTP_OK + && r->headers_out.status != NGX_HTTP_FORBIDDEN + && r->headers_out.status != NGX_HTTP_NOT_FOUND) + || (r->headers_out.content_encoding + && r->headers_out.content_encoding->value.len) + || (r->headers_out.content_length_n != -1 + && r->headers_out.content_length_n < conf->min_length) + || ngx_http_test_content_type(r, &conf->types) == NULL + || r->header_only) + { + return ngx_http_next_header_filter(r); + } + + r->gzip_vary = 1; + + if (ngx_http_brotli_ok(r) != NGX_OK) { + return ngx_http_next_header_filter(r); + } + + ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>( + ngx_pcalloc(r->pool, sizeof(ngx_http_brotli_ctx_t))); + if (ctx == NULL) { + return NGX_ERROR; + } + + ctx->last_out = &ctx->out; + + ngx_http_brotli_filter_params(r, ctx); + + ngx_http_set_ctx(r, ctx, ngx_http_brotli_filter_module); + + h = reinterpret_cast<ngx_table_elt_t *>( + ngx_list_push(&r->headers_out.headers)); + if (h == NULL) { + return NGX_ERROR; + } + + h->hash = 1; + ngx_str_set(&h->key, "Content-Encoding"); + ngx_str_set(&h->value, "br"); + r->headers_out.content_encoding = h; + + r->main_filter_need_in_memory = 1; + + ngx_http_clear_content_length(r); + ngx_http_clear_accept_ranges(r); + ngx_http_weak_etag(r); + + return ngx_http_next_header_filter(r); +} + + +static ngx_int_t +ngx_http_brotli_body_filter(ngx_http_request_t *r, ngx_chain_t *in) +{ + int rc; + ngx_uint_t flush; + ngx_chain_t *cl; + ngx_http_brotli_ctx_t *ctx; + + ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>( + ngx_http_get_module_ctx(r, ngx_http_brotli_filter_module)); + + if (ctx == NULL || ctx->done || r->header_only) { + return ngx_http_next_body_filter(r, in); + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http brotli filter"); + + if (ctx->compressor == NULL) { + ctx->compressor = new brotli::BrotliCompressor(ctx->params); + if (ctx->compressor == NULL) { + goto failed; + } + + ctx->brotli_ring = ctx->compressor->input_block_size(); + + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "brotli compressor: lvl:%d win:%d blk:%uz", + ctx->params.quality, (1 << ctx->params.lgwin), + ctx->brotli_ring); + } + + if (in) { + if (ngx_chain_add_copy(r->pool, &ctx->in, in) != NGX_OK) { + goto failed; + } + + r->connection->buffered |= NGX_HTTP_GZIP_BUFFERED; + } + + if (ctx->nomem) { + + /* flush busy buffers */ + + if (ngx_http_next_body_filter(r, NULL) == NGX_ERROR) { + goto failed; + } + + cl = NULL; + + ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &cl, + (ngx_buf_tag_t) &ngx_http_brotli_filter_module); + ctx->nomem = 0; + flush = 0; + + } else { + flush = ctx->busy ? 1 : 0; + } + + for ( ;; ) { + + /* cycle while we can write to a client */ + + for ( ;; ) { + + rc = ngx_http_brotli_filter_add_data(r, ctx); + + if (rc == NGX_DECLINED) { + break; + } + + if (rc == NGX_AGAIN) { + continue; + } + + rc = ngx_http_brotli_filter_process(r, ctx); + + if (rc == NGX_AGAIN) { + continue; + } + + if (rc == NGX_ERROR) { + goto failed; + } + + rc = ngx_http_brotli_filter_output(r, ctx); + + if (rc == NGX_OK) { + break; + } + + if (rc == NGX_ERROR) { + goto failed; + } + + /* rc == NGX_AGAIN */ + } + + if (ctx->out == NULL && !flush) { + return ctx->busy ? NGX_AGAIN : NGX_OK; + } + + rc = ngx_http_next_body_filter(r, ctx->out); + + if (rc == NGX_ERROR) { + goto failed; + } + + ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &ctx->out, + (ngx_buf_tag_t) &ngx_http_brotli_filter_module); + ctx->last_out = &ctx->out; + + ctx->nomem = 0; + flush = 0; + + if (ctx->done) { + ctx->sent = 1; + + if (ctx->compressor) { + delete ctx->compressor; + ctx->compressor = NULL; + } + + return rc; + } + } + + /* unreachable */ + +failed: + + ctx->done = 1; + + if (ctx->compressor) { + delete ctx->compressor; + ctx->compressor = NULL; + } + + return NGX_ERROR; +} + + +static void +ngx_http_brotli_filter_params(ngx_http_request_t *r, ngx_http_brotli_ctx_t *ctx) +{ + int wbits; + ngx_http_brotli_conf_t *conf; + + conf = reinterpret_cast<ngx_http_brotli_conf_t *>( + ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module)); + + wbits = conf->win_bits; + + if (r->headers_out.content_length_n > 0) { + while (r->headers_out.content_length_n < (1 << (wbits - 1)) + && wbits > brotli::kMinWindowBits) + { + wbits--; + } + } + + ctx->params.quality = conf->quality; + ctx->params.lgwin = wbits; +} + + +static ngx_int_t +ngx_http_brotli_filter_add_data(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx) +{ + size_t size, ring; + ngx_buf_t *b; + + if (ctx->brotli_out || ctx->last || ctx->flush) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "brotli in: %p", ctx->in); + + if (ctx->in == NULL) { + return NGX_DECLINED; + } + + b = ctx->in->buf; + + size = ngx_buf_size(b); + ring = ctx->brotli_ring - ctx->brotli_in; + + if (size > ring) { + size = ring; + + } else { + if (b->last_buf) { + ctx->last = 1; + + } else if (b->flush) { + ctx->flush = 1; + } + } + + if (size == ring) { + ctx->flush = 1; + } + + if (size > 0) { + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "brotli copy: %p, size:%uz", b, size); + + ctx->compressor->CopyInputToRingBuffer(size, b->pos); + + ctx->brotli_in += size; + ctx->bytes_in += size; + b->pos += size; + } + + if (ngx_buf_size(b) == 0) { + ctx->in = ctx->in->next; + size = 0; + } + + if (size == 0 && !ctx->flush && !ctx->last) { + return NGX_AGAIN; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_brotli_filter_process(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx) +{ + size_t size; + u_char *out; + + if (ctx->brotli_out) { + return NGX_OK; + } + + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "brotli process: size:%uz l:%d f:%d", + ctx->brotli_in, ctx->last, ctx->flush); + + if (!ctx->compressor->WriteBrotliData(ctx->last, ctx->flush, &size, &out)) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "brotli failed: size:%uz l:%d f:%d", + ctx->brotli_in, ctx->last, ctx->flush); + return NGX_ERROR; + } + + ctx->brotli_in = 0; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "brotli data: %p, size:%uz", out, size); + + if (size == 0) { + return NGX_AGAIN; + } + + ctx->brotli_out = out; + ctx->brotli_last = out + size; + ctx->bytes_out += size; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_brotli_filter_output(ngx_http_request_t *r, ngx_http_brotli_ctx_t *ctx) +{ + int rc; + size_t size; + ngx_chain_t *cl; + + if (ctx->brotli_out == NULL) { + return NGX_AGAIN; + } + +#if (NGX_SUPPRESS_WARN) + cl = NULL; +#endif + + while (ctx->brotli_out < ctx->brotli_last) { + + rc = ngx_http_brotli_filter_get_buf(r, ctx); + + if (rc == NGX_DECLINED) { + return NGX_OK; + } + + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + size = ngx_min(ctx->out_buf->end - ctx->out_buf->last, + ctx->brotli_last - ctx->brotli_out); + + ngx_memcpy(ctx->out_buf->last, ctx->brotli_out, size); + + ctx->out_buf->last += size; + ctx->brotli_out += size; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "brotli out: %p, size:%uz", + ctx->out_buf, ngx_buf_size(ctx->out_buf)); + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + cl->buf = ctx->out_buf; + cl->next = NULL; + *ctx->last_out = cl; + ctx->last_out = &cl->next; + } + + ctx->brotli_out = NULL; + ctx->brotli_last = NULL; + + if (ctx->last || ctx->flush) { + + if (ctx->last) { + ctx->done = 1; + cl->buf->last_buf = 1; + + } else if (ctx->flush) { + ctx->flush = 0; + cl->buf->flush = 1; + } + + r->connection->buffered &= ~NGX_HTTP_GZIP_BUFFERED; + + return NGX_OK; + } + + return NGX_AGAIN; +} + + +static ngx_int_t +ngx_http_brotli_filter_get_buf(ngx_http_request_t *r, + ngx_http_brotli_ctx_t *ctx) +{ + ngx_http_brotli_conf_t *conf; + + if (ctx->free) { + ctx->out_buf = ctx->free->buf; + ctx->free = ctx->free->next; + return NGX_OK; + } + + conf = reinterpret_cast<ngx_http_brotli_conf_t *>( + ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module)); + + if (ctx->bufs < conf->bufs.num) { + ctx->out_buf = ngx_create_temp_buf(r->pool, conf->bufs.size); + if (ctx->out_buf == NULL) { + return NGX_ERROR; + } + + ctx->out_buf->tag = (ngx_buf_tag_t) &ngx_http_brotli_filter_module; + ctx->out_buf->recycled = 1; + ctx->bufs++; + + } else { + ctx->nomem = 1; + return NGX_DECLINED; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_brotli_ok(ngx_http_request_t *r) +{ + ngx_table_elt_t *ae; + + if (r != r->main) { + return NGX_DECLINED; + } + + ae = r->headers_in.accept_encoding; + if (ae == NULL) { + return NGX_DECLINED; + } + + if (ae->value.len < sizeof("br") - 1) { + return NGX_DECLINED; + } + + if (ngx_memcmp(ae->value.data, "br,", sizeof("br,") - 1) != 0 + && ngx_http_brotli_accept_encoding(&ae->value) != NGX_OK) + { + return NGX_DECLINED; + } + + r->gzip_tested = 1; + r->gzip_ok = 0; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_brotli_accept_encoding(ngx_str_t *ae) +{ + u_char *p; + + p = ngx_strcasestrn(ae->data, const_cast<char *>("br"), sizeof("br") - 2); + if (p == NULL) { + return NGX_DECLINED; + } + + if (p == ae->data || (*(p - 1) == ',' || *(p - 1) == ' ')) { + + p += sizeof("br") - 1; + + if (p == ae->data + ae->len || *p == ',' || *p == ';' || *p == ' ') { + return NGX_OK; + } + } + + return NGX_DECLINED; +} + + +static ngx_int_t +ngx_http_brotli_add_variables(ngx_conf_t *cf) +{ + ngx_http_variable_t *var; + + var = ngx_http_add_variable(cf, &ngx_http_brotli_ratio, 0); + if (var == NULL) { + return NGX_ERROR; + } + + var->get_handler = ngx_http_brotli_ratio_variable; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_brotli_ratio_variable(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data) +{ + ngx_uint_t ratio_int, ratio_frac; + ngx_http_brotli_ctx_t *ctx; + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>( + ngx_http_get_module_ctx(r, ngx_http_brotli_filter_module)); + + if (ctx == NULL || ctx->bytes_out == 0 || !ctx->sent) { + v->not_found = 1; + return NGX_OK; + } + + v->data = reinterpret_cast<u_char *>( + ngx_pnalloc(r->pool, NGX_INT32_LEN + 3)); + if (v->data == NULL) { + return NGX_ERROR; + } + + ratio_int = (ngx_uint_t) (ctx->bytes_in / ctx->bytes_out); + ratio_frac = (ngx_uint_t) ((ctx->bytes_in * 100 / ctx->bytes_out) % 100); + + if ((ctx->bytes_in * 1000 / ctx->bytes_out) % 10 > 4) { + + /* the rounding, e.g., 2.125 to 2.13 */ + + ratio_frac++; + + if (ratio_frac > 99) { + ratio_int++; + ratio_frac = 0; + } + } + + v->len = ngx_sprintf(v->data, "%ui.%02ui", ratio_int, ratio_frac) - v->data; + + return NGX_OK; +} + + +static void * +ngx_http_brotli_create_conf(ngx_conf_t *cf) +{ + ngx_http_brotli_conf_t *conf; + + conf = reinterpret_cast<ngx_http_brotli_conf_t *>( + ngx_pcalloc(cf->pool, sizeof(ngx_http_brotli_conf_t))); + if (conf == NULL) { + return NULL; + } + + /* + * set by ngx_pcalloc(): + * + * conf->bufs.num = 0; + * conf->types = { NULL }; + * conf->types_keys = NULL; + */ + + conf->enable = NGX_CONF_UNSET; + + conf->quality = NGX_CONF_UNSET; + conf->win_bits = NGX_CONF_UNSET_SIZE; + conf->min_length = NGX_CONF_UNSET; + + return conf; +} + + +static char * +ngx_http_brotli_merge_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_http_brotli_conf_t *prev; + ngx_http_brotli_conf_t *conf; + + prev = reinterpret_cast<ngx_http_brotli_conf_t *>(parent); + conf = reinterpret_cast<ngx_http_brotli_conf_t *>(child); + + ngx_conf_merge_value(conf->enable, prev->enable, 0); + + ngx_conf_merge_bufs_value(conf->bufs, prev->bufs, + (128 * 1024) / ngx_pagesize, ngx_pagesize); + + ngx_conf_merge_value(conf->quality, prev->quality, 6); + ngx_conf_merge_size_value(conf->win_bits, prev->win_bits, 19); + ngx_conf_merge_value(conf->min_length, prev->min_length, 20); + + if (ngx_http_merge_types(cf, &conf->types_keys, &conf->types, + &prev->types_keys, &prev->types, + ngx_http_html_default_types) + != NGX_OK) + { + return reinterpret_cast<char *>(NGX_CONF_ERROR); + } + + return reinterpret_cast<char *>(NGX_CONF_OK); +} + + +static ngx_int_t +ngx_http_brotli_filter_init(ngx_conf_t *cf) +{ + ngx_http_next_header_filter = ngx_http_top_header_filter; + ngx_http_top_header_filter = ngx_http_brotli_header_filter; + + ngx_http_next_body_filter = ngx_http_top_body_filter; + ngx_http_top_body_filter = ngx_http_brotli_body_filter; + + return NGX_OK; +} + + +static char * +ngx_http_brotli_window(ngx_conf_t *cf, void *post, void *data) +{ + size_t *np = reinterpret_cast<size_t *>(data); + + size_t wbits, wsize, mbits; + + wbits = brotli::kMaxWindowBits; + mbits = brotli::kMinWindowBits; + + for (wsize = (1 << wbits); wsize >= (1U << mbits); wsize >>= 1) { + + if (wsize == *np) { + *np = wbits; + + return reinterpret_cast<char *>(NGX_CONF_OK); + } + + wbits--; + } + + return const_cast<char *>("must be 1k, 2k, 4k, 8k, 16k, 32k, 64k, " + "128k, 256k, 512k, 1m, 2m, 4m, 8m or 16m"); +}
diff --git a/src/ngx_http_brotli_static_module.c b/src/ngx_http_brotli_static_module.c new file mode 100644 index 0000000..15b5bb8 --- /dev/null +++ b/src/ngx_http_brotli_static_module.c
@@ -0,0 +1,389 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) Nginx, Inc. + * Copyright (C) Google Inc. + */ + + +#include <ngx_config.h> +#include <ngx_core.h> +#include <ngx_http.h> + + +#define NGX_HTTP_BROTLI_STATIC_OFF 0 +#define NGX_HTTP_BROTLI_STATIC_ON 1 +#define NGX_HTTP_BROTLI_STATIC_ALWAYS 2 + + +typedef struct { + ngx_uint_t enable; +} ngx_http_brotli_static_conf_t; + + +static ngx_int_t ngx_http_brotli_static_handler(ngx_http_request_t *r); + +static ngx_int_t ngx_http_brotli_ok(ngx_http_request_t *r); +static ngx_int_t ngx_http_brotli_accept_encoding(ngx_str_t *ae); + +static void *ngx_http_brotli_static_create_conf(ngx_conf_t *cf); +static char *ngx_http_brotli_static_merge_conf(ngx_conf_t *cf, + void *parent, void *child); +static ngx_int_t ngx_http_brotli_static_init(ngx_conf_t *cf); + + +static ngx_conf_enum_t ngx_http_brotli_static[] = { + { ngx_string("off"), NGX_HTTP_BROTLI_STATIC_OFF }, + { ngx_string("on"), NGX_HTTP_BROTLI_STATIC_ON }, + { ngx_string("always"), NGX_HTTP_BROTLI_STATIC_ALWAYS }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_http_brotli_static_commands[] = { + + { ngx_string("brotli_static"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_brotli_static_conf_t, enable), + &ngx_http_brotli_static }, + + ngx_null_command +}; + + +ngx_http_module_t ngx_http_brotli_static_module_ctx = { + NULL, /* preconfiguration */ + ngx_http_brotli_static_init, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_http_brotli_static_create_conf, /* create location configuration */ + ngx_http_brotli_static_merge_conf /* merge location configuration */ +}; + + +ngx_module_t ngx_http_brotli_static_module = { + NGX_MODULE_V1, + &ngx_http_brotli_static_module_ctx, /* module context */ + ngx_http_brotli_static_commands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static ngx_int_t +ngx_http_brotli_static_handler(ngx_http_request_t *r) +{ + u_char *p; + size_t root; + ngx_str_t path; + ngx_int_t rc; + ngx_uint_t level; + ngx_log_t *log; + ngx_buf_t *b; + ngx_chain_t out; + ngx_table_elt_t *h; + ngx_open_file_info_t of; + ngx_http_core_loc_conf_t *clcf; + ngx_http_brotli_static_conf_t *bscf; + + if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) { + return NGX_DECLINED; + } + + if (r->uri.data[r->uri.len - 1] == '/') { + return NGX_DECLINED; + } + + bscf = ngx_http_get_module_loc_conf(r, ngx_http_brotli_static_module); + + if (bscf->enable == NGX_HTTP_BROTLI_STATIC_OFF) { + return NGX_DECLINED; + } + + if (bscf->enable == NGX_HTTP_BROTLI_STATIC_ON) { + rc = ngx_http_brotli_ok(r); + + } else { /* NGX_HTTP_BROTLI_STATIC_ALWAYS */ + rc = NGX_OK; + } + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + if (!clcf->gzip_vary && rc != NGX_OK) { + return NGX_DECLINED; + } + + log = r->connection->log; + + p = ngx_http_map_uri_to_path(r, &path, &root, sizeof(".br") - 1); + if (p == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + *p++ = '.'; + *p++ = 'b'; + *p++ = 'r'; + *p = '\0'; + + path.len = p - path.data; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, + "http filename: \"%s\"", path.data); + + ngx_memzero(&of, sizeof(ngx_open_file_info_t)); + + of.read_ahead = clcf->read_ahead; + of.directio = clcf->directio; + of.valid = clcf->open_file_cache_valid; + of.min_uses = clcf->open_file_cache_min_uses; + of.errors = clcf->open_file_cache_errors; + of.events = clcf->open_file_cache_events; + + if (ngx_http_set_disable_symlinks(r, clcf, &path, &of) != NGX_OK) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool) + != NGX_OK) + { + switch (of.err) { + + case 0: + return NGX_HTTP_INTERNAL_SERVER_ERROR; + + case NGX_ENOENT: + case NGX_ENOTDIR: + case NGX_ENAMETOOLONG: + + return NGX_DECLINED; + + case NGX_EACCES: +#if (NGX_HAVE_OPENAT) + case NGX_EMLINK: + case NGX_ELOOP: +#endif + + level = NGX_LOG_ERR; + break; + + default: + + level = NGX_LOG_CRIT; + break; + } + + ngx_log_error(level, log, of.err, + "%s \"%s\" failed", of.failed, path.data); + + return NGX_DECLINED; + } + + if (bscf->enable == NGX_HTTP_BROTLI_STATIC_ON) { + r->gzip_vary = 1; + + if (rc != NGX_OK) { + return NGX_DECLINED; + } + } + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "http static fd: %d", of.fd); + + if (of.is_dir) { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, "http dir"); + return NGX_DECLINED; + } + +#if !(NGX_WIN32) /* the not regular files are probably Unix specific */ + + if (!of.is_file) { + ngx_log_error(NGX_LOG_CRIT, log, 0, + "\"%s\" is not a regular file", path.data); + + return NGX_HTTP_NOT_FOUND; + } + +#endif + + r->root_tested = !r->error_page; + + rc = ngx_http_discard_request_body(r); + + if (rc != NGX_OK) { + return rc; + } + + log->action = "sending response to client"; + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = of.size; + r->headers_out.last_modified_time = of.mtime; + + if (ngx_http_set_etag(r) != NGX_OK) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (ngx_http_set_content_type(r) != NGX_OK) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + h->hash = 1; + ngx_str_set(&h->key, "Content-Encoding"); + ngx_str_set(&h->value, "br"); + r->headers_out.content_encoding = h; + + /* we need to allocate all before the header would be sent */ + + b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); + if (b == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t)); + if (b->file == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + rc = ngx_http_send_header(r); + + if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { + return rc; + } + + b->file_pos = 0; + b->file_last = of.size; + + b->in_file = b->file_last ? 1 : 0; + b->last_buf = (r == r->main) ? 1 : 0; + b->last_in_chain = 1; + + b->file->fd = of.fd; + b->file->name = path; + b->file->log = log; + b->file->directio = of.is_directio; + + out.buf = b; + out.next = NULL; + + return ngx_http_output_filter(r, &out); +} + + +static ngx_int_t +ngx_http_brotli_ok(ngx_http_request_t *r) +{ + ngx_table_elt_t *ae; + + if (r != r->main) { + return NGX_DECLINED; + } + + ae = r->headers_in.accept_encoding; + if (ae == NULL) { + return NGX_DECLINED; + } + + if (ae->value.len < sizeof("br") - 1) { + return NGX_DECLINED; + } + + if (ngx_memcmp(ae->value.data, "br,", sizeof("br,") - 1) != 0 + && ngx_http_brotli_accept_encoding(&ae->value) != NGX_OK) + { + return NGX_DECLINED; + } + + r->gzip_tested = 1; + r->gzip_ok = 0; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_brotli_accept_encoding(ngx_str_t *ae) +{ + u_char *p; + + p = ngx_strcasestrn(ae->data, "br", sizeof("br") - 2); + if (p == NULL) { + return NGX_DECLINED; + } + + if (p == ae->data || (*(p - 1) == ',' || *(p - 1) == ' ')) { + + p += sizeof("br") - 1; + + if (p == ae->data + ae->len || *p == ',' || *p == ';' || *p == ' ') { + return NGX_OK; + } + } + + return NGX_DECLINED; +} + + +static void * +ngx_http_brotli_static_create_conf(ngx_conf_t *cf) +{ + ngx_http_brotli_static_conf_t *conf; + + conf = ngx_palloc(cf->pool, sizeof(ngx_http_brotli_static_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->enable = NGX_CONF_UNSET_UINT; + + return conf; +} + + +static char * +ngx_http_brotli_static_merge_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_http_brotli_static_conf_t *prev = parent; + ngx_http_brotli_static_conf_t *conf = child; + + ngx_conf_merge_uint_value(conf->enable, prev->enable, + NGX_HTTP_BROTLI_STATIC_OFF); + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_http_brotli_static_init(ngx_conf_t *cf) +{ + ngx_http_handler_pt *h; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_brotli_static_handler; + + return NGX_OK; +}