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;
+}