This is an automated email from the ASF dual-hosted git repository.

monkeydluffy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 7dbabf919 feat: add brotli plugin (#10515)
7dbabf919 is described below

commit 7dbabf9190d5c5095b4712b6c3032e7a93fe7acc
Author: yuweizzz <yuwei764969...@gmail.com>
AuthorDate: Fri Dec 8 13:51:53 2023 +0800

    feat: add brotli plugin (#10515)
---
 .github/workflows/fuzzing-ci.yaml          |   9 -
 apisix-master-0.rockspec                   |   3 +-
 apisix/plugins/brotli.lua                  | 241 ++++++++++
 ci/centos7-ci.sh                           |   4 +
 ci/common.sh                               |  17 +
 ci/linux_apisix_current_luarocks_runner.sh |   1 +
 ci/linux_apisix_master_luarocks_runner.sh  |   1 +
 ci/linux_openresty_common_runner.sh        |   3 +
 ci/redhat-ci.sh                            |   4 +
 conf/config-default.yaml                   |   1 +
 docs/en/latest/config.json                 |   1 +
 docs/en/latest/plugins/brotli.md           | 123 +++++
 t/plugin/brotli.t                          | 720 +++++++++++++++++++++++++++++
 13 files changed, 1118 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/fuzzing-ci.yaml 
b/.github/workflows/fuzzing-ci.yaml
index 1e9fc0fa4..cf5133e80 100644
--- a/.github/workflows/fuzzing-ci.yaml
+++ b/.github/workflows/fuzzing-ci.yaml
@@ -56,15 +56,6 @@ jobs:
         source ./ci/common.sh
         export_version_info
         export_or_prefix
-        wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add 
-
-        sudo apt-get update
-        sudo apt-get -y install software-properties-common
-        sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu 
$(lsb_release -sc) main"
-        sudo apt-get update
-        sudo apt-get install -y git curl openresty-openssl111-dev unzip make 
gcc libldap2-dev
-        ./utils/linux-install-luarocks.sh
-
-        make deps
         make init
         make run
 
diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec
index d4e4b71a0..6779af88f 100644
--- a/apisix-master-0.rockspec
+++ b/apisix-master-0.rockspec
@@ -79,7 +79,8 @@ dependencies = {
     "nanoid = 0.1-1",
     "lua-resty-mediador = 0.1.2-1",
     "lua-resty-ldap = 0.1.0-0",
-    "lua-resty-t1k = 1.1.0"
+    "lua-resty-t1k = 1.1.0",
+    "brotli-ffi = 0.3-1"
 }
 
 build = {
diff --git a/apisix/plugins/brotli.lua b/apisix/plugins/brotli.lua
new file mode 100644
index 000000000..9b4954aea
--- /dev/null
+++ b/apisix/plugins/brotli.lua
@@ -0,0 +1,241 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core = require("apisix.core")
+local ngx = ngx
+local ngx_re_gmatch = ngx.re.gmatch
+local ngx_header = ngx.header
+local req_http_version = ngx.req.http_version
+local str_sub = string.sub
+local ipairs = ipairs
+local tonumber = tonumber
+local type = type
+local is_loaded, brotli = pcall(require, "brotli")
+
+
+local schema = {
+    type = "object",
+    properties = {
+        types = {
+            anyOf = {
+                {
+                    type = "array",
+                    minItems = 1,
+                    items = {
+                        type = "string",
+                        minLength = 1,
+                    },
+                },
+                {
+                    enum = {"*"}
+                }
+            },
+            default = {"text/html"}
+        },
+        min_length = {
+            type = "integer",
+            minimum = 1,
+            default = 20,
+        },
+        mode = {
+            type = "integer",
+            minimum = 0,
+            maximum = 2,
+            default = 0,
+            -- 0: MODE_GENERIC (default),
+            -- 1: MODE_TEXT (for UTF-8 format text input)
+            -- 2: MODE_FONT (for WOFF 2.0)
+        },
+        comp_level = {
+            type = "integer",
+            minimum = 0,
+            maximum = 11,
+            default = 6,
+            -- follow the default value from ngx_brotli brotli_comp_level
+        },
+        lgwin = {
+            type = "integer",
+            default = 19,
+            -- follow the default value from ngx_brotli brotli_window
+            enum = {0,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24},
+        },
+        lgblock = {
+            type = "integer",
+            default = 0,
+            enum = {0,16,17,18,19,20,21,22,23,24},
+        },
+        http_version = {
+            enum = {1.1, 1.0},
+            default = 1.1,
+        },
+        vary = {
+            type = "boolean",
+        }
+    },
+}
+
+
+local _M = {
+    version = 0.1,
+    priority = 996,
+    name = "brotli",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function create_brotli_compressor(mode, comp_level, lgwin, lgblock)
+    local options = {
+        mode = mode,
+        quality = comp_level,
+        lgwin = lgwin,
+        lgblock = lgblock,
+    }
+    return brotli.compressor:new(options)
+end
+
+
+local function check_accept_encoding(ctx)
+    local accept_encoding = core.request.header(ctx, "Accept-Encoding")
+    -- no Accept-Encoding
+    if not accept_encoding then
+        return false
+    end
+
+    -- single Accept-Encoding
+    if accept_encoding == "*" or accept_encoding == "br" then
+        return true
+    end
+
+    -- multi Accept-Encoding
+    local iterator, err = ngx_re_gmatch(accept_encoding,
+                                        [[([a-z\*]+)(;q=)?([0-9.]*)?]], "jo")
+    if not iterator then
+        core.log.error("gmatch failed, error: ", err)
+        return false
+    end
+
+    local captures
+    while true do
+        captures, err = iterator()
+        if not captures then
+            break
+        end
+        if err then
+            core.log.error("iterator failed, error: ", err)
+            return false
+        end
+        if (captures[1] == "br" or captures[1] == "*") and
+           (not captures[2] or captures[3] ~= "0") then
+            return true
+        end
+    end
+
+    return false
+end
+
+
+function _M.header_filter(conf, ctx)
+    if not is_loaded then
+        core.log.error("please check the brotli library")
+        return
+    end
+
+    local allow_encoding = check_accept_encoding(ctx)
+    if not allow_encoding then
+        return
+    end
+
+    local types = conf.types
+    local content_type = ngx_header["Content-Type"]
+    if not content_type then
+        -- Like Nginx, don't compress if Content-Type is missing
+        return
+    end
+
+    if type(types) == "table" then
+        local matched = false
+        local from = core.string.find(content_type, ";")
+        if from then
+            content_type = str_sub(content_type, 1, from - 1)
+        end
+
+        for _, ty in ipairs(types) do
+            if content_type == ty then
+                matched = true
+                break
+            end
+        end
+
+        if not matched then
+            return
+        end
+    end
+
+    local content_length = tonumber(ngx_header["Content-Length"])
+    if content_length then
+        local min_length = conf.min_length
+        if content_length < min_length then
+            return
+        end
+        -- Like Nginx, don't check min_length if Content-Length is missing
+    end
+
+    local http_version = req_http_version()
+    if http_version < conf.http_version then
+        return
+    end
+
+    if conf.vary then
+        core.response.add_header("Vary", "Accept-Encoding")
+    end
+
+    local compressor = create_brotli_compressor(conf.mode, conf.comp_level,
+                                                conf.lgwin, conf.lgblock)
+    if not compressor then
+        core.log.error("failed to create brotli compressor")
+        return
+    end
+
+    ctx.brotli_matched = true
+    ctx.compressor = compressor
+    core.response.clear_header_as_body_modified()
+    core.response.add_header("Content-Encoding", "br")
+end
+
+
+function _M.body_filter(conf, ctx)
+    if not ctx.brotli_matched then
+        return
+    end
+
+    local chunk, eof = ngx.arg[1], ngx.arg[2]
+    if type(chunk) == "string" and chunk ~= "" then
+        local encode_chunk = ctx.compressor:compress(chunk)
+        ngx.arg[1] = encode_chunk .. ctx.compressor:flush()
+    end
+
+    if eof then
+        ngx.arg[1] = ctx.compressor:finish()
+    end
+end
+
+
+return _M
diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh
index beb3750a1..485952a26 100755
--- a/ci/centos7-ci.sh
+++ b/ci/centos7-ci.sh
@@ -55,6 +55,10 @@ install_dependencies() {
     # install vault cli capabilities
     install_vault_cli
 
+    # install brotli
+    yum install -y cmake3
+    install_brotli
+
     # install test::nginx
     yum install -y cpanminus perl
     cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && 
exit 1)
diff --git a/ci/common.sh b/ci/common.sh
index 6d0e17c4c..525118e1a 100644
--- a/ci/common.sh
+++ b/ci/common.sh
@@ -104,6 +104,23 @@ install_nodejs () {
     npm config set registry https://registry.npmjs.org/
 }
 
+install_brotli () {
+    local BORTLI_VERSION="1.1.0"
+    wget -q 
https://github.com/google/brotli/archive/refs/tags/v${BORTLI_VERSION}.zip
+    unzip v${BORTLI_VERSION}.zip && cd ./brotli-${BORTLI_VERSION} && mkdir 
build && cd build
+    local CMAKE=$(command -v cmake3 > /dev/null 2>&1 && echo cmake3 || echo 
cmake)
+    ${CMAKE} -DCMAKE_BUILD_TYPE=Release 
-DCMAKE_INSTALL_PREFIX=/usr/local/brotli ..
+    sudo ${CMAKE} --build . --config Release --target install
+    if [ -d "/usr/local/brotli/lib64" ]; then
+        echo /usr/local/brotli/lib64 | sudo tee /etc/ld.so.conf.d/brotli.conf
+    else
+        echo /usr/local/brotli/lib | sudo tee /etc/ld.so.conf.d/brotli.conf
+    fi
+    sudo ldconfig
+    cd ../..
+    rm -rf brotli-${BORTLI_VERSION}
+}
+
 set_coredns() {
     # test a domain name is configured as upstream
     echo "127.0.0.1 test.com" | sudo tee -a /etc/hosts
diff --git a/ci/linux_apisix_current_luarocks_runner.sh 
b/ci/linux_apisix_current_luarocks_runner.sh
index 112904a42..39b9df8d0 100755
--- a/ci/linux_apisix_current_luarocks_runner.sh
+++ b/ci/linux_apisix_current_luarocks_runner.sh
@@ -20,6 +20,7 @@
 
 do_install() {
     linux_get_dependencies
+    install_brotli
 
     export_or_prefix
 
diff --git a/ci/linux_apisix_master_luarocks_runner.sh 
b/ci/linux_apisix_master_luarocks_runner.sh
index afc487ddd..4137f4399 100755
--- a/ci/linux_apisix_master_luarocks_runner.sh
+++ b/ci/linux_apisix_master_luarocks_runner.sh
@@ -20,6 +20,7 @@
 
 do_install() {
     linux_get_dependencies
+    install_brotli
 
     export_or_prefix
 
diff --git a/ci/linux_openresty_common_runner.sh 
b/ci/linux_openresty_common_runner.sh
index 466fe8b69..8a1f1eaf7 100755
--- a/ci/linux_openresty_common_runner.sh
+++ b/ci/linux_openresty_common_runner.sh
@@ -62,6 +62,9 @@ do_install() {
 
     # install vault cli capabilities
     install_vault_cli
+
+    # install brotli
+    install_brotli
 }
 
 script() {
diff --git a/ci/redhat-ci.sh b/ci/redhat-ci.sh
index d40ccbfeb..825cbe0b4 100755
--- a/ci/redhat-ci.sh
+++ b/ci/redhat-ci.sh
@@ -52,6 +52,10 @@ install_dependencies() {
     # install vault cli capabilities
     install_vault_cli
 
+    # install brotli
+    yum install -y cmake3
+    install_brotli
+
     # install test::nginx
     yum install -y --disablerepo=* --enablerepo=ubi-8-appstream-rpms 
--enablerepo=ubi-8-baseos-rpms cpanminus perl
     cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && 
exit 1)
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index 677f08272..866bb298f 100755
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -484,6 +484,7 @@ plugins:                           # plugin list (sorted by 
priority)
   - limit-count                    # priority: 1002
   - limit-req                      # priority: 1001
   #- node-status                   # priority: 1000
+  #- brotli                        # priority: 996
   - gzip                           # priority: 995
   - server-info                    # priority: 990
   - traffic-split                  # priority: 966
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 2eea87849..0569c82ee 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -72,6 +72,7 @@
             "plugins/redirect",
             "plugins/echo",
             "plugins/gzip",
+            "plugins/brotli",
             "plugins/real-ip",
             "plugins/server-info",
             "plugins/ext-plugin-pre-req",
diff --git a/docs/en/latest/plugins/brotli.md b/docs/en/latest/plugins/brotli.md
new file mode 100644
index 000000000..eaf9cb299
--- /dev/null
+++ b/docs/en/latest/plugins/brotli.md
@@ -0,0 +1,123 @@
+---
+title: brotli
+keywords:
+  - Apache APISIX
+  - API Gateway
+  - Plugin
+  - brotli
+description: This document contains information about the Apache APISIX brotli 
Plugin.
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## Description
+
+The `brotli` Plugin dynamically sets the behavior of [brotli in 
Nginx](https://github.com/google/ngx_brotli).
+
+## Prerequisites
+
+This Plugin requires brotli shared libraries.
+
+The example commands to build and install brotli shared libraries:
+
+``` shell
+wget https://github.com/google/brotli/archive/refs/tags/v1.1.0.zip
+unzip v1.1.0.zip
+cd brotli-1.1.0 && mkdir build && cd build
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local/brotli ..
+sudo cmake --build . --config Release --target install
+sudo sh -c "echo /usr/local/brotli/lib >> /etc/ld.so.conf.d/brotli.conf"
+sudo ldconfig
+```
+
+## Attributes
+
+| Name           | Type                 | Required | Default       | Valid 
values | Description                                                            
                 |
+|----------------|----------------------|----------|---------------|--------------|-----------------------------------------------------------------------------------------|
+| types          | array[string] or "*" | False    | ["text/html"] |           
   | Dynamically sets the `brotli_types` directive. Special value `"*"` matches 
any MIME type. |
+| min_length     | integer              | False    | 20            | >= 1      
   | Dynamically sets the `brotli_min_length` directive. |
+| comp_level     | integer              | False    | 6             | [0, 11]   
   | Dynamically sets the `brotli_comp_level` directive. |
+| mode           | integer              | False    | 0             | [0, 2]    
   | Dynamically sets the `brotli decompress mode`, more info in [RFC 
7932](https://tools.ietf.org/html/rfc7932). |
+| lgwin          | integer              | False    | 19            | [0, 
10-24]   | Dynamically sets the `brotli sliding window size`, `lgwin` is Base 2 
logarithm of the sliding window size, set to `0` lets compressor decide over 
the optimal value, more info in [RFC 
7932](https://tools.ietf.org/html/rfc7932). |
+| lgblock        | integer              | False    | 0             | [0, 
16-24]   | Dynamically sets the `brotli input block size`, `lgblock` is Base 2 
logarithm of the maximum input block size, set to `0` lets compressor decide 
over the optimal value, more info in [RFC 
7932](https://tools.ietf.org/html/rfc7932). |
+| http_version   | number               | False    | 1.1           | 1.1, 1.0  
   | Like the `gzip_http_version` directive, sets the minimum HTTP version of a 
request required to compress a response. |
+| vary           | boolean              | False    | false         |           
   | Like the `gzip_vary` directive, enables or disables inserting the “Vary: 
Accept-Encoding” response header field. |
+
+## Enable Plugin
+
+The example below enables the `brotli` Plugin on the specified Route:
+
+```shell
+curl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/",
+    "plugins": {
+        "brotli": {
+        }
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "httpbin.org": 1
+        }
+    }
+}'
+```
+
+## Example usage
+
+Once you have configured the Plugin as shown above, you can make a request as 
shown below:
+
+```shell
+curl http://127.0.0.1:9080/ -i -H "Accept-Encoding: br"
+```
+
+```
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+Transfer-Encoding: chunked
+Connection: keep-alive
+Date: Tue, 05 Dec 2023 03:06:49 GMT
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Credentials: true
+Server: APISIX/3.6.0
+Content-Encoding: br
+
+Warning: Binary output can mess up your terminal. Use "--output -" to tell
+Warning: curl to output it to your terminal anyway, or consider "--output
+Warning: <FILE>" to save to a file.
+```
+
+## Delete Plugin
+
+To remove the `brotli` Plugin, you can delete the corresponding JSON 
configuration from the Plugin configuration. APISIX will automatically reload 
and you do not have to restart for this to take effect.
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1  -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/",
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "httpbin.org": 1
+        }
+    }
+}'
+```
diff --git a/t/plugin/brotli.t b/t/plugin/brotli.t
new file mode 100644
index 000000000..5f7c6cae3
--- /dev/null
+++ b/t/plugin/brotli.t
@@ -0,0 +1,720 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    my $extra_yaml_config = <<_EOC_;
+plugins:
+    - brotli
+_EOC_
+
+    $block->set_value("extra_yaml_config", $extra_yaml_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 2: hit, single Accept-Encoding
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary:
+
+
+
+=== TEST 3: hit, single wildcard Accept-Encoding
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: *
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary:
+
+
+
+=== TEST 4: not hit, single Accept-Encoding
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip
+Content-Type: text/html
+--- response_headers
+Vary:
+
+
+
+=== TEST 5: hit, br in multi Accept-Encoding
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip, br
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary:
+
+
+
+=== TEST 6: hit, no br in multi Accept-Encoding, but wildcard
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip, *
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary:
+
+
+
+=== TEST 7: not hit, no br in multi Accept-Encoding
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip, deflate
+Content-Type: text/html
+--- response_headers
+Vary:
+
+
+
+=== TEST 8: hit, multi Accept-Encoding with quality
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip;q=0.5, br;q=0.6
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary:
+
+
+
+=== TEST 9: not hit, multi Accept-Encoding with quality and disable br
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip;q=0.5, br;q=0
+Content-Type: text/html
+--- response_headers
+Vary:
+
+
+
+=== TEST 10: hit, multi Accept-Encoding with quality and wildcard
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: gzip;q=0.8, deflate, sdch;q=0.6, *;q=0.1
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary:
+
+
+
+=== TEST 11: default buffers and compress level
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.brotli")
+            local core = require("apisix.core")
+            local json = require("toolkit.json")
+
+            for _, conf in ipairs({
+                {},
+                {mode = 1},
+                {comp_level = 5},
+                {comp_level = 5, lgwin = 12},
+                {comp_level = 5, lgwin = 12, vary = true},
+                {comp_level = 5, lgwin = 12, lgblock = 16, vary = true},
+                {mode = 2, comp_level = 5, lgwin = 12, lgblock = 16, vary = 
true},
+            }) do
+                local ok, err = plugin.check_schema(conf)
+                if not ok then
+                    ngx.say(err)
+                    return
+                end
+                ngx.say(json.encode(conf))
+            end
+        }
+    }
+--- response_body
+{"comp_level":6,"http_version":1.1,"lgblock":0,"lgwin":19,"min_length":20,"mode":0,"types":["text/html"]}
+{"comp_level":6,"http_version":1.1,"lgblock":0,"lgwin":19,"min_length":20,"mode":1,"types":["text/html"]}
+{"comp_level":5,"http_version":1.1,"lgblock":0,"lgwin":19,"min_length":20,"mode":0,"types":["text/html"]}
+{"comp_level":5,"http_version":1.1,"lgblock":0,"lgwin":12,"min_length":20,"mode":0,"types":["text/html"]}
+{"comp_level":5,"http_version":1.1,"lgblock":0,"lgwin":12,"min_length":20,"mode":0,"types":["text/html"],"vary":true}
+{"comp_level":5,"http_version":1.1,"lgblock":16,"lgwin":12,"min_length":20,"mode":0,"types":["text/html"],"vary":true}
+{"comp_level":5,"http_version":1.1,"lgblock":16,"lgwin":12,"min_length":20,"mode":2,"types":["text/html"],"vary":true}
+
+
+
+=== TEST 12: compress level
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [=[{
+                    "uri": "/echo",
+                    "vars": [["http_x", "==", "1"]],
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "comp_level": 0
+                        }
+                    }
+                }]=]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+            return
+        end
+
+        local code, body = t('/apisix/admin/routes/1',
+            ngx.HTTP_PUT,
+            [=[{
+                "uri": "/echo",
+                "vars": [["http_x", "==", "2"]],
+                "upstream": {
+                    "type": "roundrobin",
+                    "nodes": {
+                        "127.0.0.1:1980": 1
+                    }
+                },
+                "plugins": {
+                    "brotli": {
+                        "comp_level": 11
+                    }
+                }
+            }]=]
+        )
+
+        if code >= 300 then
+            ngx.status = code
+            return
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 13: hit
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/echo"
+            local httpc = http.new()
+            local res, err = httpc:request_uri(uri,
+                {method = "POST", headers = {x = "1"}, body = 
("0123"):rep(1024)})
+            if not res then
+                ngx.say(err)
+                return
+            end
+            local less_compressed = res.body
+            local res, err = httpc:request_uri(uri,
+                {method = "POST", headers = {x = "2"}, body = 
("0123"):rep(1024)})
+            if not res then
+                ngx.say(err)
+                return
+            end
+            if #less_compressed < 4096 and #less_compressed < #res.body then
+                ngx.say("ok")
+            end
+        }
+    }
+--- response_body
+ok
+
+
+
+=== TEST 14: min length
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "min_length": 21
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 15: not hit
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/html
+--- response_headers
+Content-Encoding:
+
+
+
+=== TEST 16: http version
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "http_version": 1.1
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 17: not hit
+--- request
+POST /echo HTTP/1.0
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/html
+--- response_headers
+Content-Encoding:
+
+
+
+=== TEST 18: hit again
+--- request
+POST /echo HTTP/1.1
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+
+
+
+=== TEST 19: types
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "types": ["text/plain", "text/xml"]
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 20: not hit
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/html
+--- response_headers
+Content-Encoding:
+
+
+
+=== TEST 21: hit again
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/xml
+--- response_headers
+Content-Encoding: br
+
+
+
+=== TEST 22: hit with charset
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: text/plain; charset=UTF-8
+--- response_headers
+Content-Encoding: br
+
+
+
+=== TEST 23: match all types
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "types": "*"
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 24: hit
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Content-Type: video/3gpp
+--- response_headers
+Content-Encoding: br
+
+
+
+=== TEST 25: vary
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "vary": true
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 26: hit
+--- request
+POST /echo
+0123456789
+012345678
+--- more_headers
+Accept-Encoding: br
+Vary: upstream
+Content-Type: text/html
+--- response_headers
+Content-Encoding: br
+Vary: upstream, Accept-Encoding
+
+
+
+=== TEST 27: schema check
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            for _, case in ipairs({
+                {input = {
+                    types = {}
+                }},
+                {input = {
+                    min_length = 0
+                }},
+                {input = {
+                    mode = 4
+                }},
+                {input = {
+                    comp_level = 12
+                }},
+                {input = {
+                    http_version = 2
+                }},
+                {input = {
+                    lgwin = 100
+                }},
+                {input = {
+                    lgblock = 8
+                }},
+                {input = {
+                    vary = 0
+                }}
+            }) do
+                local code, body = t('/apisix/admin/global_rules/1',
+                    ngx.HTTP_PUT,
+                    {
+                        id = "1",
+                        plugins = {
+                            ["brotli"] = case.input
+                        }
+                    }
+                )
+                ngx.print(body)
+            end
+    }
+}
+--- response_body
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"types\" validation failed: object matches none of the required"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"min_length\" validation failed: expected 0 to be at least 1"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"mode\" validation failed: expected 4 to be at most 2"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"comp_level\" validation failed: expected 12 to be at most 11"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"http_version\" validation failed: matches none of the enum values"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"lgwin\" validation failed: matches none of the enum values"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"lgblock\" validation failed: matches none of the enum values"}
+{"error_msg":"failed to check the configuration of plugin brotli err: property 
\"vary\" validation failed: wrong type: expected boolean, got number"}
+
+
+
+=== TEST 28: body checksum
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    },
+                    "plugins": {
+                        "brotli": {
+                            "types": "*"
+                        }
+                    }
+            }]]
+            )
+
+        if code >= 300 then
+            ngx.status = code
+        end
+        ngx.say(body)
+    }
+}
+--- response_body
+passed
+
+
+
+=== TEST 29: hit - decompressed respone body same as requset body
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/echo"
+            local httpc = http.new()
+            local req_body = ("abcdf01234"):rep(1024)
+            local res, err = httpc:request_uri(uri,
+                {method = "POST", headers = {["Accept-Encoding"] = "br"}, body 
= req_body})
+            if not res then
+                ngx.say(err)
+                return
+            end
+
+            local brotli = require "brotli"
+            local decompressor = brotli.decompressor:new()
+            local chunk = decompressor:decompress(res.body)
+            local chunk_fin = decompressor:finish()
+            local chunks = chunk .. chunk_fin
+            if #chunks == #req_body then
+                ngx.say("ok")
+            end
+        }
+    }
+--- response_body
+ok


Reply via email to