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