├── .gitmodules ├── script ├── test_h2.conf ├── .travis-before-test.sh ├── test.conf ├── .travis-compile.sh └── .travis-test.sh ├── .travis.yml ├── LICENSE ├── CONTRIBUTING.md ├── config ├── static ├── config └── ngx_http_brotli_static_module.c ├── filter ├── config └── ngx_http_brotli_filter_module.c └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/brotli"] 2 | path = deps/brotli 3 | url = https://github.com/google/brotli.git 4 | -------------------------------------------------------------------------------- /script/test_h2.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4; 3 | } 4 | 5 | daemon on; 6 | error_log /dev/stdout info; 7 | 8 | http { 9 | access_log ./access.log; 10 | error_log ./error.log; 11 | 12 | gzip on; 13 | gzip_comp_level 1; 14 | gzip_types text/plain text/css; 15 | 16 | brotli on; 17 | brotli_comp_level 1; 18 | brotli_types text/plain text/css; 19 | 20 | server { 21 | listen 8080 http2; 22 | listen [::]:8080 http2; 23 | 24 | root ./; 25 | 26 | index index.html; 27 | 28 | location / { 29 | try_files $uri $uri/ =404; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /script/.travis-before-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # Setup shortcuts. 5 | ROOT=`pwd` 6 | FILES=$ROOT/script/test 7 | 8 | # Setup directory structure. 9 | cd $ROOT/script 10 | if [ ! -d test ]; then 11 | mkdir test 12 | fi 13 | cd test 14 | if [ ! -d logs ]; then 15 | mkdir logs 16 | fi 17 | 18 | # Download sample texts. 19 | curl --compressed -o $FILES/war-and-peace.txt http://www.gutenberg.org/files/2600/2600-0.txt 20 | echo "Kot lomom kolol slona!" > $FILES/small.txt 21 | echo "Kot lomom kolol slona!" > $FILES/small.html 22 | 23 | # Restore status-quo. 24 | cd $ROOT 25 | -------------------------------------------------------------------------------- /script/test.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4; 3 | } 4 | 5 | daemon on; 6 | error_log /dev/stdout info; 7 | 8 | http { 9 | access_log ./access.log; 10 | error_log ./error.log; 11 | 12 | gzip on; 13 | gzip_comp_level 1; 14 | gzip_types text/plain text/css; 15 | 16 | brotli on; 17 | brotli_comp_level 1; 18 | brotli_types text/plain text/css; 19 | 20 | server { 21 | listen 8080 default_server; 22 | listen [::]:8080 default_server; 23 | 24 | root ./; 25 | 26 | index index.html; 27 | 28 | location / { 29 | try_files $uri $uri/ =404; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /script/.travis-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # Setup shortcuts. 5 | ROOT=`pwd` 6 | 7 | # Clone nginx read-only git repository. 8 | if [ ! -d "nginx" ]; then 9 | git clone https://github.com/nginx/nginx.git 10 | fi 11 | 12 | # Build nginx + filter module. 13 | cd $ROOT/nginx 14 | # Pro memoria: --with-debug 15 | ./auto/configure \ 16 | --prefix=$ROOT/script/test \ 17 | --with-http_v2_module \ 18 | --add-module=$ROOT 19 | make -j 16 20 | 21 | # Build brotli CLI. 22 | cd $ROOT/deps/brotli 23 | mkdir out 24 | cd out 25 | cmake .. 26 | make -j 16 brotli 27 | 28 | # Restore status-quo. 29 | cd $ROOT 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # required for http2 support in curl. 2 | dist: bionic 3 | language: c 4 | sudo: false 5 | matrix: 6 | include: 7 | # unfortunately, gcc-4.9 is dropped in bionic 8 | - os: linux 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-5 15 | env: 16 | - MATRIX_EVAL="CC=gcc-5 && CXX=g++-5" 17 | 18 | - os: linux 19 | addons: 20 | apt: 21 | sources: 22 | - ubuntu-toolchain-r-test 23 | packages: 24 | - g++-6 25 | env: 26 | - MATRIX_EVAL="CC=gcc-6 && CXX=g++-6" 27 | - os: linux 28 | addons: 29 | apt: 30 | sources: 31 | - ubuntu-toolchain-r-test 32 | packages: 33 | - g++-7 34 | env: 35 | - MATRIX_EVAL="CC=gcc-7 && CXX=g++-7" 36 | 37 | 38 | script: 39 | - script/.travis-compile.sh 40 | - script/.travis-before-test.sh 41 | - script/.travis-test.sh 42 | after_success: 43 | - killall nginx 44 | after_failure: 45 | - killall nginx 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2002-2015 Igor Sysoev 3 | * Copyright (C) 2011-2015 Nginx, Inc. 4 | * Copyright (C) 2015-2019 Google Inc. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 1. Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | * SUCH DAMAGE. 27 | */ 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Want to contribute? Great! First, read this page (including the small print at the end). 4 | 5 | ### Before you contribute 6 | Before we can use your code, you must sign the 7 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 8 | (CLA), which you can do online. The CLA is necessary mainly because you own the 9 | copyright to your changes, even after your contribution becomes part of our 10 | codebase, so we need your permission to use and distribute your code. We also 11 | need to be sure of various other things—for instance that you'll tell us if you 12 | know that your code infringes on other people's patents. You don't have to sign 13 | the CLA until after you've submitted your code for review and a member has 14 | approved it, but you must do it before we can put your code into our codebase. 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. We 22 | use Github pull requests for this purpose. 23 | 24 | ### The small print 25 | Contributions made by corporations are covered by a different agreement than 26 | the one above, the 27 | [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). 28 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | 25 | # Make sure the module knows it is a submodule. 26 | ngx_addon_name=ngx_brotli 27 | . $ngx_addon_dir/filter/config 28 | 29 | # Make sure the module knows it is a submodule. 30 | ngx_addon_name=ngx_brotli 31 | . $ngx_addon_dir/static/config 32 | 33 | # The final name for reporting. 34 | ngx_addon_name=ngx_brotli 35 | -------------------------------------------------------------------------------- /static/config: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2019 Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | 25 | if [ "$ngx_addon_name" = "ngx_brotli" ]; then 26 | BROTLI_MODULE_SRC_DIR="$ngx_addon_dir/static" 27 | else 28 | BROTLI_MODULE_SRC_DIR="$ngx_addon_dir" 29 | fi 30 | 31 | ngx_addon_name=ngx_brotli_static 32 | 33 | if [ -z "$ngx_module_link" ]; then 34 | cat << END 35 | 36 | $0: error: Brotli module requires recent version of NGINX (1.9.11+). 37 | 38 | END 39 | exit 1 40 | fi 41 | 42 | ngx_module_type=HTTP 43 | ngx_module_name=ngx_http_brotli_static_module 44 | ngx_module_incs= 45 | ngx_module_deps= 46 | ngx_module_srcs="$BROTLI_MODULE_SRC_DIR/ngx_http_brotli_static_module.c" 47 | ngx_module_libs= 48 | ngx_module_order= 49 | 50 | . auto/module 51 | 52 | have=NGX_HTTP_GZIP . auto/have 53 | have=NGX_HTTP_BROTLI_STATIC . auto/have 54 | have=NGX_HTTP_BROTLI_STATIC_MODULE . auto/have # deprecated 55 | -------------------------------------------------------------------------------- /filter/config: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2016 Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | 25 | if [ "$ngx_addon_name" = "ngx_brotli" ]; then 26 | BROTLI_MODULE_SRC_DIR="$ngx_addon_dir/filter" 27 | else 28 | BROTLI_MODULE_SRC_DIR="$ngx_addon_dir" 29 | fi 30 | 31 | ngx_addon_name=ngx_brotli_filter 32 | 33 | if [ -z "$ngx_module_link" ]; then 34 | cat << END 35 | 36 | $0: error: Brotli module requires recent version of NGINX (1.9.11+). 37 | 38 | END 39 | exit 1 40 | fi 41 | 42 | ngx_module_type=HTTP_FILTER 43 | ngx_module_name=ngx_http_brotli_filter_module 44 | 45 | brotli="$ngx_addon_dir/deps/brotli/c" 46 | if [ ! -f "$brotli/include/brotli/encode.h" ]; then 47 | brotli="/usr/local" 48 | fi 49 | if [ ! -f "$brotli/include/brotli/encode.h" ]; then 50 | brotli="/usr" 51 | fi 52 | if [ ! -f "$brotli/include/brotli/encode.h" ]; then 53 | cat << END 54 | 55 | $0: error: \ 56 | Brotli library is missing from the $brotli directory. 57 | 58 | Please make sure that the git submodule has been checked out: 59 | 60 | cd $ngx_addon_dir && git submodule update --init && cd $PWD 61 | 62 | END 63 | exit 1 64 | fi 65 | 66 | BROTLI_LISTS_FILE="$brotli/../scripts/sources.lst" 67 | 68 | if [ -f "$BROTLI_LISTS_FILE" ]; then 69 | 70 | BROTLI_LISTS=`cat "$BROTLI_LISTS_FILE" | grep -v "#" | tr '\n' '#' | \ 71 | sed 's/\\\\#//g' | tr -s ' ' '+' | tr -s '#' ' ' | \ 72 | sed 's/+c/+$brotli/g' | sed 's/+=+/=/g'` 73 | for ITEM in ${BROTLI_LISTS}; do 74 | VAR=`echo $ITEM | sed 's/=.*//'` 75 | VAL=`echo $ITEM | sed 's/.*=//' | tr '+' ' '` 76 | eval ${VAR}=\"$VAL\" 77 | done 78 | 79 | else # BROTLI_LISTS_FILE 80 | 81 | BROTLI_ENC_H="$brotli/include/brotli/encode.h \ 82 | $brotli/include/brotli/port.h \ 83 | $brotli/include/brotli/types.h" 84 | BROTLI_ENC_LIB="-lbrotlienc" 85 | 86 | fi 87 | 88 | ngx_module_incs="$brotli/include" 89 | ngx_module_deps="$BROTLI_COMMON_H $BROTLI_ENC_H" 90 | ngx_module_srcs="$BROTLI_COMMON_C $BROTLI_ENC_C \ 91 | $BROTLI_MODULE_SRC_DIR/ngx_http_brotli_filter_module.c" 92 | ngx_module_libs="$BROTLI_ENC_LIB -lm" 93 | ngx_module_order="$ngx_module_name \ 94 | ngx_pagespeed \ 95 | ngx_http_postpone_filter_module \ 96 | ngx_http_ssi_filter_module \ 97 | ngx_http_charset_filter_module \ 98 | ngx_http_xslt_filter_module \ 99 | ngx_http_image_filter_module \ 100 | ngx_http_sub_filter_module \ 101 | ngx_http_addition_filter_module \ 102 | ngx_http_gunzip_filter_module \ 103 | ngx_http_userid_filter_module \ 104 | ngx_http_headers_filter_module \ 105 | ngx_http_copy_filter_module \ 106 | ngx_http_range_body_filter_module \ 107 | ngx_http_not_modified_filter_module \ 108 | ngx_http_slice_filter_module" 109 | 110 | . auto/module 111 | 112 | if [ "$ngx_module_link" != DYNAMIC ]; then 113 | # ngx_module_order doesn't work with static modules, 114 | # so we must re-order filters here. 115 | 116 | if [ "$HTTP_GZIP" = YES ]; then 117 | next=ngx_http_gzip_filter_module 118 | elif echo $HTTP_FILTER_MODULES | grep pagespeed_etag_filter >/dev/null; then 119 | next=ngx_pagespeed_etag_filter 120 | else 121 | next=ngx_http_range_header_filter_module 122 | fi 123 | 124 | HTTP_FILTER_MODULES=`echo $HTTP_FILTER_MODULES \ 125 | | sed "s/$ngx_module_name//" \ 126 | | sed "s/$next/$next $ngx_module_name/"` 127 | fi 128 | 129 | CFLAGS="$CFLAGS -Wno-deprecated-declarations" 130 | 131 | have=NGX_HTTP_BROTLI_FILTER . auto/have 132 | have=NGX_HTTP_BROTLI_FILTER_MODULE . auto/have # deprecated 133 | -------------------------------------------------------------------------------- /script/.travis-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup shortcuts. 4 | ROOT=`pwd` 5 | NGINX=$ROOT/nginx/objs/nginx 6 | BROTLI=$ROOT/deps/brotli/out/brotli 7 | SERVER=http://localhost:8080 8 | FILES=$ROOT/script/test 9 | HR="---------------------------------------------------------------------------" 10 | 11 | if [ ! -d tmp ]; then 12 | mkdir tmp 13 | fi 14 | 15 | rm tmp/* 16 | 17 | add_result() { 18 | echo $1 >&2 19 | echo $1 >> tmp/results.log 20 | } 21 | 22 | get_failed() { 23 | echo `cat tmp/results.log | grep -v OK | wc -l` 24 | } 25 | 26 | get_count() { 27 | echo `cat tmp/results.log | wc -l` 28 | } 29 | 30 | expect_equal() { 31 | expected=$1 32 | actual=$2 33 | if cmp $expected $actual; then 34 | add_result "OK" 35 | else 36 | add_result "FAIL (equality)" 37 | fi 38 | } 39 | 40 | expect_br_equal() { 41 | expected=$1 42 | actual_br=$2 43 | if $BROTLI -dfk ./${actual_br}.br; then 44 | expect_equal $expected $actual_br 45 | else 46 | add_result "FAIL (decompression)" 47 | fi 48 | } 49 | 50 | ################################################################################ 51 | 52 | # Start default server. 53 | echo "Statring NGINX" 54 | $NGINX -c $ROOT/script/test.conf 55 | # Fetch vanilla 404 response. 56 | curl -s -o tmp/notfound.txt $SERVER/notfound 57 | 58 | CURL="curl -s" 59 | 60 | # Run tests. 61 | echo $HR 62 | 63 | echo "Test: long file with rate limit" 64 | $CURL -H 'Accept-encoding: br' -o tmp/war-and-peace.br --limit-rate 300K $SERVER/war-and-peace.txt 65 | expect_br_equal $FILES/war-and-peace.txt tmp/war-and-peace 66 | 67 | echo "Test: compressed 404" 68 | $CURL -H 'Accept-encoding: br' -o tmp/notfound.br $SERVER/notfound 69 | expect_br_equal tmp/notfound.txt tmp/notfound 70 | 71 | echo "Test: A-E: 'gzip, br'" 72 | $CURL -H 'Accept-encoding: gzip, br' -o tmp/ae-01.br $SERVER/small.txt 73 | expect_br_equal $FILES/small.txt tmp/ae-01 74 | 75 | echo "Test: A-E: 'gzip, br, deflate'" 76 | $CURL -H 'Accept-encoding: gzip, br, deflate' -o tmp/ae-02.br $SERVER/small.txt 77 | expect_br_equal $FILES/small.txt tmp/ae-02 78 | 79 | echo "Test: A-E: 'gzip, br;q=1, deflate'" 80 | $CURL -H 'Accept-encoding: gzip, br;q=1, deflate' -o tmp/ae-03.br $SERVER/small.txt 81 | expect_br_equal $FILES/small.txt tmp/ae-03 82 | 83 | echo "Test: A-E: 'br;q=0.001'" 84 | $CURL -H 'Accept-encoding: br;q=0.001' -o tmp/ae-04.br $SERVER/small.txt 85 | expect_br_equal $FILES/small.txt tmp/ae-04 86 | 87 | echo "Test: A-E: 'bro'" 88 | $CURL -H 'Accept-encoding: bro' -o tmp/ae-05.txt $SERVER/small.txt 89 | expect_equal $FILES/small.txt tmp/ae-05.txt 90 | 91 | echo "Test: A-E: 'bo'" 92 | $CURL -H 'Accept-encoding: bo' -o tmp/ae-06.txt $SERVER/small.txt 93 | expect_equal $FILES/small.txt tmp/ae-06.txt 94 | 95 | echo "Test: A-E: 'br;q=0'" 96 | $CURL -H 'Accept-encoding: br;q=0' -o tmp/ae-07.txt $SERVER/small.txt 97 | expect_equal $FILES/small.txt tmp/ae-07.txt 98 | 99 | echo "Test: A-E: 'br;q=0.'" 100 | $CURL -H 'Accept-encoding: br;q=0.' -o tmp/ae-08.txt $SERVER/small.txt 101 | expect_equal $FILES/small.txt tmp/ae-08.txt 102 | 103 | echo "Test: A-E: 'br;q=0.0'" 104 | $CURL -H 'Accept-encoding: br;q=0.0' -o tmp/ae-09.txt $SERVER/small.txt 105 | expect_equal $FILES/small.txt tmp/ae-09.txt 106 | 107 | echo "Test: A-E: 'br;q=0.00'" 108 | $CURL -H 'Accept-encoding: br;q=0.00' -o tmp/ae-10.txt $SERVER/small.txt 109 | expect_equal $FILES/small.txt tmp/ae-10.txt 110 | 111 | echo "Test: A-E: 'br ; q = 0.000'" 112 | $CURL -H 'Accept-encoding: br ; q = 0.000' -o tmp/ae-11.txt $SERVER/small.txt 113 | expect_equal $FILES/small.txt tmp/ae-11.txt 114 | 115 | echo "Test: A-E: 'bar'" 116 | $CURL -H 'Accept-encoding: bar' -o tmp/ae-12.txt $SERVER/small.html 117 | expect_equal $FILES/small.html tmp/ae-12.txt 118 | 119 | echo "Test: A-E: 'b'" 120 | $CURL -H 'Accept-encoding: b' -o tmp/ae-13.txt $SERVER/small.html 121 | expect_equal $FILES/small.html tmp/ae-13.txt 122 | 123 | echo $HR 124 | echo "Stopping default NGINX" 125 | # Stop server. 126 | $NGINX -c $ROOT/script/test.conf -s stop 127 | 128 | ################################################################################ 129 | 130 | # Start default server. 131 | echo "Statring h2 NGINX" 132 | $NGINX -c $ROOT/script/test_h2.conf 133 | 134 | CURL="curl --http2-prior-knowledge -s" 135 | 136 | # Run tests. 137 | echo $HR 138 | 139 | echo "Test: long file with rate limit" 140 | $CURL -H 'Accept-encoding: br' -o tmp/h2-war-and-peace.br --limit-rate 300K $SERVER/war-and-peace.txt 141 | expect_br_equal $FILES/war-and-peace.txt tmp/h2-war-and-peace 142 | 143 | echo "Test: A-E: 'gzip, br'" 144 | $CURL -H 'Accept-encoding: gzip, br' -o tmp/h2-ae-01.br $SERVER/small.txt 145 | expect_br_equal $FILES/small.txt tmp/h2-ae-01 146 | 147 | echo "Test: A-E: 'b'" 148 | $CURL -H 'Accept-encoding: b' -o tmp/h2-ae-13.txt $SERVER/small.html 149 | expect_equal $FILES/small.html tmp/h2-ae-13.txt 150 | 151 | echo $HR 152 | echo "Stopping h2 NGINX" 153 | # Stop server. 154 | $NGINX -c $ROOT/script/test_h2.conf -s stop 155 | 156 | ################################################################################ 157 | 158 | # Report. 159 | 160 | FAILED=$(get_failed $STATUS) 161 | COUNT=$(get_count $STATUS) 162 | echo $HR 163 | echo "Results: $FAILED of $COUNT tests failed" 164 | 165 | # Restore status-quo. 166 | cd $ROOT 167 | 168 | exit $FAILED 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx_brotli 2 | 3 | Brotli is a generic-purpose lossless compression algorithm that compresses data 4 | using a combination of a modern variant of the LZ77 algorithm, Huffman coding 5 | and 2nd order context modeling, with a compression ratio comparable to the best 6 | currently available general-purpose compression methods. It is similar in speed 7 | with deflate but offers more dense compression. 8 | 9 | ngx_brotli is a set of two nginx modules: 10 | 11 | - ngx_brotli filter module - used to compress responses on-the-fly, 12 | - ngx_brotli static module - used to serve pre-compressed files. 13 | 14 | [![TravisCI Build Status](https://travis-ci.org/google/ngx_brotli.svg?branch=master)](https://travis-ci.org/google/ngx_brotli) 15 | 16 | ## Table of Contents 17 | 18 | - [Status](#status) 19 | - [Installation](#installation) 20 | - [Configuration directives](#configuration-directives) 21 | - [`brotli_static`](#brotli_static) 22 | - [`brotli`](#brotli) 23 | - [`brotli_types`](#brotli_types) 24 | - [`brotli_buffers`](#brotli_buffers) 25 | - [`brotli_comp_level`](#brotli_comp_level) 26 | - [`brotli_window`](#brotli_window) 27 | - [`brotli_min_length`](#brotli_min_length) 28 | - [Variables](#variables) 29 | - [`$brotli_ratio`](#brotli_ratio) 30 | - [Contributing](#contributing) 31 | - [License](#license) 32 | 33 | ## Status 34 | 35 | Both Brotli library and nginx module are under active development. 36 | 37 | ## Installation 38 | 39 | ### Dynamically loaded 40 | 41 | $ cd nginx-1.x.x 42 | $ ./configure --with-compat --add-dynamic-module=/path/to/ngx_brotli 43 | $ make modules 44 | 45 | You will need to use **exactly** the same `./configure` arguments as your Nginx configuration and append `--with-compat --add-dynamic-module=/path/to/ngx_brotli` to the end, otherwise you will get a "module is not binary compatible" error on startup. You can run `nginx -V` to get the configuration arguments for your Nginx installation. 46 | 47 | `make modules` will result in `ngx_http_brotli_filter_module.so` and `ngx_http_brotli_static_module.so` in the `objs` directory. Copy these to `/usr/lib/nginx/modules/` then add the `load_module` lines above to `nginx.conf`. 48 | 49 | ### Statically compiled 50 | 51 | $ cd nginx-1.x.x 52 | $ ./configure --add-module=/path/to/ngx_brotli 53 | $ make && make install 54 | 55 | This will compile the module directly into Nginx. 56 | 57 | ## Configuration directives 58 | 59 | ### `brotli_static` 60 | 61 | - **syntax**: `brotli_static on|off|always` 62 | - **default**: `off` 63 | - **context**: `http`, `server`, `location` 64 | 65 | Enables or disables checking of the existence of pre-compressed files with`.br` 66 | extension. With the `always` value, pre-compressed file is used in all cases, 67 | without checking if the client supports it. 68 | 69 | ### `brotli` 70 | 71 | - **syntax**: `brotli on|off` 72 | - **default**: `off` 73 | - **context**: `http`, `server`, `location`, `if` 74 | 75 | Enables or disables on-the-fly compression of responses. 76 | 77 | ### `brotli_types` 78 | 79 | - **syntax**: `brotli_types [..]` 80 | - **default**: `text/html` 81 | - **context**: `http`, `server`, `location` 82 | 83 | Enables on-the-fly compression of responses for the specified MIME types 84 | in addition to `text/html`. The special value `*` matches any MIME type. 85 | Responses with the `text/html` MIME type are always compressed. 86 | 87 | ### `brotli_buffers` 88 | 89 | - **syntax**: `brotli_buffers ` 90 | - **default**: `32 4k|16 8k` 91 | - **context**: `http`, `server`, `location` 92 | 93 | **Deprecated**, ignored. 94 | 95 | ### `brotli_comp_level` 96 | 97 | - **syntax**: `brotli_comp_level ` 98 | - **default**: `6` 99 | - **context**: `http`, `server`, `location` 100 | 101 | Sets on-the-fly compression Brotli quality (compression) `level`. 102 | Acceptable values are in the range from `0` to `11`. 103 | 104 | ### `brotli_window` 105 | 106 | - **syntax**: `brotli_window ` 107 | - **default**: `512k` 108 | - **context**: `http`, `server`, `location` 109 | 110 | Sets Brotli window `size`. Acceptable values are `1k`, `2k`, `4k`, `8k`, `16k`, 111 | `32k`, `64k`, `128k`, `256k`, `512k`, `1m`, `2m`, `4m`, `8m` and `16m`. 112 | 113 | ### `brotli_min_length` 114 | 115 | - **syntax**: `brotli_min_length ` 116 | - **default**: `20` 117 | - **context**: `http`, `server`, `location` 118 | 119 | Sets the minimum `length` of a response that will be compressed. 120 | The length is determined only from the `Content-Length` response header field. 121 | 122 | ## Variables 123 | 124 | ### `$brotli_ratio` 125 | 126 | Achieved compression ratio, computed as the ratio between the original 127 | and compressed response sizes. 128 | 129 | ## Contributing 130 | 131 | See [Contributing](CONTRIBUTING.md). 132 | 133 | ## License 134 | 135 | Copyright (C) 2002-2015 Igor Sysoev 136 | Copyright (C) 2011-2015 Nginx, Inc. 137 | Copyright (C) 2015 Google Inc. 138 | All rights reserved. 139 | 140 | Redistribution and use in source and binary forms, with or without 141 | modification, are permitted provided that the following conditions 142 | are met: 143 | 1. Redistributions of source code must retain the above copyright 144 | notice, this list of conditions and the following disclaimer. 145 | 2. Redistributions in binary form must reproduce the above copyright 146 | notice, this list of conditions and the following disclaimer in the 147 | documentation and/or other materials provided with the distribution. 148 | 149 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 150 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 151 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 152 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 153 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 154 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 155 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 156 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 157 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 158 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 159 | SUCH DAMAGE. 160 | -------------------------------------------------------------------------------- /static/ngx_http_brotli_static_module.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (C) Igor Sysoev 4 | * Copyright (C) Nginx, Inc. 5 | * Copyright (C) Google Inc. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | /* >> Configuration */ 13 | 14 | #define NGX_HTTP_BROTLI_STATIC_OFF 0 15 | #define NGX_HTTP_BROTLI_STATIC_ON 1 16 | #define NGX_HTTP_BROTLI_STATIC_ALWAYS 2 17 | 18 | typedef struct { 19 | ngx_uint_t enable; 20 | } configuration_t; 21 | 22 | static ngx_conf_enum_t kBrotliStaticEnum[] = { 23 | {ngx_string("off"), NGX_HTTP_BROTLI_STATIC_OFF}, 24 | {ngx_string("on"), NGX_HTTP_BROTLI_STATIC_ON}, 25 | {ngx_string("always"), NGX_HTTP_BROTLI_STATIC_ALWAYS}, 26 | {ngx_null_string, 0}}; 27 | 28 | /* << Configuration */ 29 | 30 | /* >> Forward declarations */ 31 | 32 | static ngx_int_t handler(ngx_http_request_t* req); 33 | static void* create_conf(ngx_conf_t* root_cfg); 34 | static char* merge_conf(ngx_conf_t* root_cfg, void* parent, void* child); 35 | static ngx_int_t init(ngx_conf_t* root_cfg); 36 | 37 | /* << Forward declarations*/ 38 | 39 | /* >> Module definition */ 40 | 41 | static ngx_command_t kCommands[] = { 42 | {ngx_string("brotli_static"), 43 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 44 | NGX_CONF_TAKE1, 45 | ngx_conf_set_enum_slot, NGX_HTTP_LOC_CONF_OFFSET, 46 | offsetof(configuration_t, enable), &kBrotliStaticEnum}, 47 | ngx_null_command}; 48 | 49 | static ngx_http_module_t kModuleContext = { 50 | NULL, /* preconfiguration */ 51 | init, /* postconfiguration */ 52 | 53 | NULL, /* create main configuration */ 54 | NULL, /* init main configuration */ 55 | 56 | NULL, /* create server configuration */ 57 | NULL, /* merge server configuration */ 58 | 59 | create_conf, /* create location configuration */ 60 | merge_conf /* merge location configuration */ 61 | }; 62 | 63 | ngx_module_t ngx_http_brotli_static_module = { 64 | NGX_MODULE_V1, 65 | &kModuleContext, /* module context */ 66 | kCommands, /* module directives */ 67 | NGX_HTTP_MODULE, /* module type */ 68 | NULL, /* init master */ 69 | NULL, /* init module */ 70 | NULL, /* init process */ 71 | NULL, /* init thread */ 72 | NULL, /* exit thread */ 73 | NULL, /* exit process */ 74 | NULL, /* exit master */ 75 | NGX_MODULE_V1_PADDING}; 76 | 77 | /* << Module definition*/ 78 | 79 | static const u_char kContentEncoding[] = "Content-Encoding"; 80 | static /* const */ char kEncoding[] = "br"; 81 | static const size_t kEncodingLen = 2; 82 | static /* const */ u_char kSuffix[] = ".br"; 83 | static const size_t kSuffixLen = 3; 84 | 85 | static ngx_int_t check_accept_encoding(ngx_http_request_t* req) { 86 | ngx_table_elt_t* accept_encoding_entry; 87 | ngx_str_t* accept_encoding; 88 | u_char* cursor; 89 | u_char* end; 90 | u_char before; 91 | u_char after; 92 | 93 | accept_encoding_entry = req->headers_in.accept_encoding; 94 | if (accept_encoding_entry == NULL) return NGX_DECLINED; 95 | accept_encoding = &accept_encoding_entry->value; 96 | 97 | cursor = accept_encoding->data; 98 | end = cursor + accept_encoding->len; 99 | while (1) { 100 | u_char digit; 101 | /* It would be an idiotic idea to rely on compiler to produce performant 102 | binary, that is why we just do -1 at every call site. */ 103 | cursor = ngx_strcasestrn(cursor, kEncoding, kEncodingLen - 1); 104 | if (cursor == NULL) return NGX_DECLINED; 105 | before = (cursor == accept_encoding->data) ? ' ' : cursor[-1]; 106 | cursor += kEncodingLen; 107 | after = (cursor >= end) ? ' ' : *cursor; 108 | if (before != ',' && before != ' ') continue; 109 | if (after != ',' && after != ' ' && after != ';') continue; 110 | 111 | /* Check for ";q=0[.[0[0[0]]]]" */ 112 | while (*cursor == ' ') cursor++; 113 | if (*(cursor++) != ';') break; 114 | while (*cursor == ' ') cursor++; 115 | if (*(cursor++) != 'q') break; 116 | while (*cursor == ' ') cursor++; 117 | if (*(cursor++) != '=') break; 118 | while (*cursor == ' ') cursor++; 119 | if (*(cursor++) != '0') break; 120 | if (*(cursor++) != '.') return NGX_DECLINED; /* ;q=0, */ 121 | digit = *(cursor++); 122 | if (digit < '0' || digit > '9') return NGX_DECLINED; /* ;q=0., */ 123 | if (digit > '0') break; 124 | digit = *(cursor++); 125 | if (digit < '0' || digit > '9') return NGX_DECLINED; /* ;q=0.0, */ 126 | if (digit > '0') break; 127 | digit = *(cursor++); 128 | if (digit < '0' || digit > '9') return NGX_DECLINED; /* ;q=0.00, */ 129 | if (digit > '0') break; 130 | return NGX_DECLINED; /* ;q=0.000 */ 131 | } 132 | return NGX_OK; 133 | } 134 | 135 | /* Test if this request is allowed to have the brotli response. */ 136 | static ngx_int_t check_eligility(ngx_http_request_t* req) { 137 | if (req != req->main) return NGX_DECLINED; 138 | if (check_accept_encoding(req) != NGX_OK) return NGX_DECLINED; 139 | req->gzip_tested = 1; 140 | req->gzip_ok = 0; 141 | return NGX_OK; 142 | } 143 | 144 | static ngx_int_t handler(ngx_http_request_t* req) { 145 | configuration_t* cfg; 146 | ngx_int_t rc; 147 | u_char* last; 148 | ngx_str_t path; 149 | size_t root; 150 | ngx_log_t* log; 151 | ngx_http_core_loc_conf_t* location_cfg; 152 | ngx_open_file_info_t file_info; 153 | ngx_table_elt_t* content_encoding_entry; 154 | ngx_buf_t* buf; 155 | ngx_chain_t out; 156 | 157 | /* Only GET and HEAD requensts are supported. */ 158 | if (!(req->method & (NGX_HTTP_GET | NGX_HTTP_HEAD))) return NGX_DECLINED; 159 | 160 | /* Only files are supported. */ 161 | if (req->uri.data[req->uri.len - 1] == '/') return NGX_DECLINED; 162 | 163 | /* Get configuration and check if module is disabled. */ 164 | cfg = ngx_http_get_module_loc_conf(req, ngx_http_brotli_static_module); 165 | if (cfg->enable == NGX_HTTP_BROTLI_STATIC_OFF) return NGX_DECLINED; 166 | 167 | if (cfg->enable == NGX_HTTP_BROTLI_STATIC_ALWAYS) { 168 | /* Ignore request properties (e.g. Accept-Encoding). */ 169 | } else { 170 | /* NGX_HTTP_BROTLI_STATIC_ON */ 171 | req->gzip_vary = 1; 172 | rc = check_eligility(req); 173 | if (rc != NGX_OK) return NGX_DECLINED; 174 | } 175 | 176 | /* Get path and append the suffix. */ 177 | last = ngx_http_map_uri_to_path(req, &path, &root, kSuffixLen); 178 | if (last == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR; 179 | /* +1 for reinstating the terminating 0. */ 180 | ngx_cpystrn(last, kSuffix, kSuffixLen + 1); 181 | path.len += kSuffixLen; 182 | 183 | log = req->connection->log; 184 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "http filename: \"%s\"", 185 | path.data); 186 | 187 | /* Prepare to read the file. */ 188 | location_cfg = ngx_http_get_module_loc_conf(req, ngx_http_core_module); 189 | ngx_memzero(&file_info, sizeof(ngx_open_file_info_t)); 190 | file_info.read_ahead = location_cfg->read_ahead; 191 | file_info.directio = location_cfg->directio; 192 | file_info.valid = location_cfg->open_file_cache_valid; 193 | file_info.min_uses = location_cfg->open_file_cache_min_uses; 194 | file_info.errors = location_cfg->open_file_cache_errors; 195 | file_info.events = location_cfg->open_file_cache_events; 196 | rc = ngx_http_set_disable_symlinks(req, location_cfg, &path, &file_info); 197 | if (rc != NGX_OK) return NGX_HTTP_INTERNAL_SERVER_ERROR; 198 | 199 | /* Try to fetch file and process errors. */ 200 | rc = ngx_open_cached_file(location_cfg->open_file_cache, &path, &file_info, 201 | req->pool); 202 | if (rc != NGX_OK) { 203 | ngx_uint_t level; 204 | switch (file_info.err) { 205 | case 0: 206 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 207 | 208 | case NGX_ENOENT: 209 | case NGX_ENOTDIR: 210 | case NGX_ENAMETOOLONG: 211 | return NGX_DECLINED; 212 | 213 | #if (NGX_HAVE_OPENAT) 214 | case NGX_EMLINK: 215 | case NGX_ELOOP: 216 | #endif 217 | case NGX_EACCES: 218 | level = NGX_LOG_ERR; 219 | break; 220 | 221 | default: 222 | level = NGX_LOG_CRIT; 223 | break; 224 | } 225 | ngx_log_error(level, log, file_info.err, "%s \"%s\" failed", 226 | file_info.failed, path.data); 227 | return NGX_DECLINED; 228 | } 229 | 230 | /* So far so good. */ 231 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "http static fd: %d", 232 | file_info.fd); 233 | 234 | /* Only files are supported. */ 235 | if (file_info.is_dir) { 236 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, "http dir"); 237 | return NGX_DECLINED; 238 | } 239 | #if !(NGX_WIN32) 240 | if (!file_info.is_file) { 241 | ngx_log_error(NGX_LOG_CRIT, log, 0, "\"%s\" is not a regular file", 242 | path.data); 243 | return NGX_HTTP_NOT_FOUND; 244 | } 245 | #endif 246 | 247 | /* Prepare request push the body. */ 248 | req->root_tested = !req->error_page; 249 | rc = ngx_http_discard_request_body(req); 250 | if (rc != NGX_OK) return rc; 251 | log->action = "sending response to client"; 252 | req->headers_out.status = NGX_HTTP_OK; 253 | req->headers_out.content_length_n = file_info.size; 254 | req->headers_out.last_modified_time = file_info.mtime; 255 | rc = ngx_http_set_etag(req); 256 | if (rc != NGX_OK) return NGX_HTTP_INTERNAL_SERVER_ERROR; 257 | rc = ngx_http_set_content_type(req); 258 | if (rc != NGX_OK) return NGX_HTTP_INTERNAL_SERVER_ERROR; 259 | 260 | /* Set "Content-Encoding" header. */ 261 | content_encoding_entry = ngx_list_push(&req->headers_out.headers); 262 | if (content_encoding_entry == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR; 263 | content_encoding_entry->hash = 1; 264 | ngx_str_set(&content_encoding_entry->key, kContentEncoding); 265 | ngx_str_set(&content_encoding_entry->value, kEncoding); 266 | req->headers_out.content_encoding = content_encoding_entry; 267 | 268 | /* Setup response body. */ 269 | buf = ngx_pcalloc(req->pool, sizeof(ngx_buf_t)); 270 | if (buf == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR; 271 | buf->file = ngx_pcalloc(req->pool, sizeof(ngx_file_t)); 272 | if (buf->file == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR; 273 | buf->file_pos = 0; 274 | buf->file_last = file_info.size; 275 | buf->in_file = buf->file_last ? 1 : 0; 276 | buf->last_buf = (req == req->main) ? 1 : 0; 277 | buf->last_in_chain = 1; 278 | buf->file->fd = file_info.fd; 279 | buf->file->name = path; 280 | buf->file->log = log; 281 | buf->file->directio = file_info.is_directio; 282 | out.buf = buf; 283 | out.next = NULL; 284 | 285 | /* Push the response header. */ 286 | rc = ngx_http_send_header(req); 287 | if (rc == NGX_ERROR || rc > NGX_OK || req->header_only) { 288 | return rc; 289 | } 290 | 291 | /* Push the response body. */ 292 | return ngx_http_output_filter(req, &out); 293 | } 294 | 295 | static void* create_conf(ngx_conf_t* root_cfg) { 296 | configuration_t* cfg; 297 | cfg = ngx_palloc(root_cfg->pool, sizeof(configuration_t)); 298 | if (cfg == NULL) return NULL; 299 | cfg->enable = NGX_CONF_UNSET_UINT; 300 | return cfg; 301 | } 302 | 303 | static char* merge_conf(ngx_conf_t* root_cfg, void* parent, void* child) { 304 | configuration_t* prev = parent; 305 | configuration_t* cfg = child; 306 | ngx_conf_merge_uint_value(cfg->enable, prev->enable, 307 | NGX_HTTP_BROTLI_STATIC_OFF); 308 | return NGX_CONF_OK; 309 | } 310 | 311 | static ngx_int_t init(ngx_conf_t* root_cfg) { 312 | ngx_http_core_main_conf_t* core_cfg; 313 | ngx_http_handler_pt* handler_slot; 314 | core_cfg = ngx_http_conf_get_module_main_conf(root_cfg, ngx_http_core_module); 315 | handler_slot = 316 | ngx_array_push(&core_cfg->phases[NGX_HTTP_CONTENT_PHASE].handlers); 317 | if (handler_slot == NULL) return NGX_ERROR; 318 | *handler_slot = handler; 319 | return NGX_OK; 320 | } 321 | -------------------------------------------------------------------------------- /filter/ngx_http_brotli_filter_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Igor Sysoev 3 | * Copyright (C) Nginx, Inc. 4 | * Copyright (C) Google Inc. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #if (NGX_HAVE_BROTLI_ENC_ENCODE_H) 12 | #include 13 | #else 14 | #include 15 | #endif 16 | 17 | /* Brotli and GZip modules never stack, i.e. when one of them sets 18 | "Content-Encoding" the other becomes a pass-through filter. Consequently, 19 | it is almost legal to reuse this "buffered" bit. 20 | IIUC, buffered == some data passed to filter has not been pushed further. */ 21 | #define NGX_HTTP_BROTLI_BUFFERED NGX_HTTP_GZIP_BUFFERED 22 | 23 | /* Module configuration. */ 24 | typedef struct { 25 | ngx_flag_t enable; 26 | 27 | /* Supported MIME types. */ 28 | ngx_hash_t types; 29 | ngx_array_t* types_keys; 30 | 31 | /* Minimal required length for compression (if known). */ 32 | ssize_t min_length; 33 | 34 | ngx_bufs_t deprecated_unused_bufs; 35 | 36 | /* Brotli encoder parameter: quality */ 37 | ngx_int_t quality; 38 | 39 | /* Brotli encoder parameter: (max) lg_win */ 40 | size_t lg_win; 41 | } ngx_http_brotli_conf_t; 42 | 43 | /* Instance context. */ 44 | typedef struct { 45 | /* Brotli encoder instance. */ 46 | BrotliEncoderState* encoder; 47 | 48 | /* Payload length; -1, if unknown. */ 49 | off_t content_length; 50 | 51 | /* (uncompressed) bytes pushed to encoder. */ 52 | size_t bytes_in; 53 | /* (compressed) bytes pulled from encoder. */ 54 | size_t bytes_out; 55 | 56 | /* Input buffer chain. */ 57 | ngx_chain_t* in; 58 | 59 | /* Output chain. */ 60 | ngx_chain_t* out_chain; 61 | 62 | /* Output buffer. */ 63 | ngx_buf_t* out_buf; 64 | 65 | /* Various state flags. */ 66 | 67 | /* 1 if encoder is initialized, output chain and buffer are allocated. */ 68 | unsigned initialized : 1; 69 | /* 1 if compression is finished / failed. */ 70 | unsigned closed : 1; 71 | /* 1 if compression is finished. */ 72 | unsigned success : 1; 73 | 74 | /* 1 if out_chain is ready to be committed, 0 otherwise. */ 75 | unsigned output_ready : 1; 76 | /* 1 if output buffer is committed to the next filter and not yet fully used. 77 | 0 otherwise. */ 78 | unsigned output_busy : 1; 79 | 80 | unsigned end_of_input : 1; 81 | unsigned end_of_block : 1; 82 | 83 | ngx_http_request_t* request; 84 | } ngx_http_brotli_ctx_t; 85 | 86 | /* Forward declarations. */ 87 | 88 | /* Initializes encoder, output chain and buffer, if necessary. Returns NGX_OK 89 | if encoder is successfully initialized (have been already initialized), 90 | and requires objects are allocated. Returns NGX_ERROR otherwise. */ 91 | static ngx_int_t ngx_http_brotli_filter_ensure_stream_initialized( 92 | ngx_http_request_t* r, ngx_http_brotli_ctx_t* ctx); 93 | /* Marks instance as closed and performs cleanup. */ 94 | static void ngx_http_brotli_filter_close(ngx_http_brotli_ctx_t* ctx); 95 | 96 | static void* ngx_http_brotli_filter_alloc(void* opaque, size_t size); 97 | static void ngx_http_brotli_filter_free(void* opaque, void* address); 98 | 99 | static ngx_int_t ngx_http_brotli_check_request(ngx_http_request_t* r); 100 | 101 | static ngx_int_t ngx_http_brotli_add_variables(ngx_conf_t* cf); 102 | static ngx_int_t ngx_http_brotli_ratio_variable(ngx_http_request_t* r, 103 | ngx_http_variable_value_t* v, 104 | uintptr_t data); 105 | 106 | static void* ngx_http_brotli_create_conf(ngx_conf_t* cf); 107 | static char* ngx_http_brotli_merge_conf(ngx_conf_t* cf, void* parent, 108 | void* child); 109 | static ngx_int_t ngx_http_brotli_filter_init(ngx_conf_t* cf); 110 | 111 | static char* ngx_http_brotli_parse_wbits(ngx_conf_t* cf, void* post, 112 | void* data); 113 | 114 | /* Configuration literals. */ 115 | 116 | static ngx_conf_num_bounds_t ngx_http_brotli_comp_level_bounds = { 117 | ngx_conf_check_num_bounds, BROTLI_MIN_QUALITY, BROTLI_MAX_QUALITY}; 118 | 119 | static ngx_conf_post_handler_pt ngx_http_brotli_parse_wbits_p = 120 | ngx_http_brotli_parse_wbits; 121 | 122 | static ngx_command_t ngx_http_brotli_filter_commands[] = { 123 | {ngx_string("brotli"), 124 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 125 | NGX_HTTP_LIF_CONF | NGX_CONF_FLAG, 126 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 127 | offsetof(ngx_http_brotli_conf_t, enable), NULL}, 128 | 129 | /* Deprecated, unused. */ 130 | {ngx_string("brotli_buffers"), 131 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 132 | NGX_CONF_TAKE2, 133 | ngx_conf_set_bufs_slot, NGX_HTTP_LOC_CONF_OFFSET, 134 | offsetof(ngx_http_brotli_conf_t, deprecated_unused_bufs), NULL}, 135 | 136 | {ngx_string("brotli_types"), 137 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 138 | NGX_CONF_1MORE, 139 | ngx_http_types_slot, NGX_HTTP_LOC_CONF_OFFSET, 140 | offsetof(ngx_http_brotli_conf_t, types_keys), 141 | &ngx_http_html_default_types[0]}, 142 | 143 | {ngx_string("brotli_comp_level"), 144 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 145 | NGX_CONF_TAKE1, 146 | ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, 147 | offsetof(ngx_http_brotli_conf_t, quality), 148 | &ngx_http_brotli_comp_level_bounds}, 149 | 150 | {ngx_string("brotli_window"), 151 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 152 | NGX_CONF_TAKE1, 153 | ngx_conf_set_size_slot, NGX_HTTP_LOC_CONF_OFFSET, 154 | offsetof(ngx_http_brotli_conf_t, lg_win), &ngx_http_brotli_parse_wbits_p}, 155 | 156 | {ngx_string("brotli_min_length"), 157 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 158 | NGX_CONF_TAKE1, 159 | ngx_conf_set_size_slot, NGX_HTTP_LOC_CONF_OFFSET, 160 | offsetof(ngx_http_brotli_conf_t, min_length), NULL}, 161 | 162 | ngx_null_command}; 163 | 164 | /* Module context hooks. */ 165 | static ngx_http_module_t ngx_http_brotli_filter_module_ctx = { 166 | ngx_http_brotli_add_variables, /* pre-configuration */ 167 | ngx_http_brotli_filter_init, /* post-configuration */ 168 | 169 | NULL, /* create main configuration */ 170 | NULL, /* init main configuration */ 171 | 172 | NULL, /* create server configuration */ 173 | NULL, /* merge server configuration */ 174 | 175 | ngx_http_brotli_create_conf, /* create location configuration */ 176 | ngx_http_brotli_merge_conf /* merge location configuration */ 177 | }; 178 | 179 | /* Module descriptor. */ 180 | ngx_module_t ngx_http_brotli_filter_module = { 181 | NGX_MODULE_V1, 182 | &ngx_http_brotli_filter_module_ctx, /* module context */ 183 | ngx_http_brotli_filter_commands, /* module directives */ 184 | NGX_HTTP_MODULE, /* module type */ 185 | NULL, /* init master */ 186 | NULL, /* init module */ 187 | NULL, /* init process */ 188 | NULL, /* init thread */ 189 | NULL, /* exit thread */ 190 | NULL, /* exit process */ 191 | NULL, /* exit master */ 192 | NGX_MODULE_V1_PADDING}; 193 | 194 | /* Variable names. */ 195 | static ngx_str_t ngx_http_brotli_ratio = ngx_string("brotli_ratio"); 196 | 197 | /* Next filter in the filter chain. */ 198 | static ngx_http_output_header_filter_pt ngx_http_next_header_filter; 199 | static ngx_http_output_body_filter_pt ngx_http_next_body_filter; 200 | 201 | static /* const */ char kEncoding[] = "br"; 202 | static const size_t kEncodingLen = 2; 203 | 204 | static ngx_int_t check_accept_encoding(ngx_http_request_t* req) { 205 | ngx_table_elt_t* accept_encoding_entry; 206 | ngx_str_t* accept_encoding; 207 | u_char* cursor; 208 | u_char* end; 209 | u_char before; 210 | u_char after; 211 | 212 | accept_encoding_entry = req->headers_in.accept_encoding; 213 | if (accept_encoding_entry == NULL) return NGX_DECLINED; 214 | accept_encoding = &accept_encoding_entry->value; 215 | 216 | cursor = accept_encoding->data; 217 | end = cursor + accept_encoding->len; 218 | while (1) { 219 | u_char digit; 220 | /* It would be an idiotic idea to rely on compiler to produce performant 221 | binary, that is why we just do -1 at every call site. */ 222 | cursor = ngx_strcasestrn(cursor, kEncoding, kEncodingLen - 1); 223 | if (cursor == NULL) return NGX_DECLINED; 224 | before = (cursor == accept_encoding->data) ? ' ' : cursor[-1]; 225 | cursor += kEncodingLen; 226 | after = (cursor >= end) ? ' ' : *cursor; 227 | if (before != ',' && before != ' ') continue; 228 | if (after != ',' && after != ' ' && after != ';') continue; 229 | 230 | /* Check for ";q=0[.[0[0[0]]]]" */ 231 | while (*cursor == ' ') cursor++; 232 | if (*(cursor++) != ';') break; 233 | while (*cursor == ' ') cursor++; 234 | if (*(cursor++) != 'q') break; 235 | while (*cursor == ' ') cursor++; 236 | if (*(cursor++) != '=') break; 237 | while (*cursor == ' ') cursor++; 238 | if (*(cursor++) != '0') break; 239 | if (*(cursor++) != '.') return NGX_DECLINED; /* ;q=0, */ 240 | digit = *(cursor++); 241 | if (digit < '0' || digit > '9') return NGX_DECLINED; /* ;q=0., */ 242 | if (digit > '0') break; 243 | digit = *(cursor++); 244 | if (digit < '0' || digit > '9') return NGX_DECLINED; /* ;q=0.0, */ 245 | if (digit > '0') break; 246 | digit = *(cursor++); 247 | if (digit < '0' || digit > '9') return NGX_DECLINED; /* ;q=0.00, */ 248 | if (digit > '0') break; 249 | return NGX_DECLINED; /* ;q=0.000 */ 250 | } 251 | return NGX_OK; 252 | } 253 | 254 | /* Process headers and decide if request is eligible for brotli compression. */ 255 | static ngx_int_t ngx_http_brotli_header_filter(ngx_http_request_t* r) { 256 | ngx_table_elt_t* h; 257 | ngx_http_brotli_ctx_t* ctx; 258 | ngx_http_brotli_conf_t* conf; 259 | 260 | conf = ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module); 261 | 262 | /* Filter only if enabled. */ 263 | if (!conf->enable) { 264 | return ngx_http_next_header_filter(r); 265 | } 266 | 267 | /* Only compress OK / forbidden / not found responses. */ 268 | if (r->headers_out.status != NGX_HTTP_OK && 269 | r->headers_out.status != NGX_HTTP_FORBIDDEN && 270 | r->headers_out.status != NGX_HTTP_NOT_FOUND) { 271 | return ngx_http_next_header_filter(r); 272 | } 273 | 274 | /* Bypass "header only" responses. */ 275 | if (r->header_only) { 276 | return ngx_http_next_header_filter(r); 277 | } 278 | 279 | /* Bypass already compressed responses. */ 280 | if (r->headers_out.content_encoding && 281 | r->headers_out.content_encoding->value.len) { 282 | return ngx_http_next_header_filter(r); 283 | } 284 | 285 | /* If response size is known, do not compress tiny responses. */ 286 | if (r->headers_out.content_length_n != -1 && 287 | r->headers_out.content_length_n < conf->min_length) { 288 | return ngx_http_next_header_filter(r); 289 | } 290 | 291 | /* Compress only certain MIME-typed responses. */ 292 | if (ngx_http_test_content_type(r, &conf->types) == NULL) { 293 | return ngx_http_next_header_filter(r); 294 | } 295 | 296 | r->gzip_vary = 1; 297 | 298 | /* Check if client support brotli encoding. */ 299 | if (ngx_http_brotli_check_request(r) != NGX_OK) { 300 | return ngx_http_next_header_filter(r); 301 | } 302 | 303 | /* Prepare instance context. */ 304 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_brotli_ctx_t)); 305 | if (ctx == NULL) { 306 | return NGX_ERROR; 307 | } 308 | ctx->request = r; 309 | ctx->content_length = r->headers_out.content_length_n; 310 | ngx_http_set_ctx(r, ctx, ngx_http_brotli_filter_module); 311 | 312 | /* Prepare response headers, so that following filters in the chain will 313 | notice that response body is compressed. */ 314 | h = ngx_list_push(&r->headers_out.headers); 315 | if (h == NULL) { 316 | return NGX_ERROR; 317 | } 318 | 319 | h->hash = 1; 320 | ngx_str_set(&h->key, "Content-Encoding"); 321 | ngx_str_set(&h->value, "br"); 322 | r->headers_out.content_encoding = h; 323 | 324 | r->main_filter_need_in_memory = 1; 325 | 326 | ngx_http_clear_content_length(r); 327 | ngx_http_clear_accept_ranges(r); 328 | ngx_http_weak_etag(r); 329 | 330 | return ngx_http_next_header_filter(r); 331 | } 332 | 333 | /* Response body filtration (compression). */ 334 | static ngx_int_t ngx_http_brotli_body_filter(ngx_http_request_t* r, 335 | ngx_chain_t* in) { 336 | int rc; 337 | ngx_http_brotli_ctx_t* ctx; 338 | size_t available_output; 339 | size_t input_size; 340 | size_t available_input; 341 | const uint8_t* next_input_byte; 342 | size_t consumed_input; 343 | BROTLI_BOOL ok; 344 | u_char* out; 345 | ngx_chain_t* link; 346 | 347 | ctx = ngx_http_get_module_ctx(r, ngx_http_brotli_filter_module); 348 | 349 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 350 | "http brotli filter"); 351 | 352 | if (ctx == NULL || ctx->closed || r->header_only) { 353 | return ngx_http_next_body_filter(r, in); 354 | } 355 | 356 | if (ngx_http_brotli_filter_ensure_stream_initialized(r, ctx) != NGX_OK) { 357 | ngx_http_brotli_filter_close(ctx); 358 | return NGX_ERROR; 359 | } 360 | 361 | /* If more input is provided - append it to our input chain. */ 362 | if (in) { 363 | if (ngx_chain_add_copy(r->pool, &ctx->in, in) != NGX_OK) { 364 | ngx_http_brotli_filter_close(ctx); 365 | return NGX_ERROR; 366 | } 367 | r->connection->buffered |= NGX_HTTP_BROTLI_BUFFERED; 368 | } 369 | 370 | /* Main loop: 371 | - if output is not yet consumed - stop; encoder should not be touched, 372 | until all the output is consumed 373 | - if encoder has output - wrap it and send to consumer 374 | - if encoder is finished (and all output is consumed) - stop 375 | - if there is more input - push it to encoder */ 376 | for (;;) { 377 | if (ctx->output_busy || ctx->output_ready) { 378 | rc = ngx_http_next_body_filter(r, 379 | ctx->output_ready ? ctx->out_chain : NULL); 380 | if (ctx->output_ready) { 381 | ctx->output_ready = 0; 382 | ctx->output_busy = 1; 383 | } 384 | if (ngx_buf_size(ctx->out_buf) == 0) { 385 | ctx->output_busy = 0; 386 | } 387 | if (rc == NGX_OK) { 388 | continue; 389 | } else if (rc == NGX_AGAIN) { 390 | if (ctx->output_busy) { 391 | /* Can't continue compression, let the outer filer decide. */ 392 | if (ctx->in != NULL) { 393 | r->connection->buffered |= NGX_HTTP_BROTLI_BUFFERED; 394 | } 395 | return NGX_AGAIN; 396 | } else { 397 | /* Inner filter has given up, but we can continue processing. */ 398 | continue; 399 | } 400 | } else { 401 | ngx_http_brotli_filter_close(ctx); 402 | return NGX_ERROR; 403 | } 404 | } 405 | 406 | if (BrotliEncoderHasMoreOutput(ctx->encoder)) { 407 | available_output = 0; 408 | out = (u_char*)BrotliEncoderTakeOutput(ctx->encoder, &available_output); 409 | if (out == NULL || available_output == 0) { 410 | ngx_http_brotli_filter_close(ctx); 411 | return NGX_ERROR; 412 | } 413 | ctx->out_buf->start = out; 414 | ctx->out_buf->pos = out; 415 | ctx->out_buf->last = out + available_output; 416 | ctx->out_buf->end = out + available_output; 417 | ctx->bytes_out += available_output; 418 | ctx->out_buf->last_buf = 0; 419 | ctx->out_buf->flush = 0; 420 | if (ctx->end_of_input && BrotliEncoderIsFinished(ctx->encoder)) { 421 | ctx->out_buf->last_buf = 1; 422 | r->connection->buffered &= ~NGX_HTTP_BROTLI_BUFFERED; 423 | } else if (ctx->end_of_block) { 424 | ctx->out_buf->flush = 1; 425 | r->connection->buffered &= ~NGX_HTTP_BROTLI_BUFFERED; 426 | } 427 | ctx->end_of_block = 0; 428 | ctx->output_ready = 1; 429 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 430 | "brotli out: %p, size:%uz", ctx->out_buf, 431 | ngx_buf_size(ctx->out_buf)); 432 | continue; 433 | } 434 | 435 | if (BrotliEncoderIsFinished(ctx->encoder)) { 436 | ctx->success = 1; 437 | r->connection->buffered &= ~NGX_HTTP_BROTLI_BUFFERED; 438 | ngx_http_brotli_filter_close(ctx); 439 | return NGX_OK; 440 | } 441 | 442 | if (ctx->end_of_input) { 443 | // Ask the encoder to dump the leftover. 444 | available_input = 0; 445 | available_output = 0; 446 | ok = BrotliEncoderCompressStream(ctx->encoder, BROTLI_OPERATION_FINISH, 447 | &available_input, NULL, 448 | &available_output, NULL, NULL); 449 | r->connection->buffered |= NGX_HTTP_BROTLI_BUFFERED; 450 | if (!ok) { 451 | ngx_http_brotli_filter_close(ctx); 452 | return NGX_ERROR; 453 | } 454 | continue; 455 | } 456 | 457 | if (ctx->in == NULL) { 458 | return NGX_OK; 459 | } 460 | 461 | /* TODO: coalesce tiny inputs, if they are not last/flush. */ 462 | input_size = ngx_buf_size(ctx->in->buf); 463 | if (input_size == 0) { 464 | if (!ctx->in->buf->last_buf && !ctx->in->buf->flush) { 465 | link = ctx->in; 466 | ctx->in = ctx->in->next; 467 | ngx_free_chain(r->pool, link); 468 | continue; 469 | } 470 | } 471 | 472 | available_input = input_size; 473 | next_input_byte = (const uint8_t*)ctx->in->buf->pos; 474 | available_output = 0; 475 | ok = BrotliEncoderCompressStream( 476 | ctx->encoder, 477 | ctx->in->buf->last_buf ? BROTLI_OPERATION_FINISH 478 | : ctx->in->buf->flush ? BROTLI_OPERATION_FLUSH 479 | : BROTLI_OPERATION_PROCESS, 480 | &available_input, &next_input_byte, &available_output, NULL, NULL); 481 | r->connection->buffered |= NGX_HTTP_BROTLI_BUFFERED; 482 | if (!ok) { 483 | ngx_http_brotli_filter_close(ctx); 484 | return NGX_ERROR; 485 | } 486 | 487 | consumed_input = input_size - available_input; 488 | ctx->bytes_in += consumed_input; 489 | ctx->in->buf->pos += consumed_input; 490 | 491 | if (consumed_input == input_size) { 492 | if (ctx->in->buf->last_buf) { 493 | ctx->end_of_input = 1; 494 | } else if (ctx->in->buf->flush) { 495 | ctx->end_of_block = 1; 496 | } 497 | link = ctx->in; 498 | ctx->in = ctx->in->next; 499 | ngx_free_chain(r->pool, link); 500 | continue; 501 | } 502 | 503 | /* Should never happen, just to make sure we don't enter infinite loop. */ 504 | if (consumed_input == 0) { 505 | ngx_http_brotli_filter_close(ctx); 506 | return NGX_ERROR; 507 | } 508 | } 509 | 510 | /* unreachable */ 511 | ngx_http_brotli_filter_close(ctx); 512 | return NGX_ERROR; 513 | } 514 | 515 | static ngx_int_t ngx_http_brotli_filter_ensure_stream_initialized( 516 | ngx_http_request_t* r, ngx_http_brotli_ctx_t* ctx) { 517 | ngx_http_brotli_conf_t* conf; 518 | BROTLI_BOOL ok; 519 | size_t wbits; 520 | 521 | if (ctx->initialized) { 522 | return NGX_OK; 523 | } 524 | ctx->initialized = 1; 525 | 526 | conf = ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module); 527 | 528 | /* Tune lg_win, if size is known. */ 529 | if (ctx->content_length > 0) { 530 | wbits = BROTLI_MIN_WINDOW_BITS; 531 | while ((wbits < conf->lg_win) && (ctx->content_length > (1 << wbits))) { 532 | wbits++; 533 | } 534 | } else { 535 | wbits = conf->lg_win; 536 | } 537 | 538 | ctx->encoder = BrotliEncoderCreateInstance( 539 | ngx_http_brotli_filter_alloc, ngx_http_brotli_filter_free, r->pool); 540 | if (ctx->encoder == NULL) { 541 | ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, 542 | "OOM / BrotliEncoderCreateInstance"); 543 | return NGX_ERROR; 544 | } 545 | 546 | ok = BrotliEncoderSetParameter(ctx->encoder, BROTLI_PARAM_QUALITY, 547 | (uint32_t)conf->quality); 548 | if (!ok) { 549 | ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, 550 | "BrotliEncoderSetParameter(QUALITY, %uD) failed", 551 | (uint32_t)conf->quality); 552 | return NGX_ERROR; 553 | } 554 | 555 | ok = BrotliEncoderSetParameter(ctx->encoder, BROTLI_PARAM_LGWIN, 556 | (uint32_t)wbits); 557 | if (!ok) { 558 | ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, 559 | "BrotliEncoderSetParameter(LGWIN, %uD) failed", 560 | (uint32_t)wbits); 561 | return NGX_ERROR; 562 | } 563 | 564 | ctx->out_buf = ngx_calloc_buf(r->pool); 565 | if (ctx->out_buf == NULL) { 566 | return NGX_ERROR; 567 | } 568 | ctx->out_buf->temporary = 1; 569 | 570 | ctx->out_chain = ngx_alloc_chain_link(r->pool); 571 | if (ctx->out_chain == NULL) { 572 | return NGX_ERROR; 573 | } 574 | ctx->out_chain->buf = ctx->out_buf; 575 | ctx->out_chain->next = NULL; 576 | 577 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 578 | "brotli encoder initialized: lvl:%i win:%d", conf->quality, 579 | (1 << wbits)); 580 | 581 | return NGX_OK; 582 | } 583 | 584 | static void* ngx_http_brotli_filter_alloc(void* opaque, size_t size) { 585 | ngx_pool_t* pool = opaque; 586 | void* p; 587 | 588 | p = ngx_palloc(pool, size); 589 | 590 | #if (NGX_DEBUG) 591 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pool->log, 0, "brotli alloc: %p, size:%uz", 592 | p, size); 593 | #endif 594 | 595 | return p; 596 | } 597 | 598 | static void ngx_http_brotli_filter_free(void* opaque, void* address) { 599 | ngx_pool_t* pool = opaque; 600 | 601 | #if (NGX_DEBUG) 602 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pool->log, 0, "brotli free: %p", address); 603 | #endif 604 | 605 | ngx_pfree(pool, address); 606 | } 607 | 608 | static void ngx_http_brotli_filter_close(ngx_http_brotli_ctx_t* ctx) { 609 | ctx->closed = 1; 610 | if (ctx->encoder) { 611 | BrotliEncoderDestroyInstance(ctx->encoder); 612 | ctx->encoder = NULL; 613 | } 614 | if (ctx->out_chain) { 615 | ngx_free_chain(ctx->request->pool, ctx->out_chain); 616 | ctx->out_chain = NULL; 617 | } 618 | if (ctx->out_buf) { 619 | ngx_pfree(ctx->request->pool, ctx->out_buf); 620 | ctx->out_buf = NULL; 621 | } 622 | } 623 | 624 | static ngx_int_t ngx_http_brotli_check_request(ngx_http_request_t* req) { 625 | if (req != req->main) return NGX_DECLINED; 626 | if (check_accept_encoding(req) != NGX_OK) return NGX_DECLINED; 627 | req->gzip_tested = 1; 628 | req->gzip_ok = 0; 629 | return NGX_OK; 630 | } 631 | 632 | static ngx_int_t ngx_http_brotli_add_variables(ngx_conf_t* cf) { 633 | ngx_http_variable_t* var; 634 | 635 | var = ngx_http_add_variable(cf, &ngx_http_brotli_ratio, 0); 636 | if (var == NULL) { 637 | return NGX_ERROR; 638 | } 639 | 640 | var->get_handler = ngx_http_brotli_ratio_variable; 641 | 642 | return NGX_OK; 643 | } 644 | 645 | static ngx_int_t ngx_http_brotli_ratio_variable(ngx_http_request_t* r, 646 | ngx_http_variable_value_t* v, 647 | uintptr_t data) { 648 | ngx_uint_t ratio_int; 649 | ngx_uint_t ratio_frac; 650 | ngx_http_brotli_ctx_t* ctx; 651 | 652 | v->valid = 1; 653 | v->no_cacheable = 0; 654 | v->not_found = 0; 655 | 656 | ctx = ngx_http_get_module_ctx(r, ngx_http_brotli_filter_module); 657 | 658 | /* Only report variable on non-failing streams. */ 659 | if (ctx == NULL || !ctx->success) { 660 | v->not_found = 1; 661 | return NGX_OK; 662 | } 663 | 664 | v->data = ngx_pnalloc(r->pool, NGX_INT32_LEN + 3); 665 | if (v->data == NULL) { 666 | return NGX_ERROR; 667 | } 668 | 669 | ratio_int = (ngx_uint_t)(ctx->bytes_in / ctx->bytes_out); 670 | ratio_frac = (ngx_uint_t)((ctx->bytes_in * 100 / ctx->bytes_out) % 100); 671 | 672 | /* Rounding; e.g. 2.125 to 2.13 */ 673 | if ((ctx->bytes_in * 1000 / ctx->bytes_out) % 10 > 4) { 674 | ratio_frac++; 675 | if (ratio_frac > 99) { 676 | ratio_int++; 677 | ratio_frac = 0; 678 | } 679 | } 680 | 681 | v->len = ngx_sprintf(v->data, "%ui.%02ui", ratio_int, ratio_frac) - v->data; 682 | 683 | return NGX_OK; 684 | } 685 | 686 | static void* ngx_http_brotli_create_conf(ngx_conf_t* cf) { 687 | ngx_http_brotli_conf_t* conf; 688 | 689 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_brotli_conf_t)); 690 | if (conf == NULL) { 691 | return NULL; 692 | } 693 | 694 | /* ngx_pcalloc fills result with zeros -> 695 | conf->bufs.num = 0; 696 | conf->types = { NULL }; 697 | conf->types_keys = NULL; */ 698 | 699 | conf->enable = NGX_CONF_UNSET; 700 | 701 | conf->quality = NGX_CONF_UNSET; 702 | conf->lg_win = NGX_CONF_UNSET_SIZE; 703 | conf->min_length = NGX_CONF_UNSET; 704 | 705 | return conf; 706 | } 707 | 708 | static char* ngx_http_brotli_merge_conf(ngx_conf_t* cf, void* parent, 709 | void* child) { 710 | ngx_http_brotli_conf_t* prev = parent; 711 | ngx_http_brotli_conf_t* conf = child; 712 | char* rc; 713 | 714 | ngx_conf_merge_value(conf->enable, prev->enable, 0); 715 | 716 | ngx_conf_merge_value(conf->quality, prev->quality, 6); 717 | ngx_conf_merge_size_value(conf->lg_win, prev->lg_win, 19); 718 | ngx_conf_merge_value(conf->min_length, prev->min_length, 20); 719 | 720 | rc = ngx_http_merge_types(cf, &conf->types_keys, &conf->types, 721 | &prev->types_keys, &prev->types, 722 | ngx_http_html_default_types); 723 | if (rc != NGX_CONF_OK) { 724 | return NGX_CONF_ERROR; 725 | } 726 | 727 | return NGX_CONF_OK; 728 | } 729 | 730 | /* Prepend to filter chain. */ 731 | static ngx_int_t ngx_http_brotli_filter_init(ngx_conf_t* cf) { 732 | ngx_http_next_header_filter = ngx_http_top_header_filter; 733 | ngx_http_top_header_filter = ngx_http_brotli_header_filter; 734 | 735 | ngx_http_next_body_filter = ngx_http_top_body_filter; 736 | ngx_http_top_body_filter = ngx_http_brotli_body_filter; 737 | 738 | return NGX_OK; 739 | } 740 | 741 | /* Translate "window size" to window bits (log2), and check bounds. */ 742 | static char* ngx_http_brotli_parse_wbits(ngx_conf_t* cf, void* post, 743 | void* data) { 744 | size_t* parameter = data; 745 | size_t bits; 746 | size_t wsize; 747 | 748 | for (bits = BROTLI_MIN_WINDOW_BITS; bits <= BROTLI_MAX_WINDOW_BITS; bits++) { 749 | wsize = 1u << bits; 750 | if (*parameter == wsize) { 751 | *parameter = bits; 752 | return NGX_CONF_OK; 753 | } 754 | } 755 | 756 | return "must be 1k, 2k, 4k, 8k, 16k, 32k, 64k, 128k, 256k, 512k, 1m, 2m, 4m, " 757 | "8m or 16m"; 758 | } 759 | --------------------------------------------------------------------------------