You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2020/10/26 13:51:02 UTC

[apisix] branch master updated: change(batch-requests): limit the body size to 1M (#2507)

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

spacewander 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 2a66762  change(batch-requests): limit the body size to 1M (#2507)
2a66762 is described below

commit 2a66762ca6719ca7d8334f2d1428b42218263845
Author: 罗泽轩 <sp...@gmail.com>
AuthorDate: Mon Oct 26 21:50:51 2020 +0800

    change(batch-requests): limit the body size to 1M (#2507)
    
    To avoid blocking on reading large body file and
    spending too much memory in encode JSON body.
---
 apisix/core/request.lua             |  54 ++++++++-
 apisix/plugins/batch-requests.lua   |  72 ++++++------
 doc/plugins/batch-requests.md       |  21 ++++
 doc/zh-cn/plugins/batch-requests.md |  19 ++++
 t/lib/test_admin.lua                |  11 +-
 t/plugin/batch-requests.t           | 215 +++++++++++++++++++++++++++++++++++-
 6 files changed, 354 insertions(+), 38 deletions(-)

diff --git a/apisix/core/request.lua b/apisix/core/request.lua
index fcf6e07..2e5af94 100644
--- a/apisix/core/request.lua
+++ b/apisix/core/request.lua
@@ -16,12 +16,15 @@
 --
 
 local lfs = require("lfs")
+local log = require("apisix.core.log")
 local ngx = ngx
 local get_headers = ngx.req.get_headers
+local clear_header = ngx.req.clear_header
 local tonumber = tonumber
 local error    = error
 local type     = type
 local str_fmt  = string.format
+local str_lower = string.lower
 local io_open  = io.open
 local req_read_body = ngx.req.read_body
 local req_get_body_data = ngx.req.get_body_data
@@ -116,11 +119,52 @@ local function get_file(file_name)
 end
 
 
-function _M.get_body(max_size)
+local function check_size(size, max_size)
+    if max_size and size > max_size then
+        return nil, "request size " .. size .. " is greater than the "
+                    .. "maximum size " .. max_size .. " allowed"
+    end
+
+    return true
+end
+
+
+local function test_expect(var)
+    local expect = var.http_expect
+    return expect and str_lower(expect) == "100-continue"
+end
+
+
+function _M.get_body(max_size, ctx)
+    -- TODO: improve the check with set client_max_body dynamically
+    -- which requires to change Nginx source code
+    if max_size then
+        local var = ctx and ctx.var or ngx.var
+        local content_length = tonumber(var.http_content_length)
+        if content_length then
+            local ok, err = check_size(content_length, max_size)
+            if not ok then
+                -- When client_max_body_size is exceeded, Nginx will set r->expect_tested = 1 to
+                -- avoid sending the 100 CONTINUE.
+                -- We use trick below to imitate this behavior.
+                if test_expect(var) then
+                    clear_header("expect")
+                end
+
+                return nil, err
+            end
+        end
+    end
+
     req_read_body()
 
     local req_body = req_get_body_data()
     if req_body then
+        local ok, err = check_size(#req_body, max_size)
+        if not ok then
+            return nil, err
+        end
+
         return req_body
     end
 
@@ -129,15 +173,17 @@ function _M.get_body(max_size)
         return nil
     end
 
+    log.info("attempt to read body from file: ", file_name)
+
     if max_size then
         local size, err = lfs.attributes (file_name, "size")
         if not size then
             return nil, err
         end
 
-        if size > max_size then
-            return nil, "request size " .. size .. " is greater than the "
-                        .. "maximum size " .. max_size .. " allowed"
+        local ok, err = check_size(size, max_size)
+        if not ok then
+            return nil, err
         end
     end
 
diff --git a/apisix/plugins/batch-requests.lua b/apisix/plugins/batch-requests.lua
index 7187821..dfe0dfd 100644
--- a/apisix/plugins/batch-requests.lua
+++ b/apisix/plugins/batch-requests.lua
@@ -16,8 +16,8 @@
 --
 local core      = require("apisix.core")
 local http      = require("resty.http")
+local plugin    = require("apisix.plugin")
 local ngx       = ngx
-local io_open   = io.open
 local ipairs    = ipairs
 local pairs     = pairs
 local str_find  = string.find
@@ -31,6 +31,20 @@ local schema = {
     additionalProperties = false,
 }
 
+local default_max_body_size = 1024 * 1024 -- 1MiB
+local metadata_schema = {
+    type = "object",
+    properties = {
+        max_body_size = {
+            description = "max pipeline body size in bytes",
+            type = "integer",
+            exclusiveMinimum = 0,
+            default = default_max_body_size,
+        },
+    },
+    additionalProperties = false,
+}
+
 local req_schema = {
     type = "object",
     properties = {
@@ -95,7 +109,8 @@ local _M = {
     version = 0.1,
     priority = 4010,
     name = plugin_name,
-    schema = schema
+    schema = schema,
+    metadata_schema = metadata_schema,
 }
 
 
@@ -178,51 +193,46 @@ local function set_common_query(data)
 end
 
 
-local function get_file(file_name)
-    local f = io_open(file_name, 'r')
-    if f then
-        local req_body = f:read("*all")
-        f:close()
-        return req_body
-    end
-
-    return
-end
+local function batch_requests(ctx)
+    local metadata = plugin.plugin_metadata(plugin_name)
+    core.log.info("metadata: ", core.json.delay_encode(metadata))
 
+    local max_body_size
+    if metadata then
+        max_body_size = metadata.value.max_body_size
+    else
+        max_body_size = default_max_body_size
+    end
 
-local function batch_requests()
-    ngx.req.read_body()
-    local req_body = ngx.req.get_body_data()
+    local req_body, err = core.request.get_body(max_body_size, ctx)
+    if err then
+        -- Nginx doesn't support 417: https://trac.nginx.org/nginx/ticket/2062
+        -- So always return 413 instead
+        return 413, { error_msg = err }
+    end
     if not req_body then
-        local file_name = ngx.req.get_body_file()
-        if file_name then
-            req_body = get_file(file_name)
-        end
-
-        if not req_body then
-            core.response.exit(400, {
-                error_msg = "no request body, you should give at least one pipeline setting"
-            })
-        end
+        return 400, {
+            error_msg = "no request body, you should give at least one pipeline setting"
+        }
     end
 
     local data, err = core.json.decode(req_body)
     if not data then
-        core.response.exit(400, {
+        return 400, {
             error_msg = "invalid request body: " .. req_body .. ", err: " .. err
-        })
+        }
     end
 
     local code, body = check_input(data)
     if code then
-        core.response.exit(code, body)
+        return code, body
     end
 
     local httpc = http.new()
     httpc:set_timeout(data.timeout)
     local ok, err = httpc:connect("127.0.0.1", ngx.var.server_port)
     if not ok then
-        core.response.exit(500, {error_msg = "connect to apisix failed: " .. err})
+        return 500, {error_msg = "connect to apisix failed: " .. err}
     end
 
     ensure_header_lowercase(data)
@@ -231,7 +241,7 @@ local function batch_requests()
 
     local responses, err = httpc:request_pipeline(data.pipeline)
     if not responses then
-        core.response.exit(400, {error_msg = "request failed: " .. err})
+        return 400, {error_msg = "request failed: " .. err}
     end
 
     local aggregated_resp = {}
@@ -252,7 +262,7 @@ local function batch_requests()
         end
         core.table.insert(aggregated_resp, sub_resp)
     end
-    core.response.exit(200, aggregated_resp)
+    return 200, aggregated_resp
 end
 
 
diff --git a/doc/plugins/batch-requests.md b/doc/plugins/batch-requests.md
index 480d60a..7df2102 100644
--- a/doc/plugins/batch-requests.md
+++ b/doc/plugins/batch-requests.md
@@ -24,6 +24,8 @@
 - [**Description**](#Description)
 - [**Attributes**](#Attributes)
 - [**How To Enable**](#how-to-Enable)
+- [**How To Configure**](#how-to-configure)
+- [**Metadata**](#metadata)
 - [**Batch Api Request/Response**](#batch-api-request/response)
 - [**Test Plugin**](#test-plugin)
 - [**Disable Plugin**](#disable-plugin)
@@ -49,6 +51,25 @@ You may need to use [interceptors](plugin-interceptors.md) to protect it.
 
 Default enabled
 
+## How To Configure
+
+By default, the maximun body size sent to the `/apisix/batch-requests` can't be larger than 1 MiB.
+You can configure it via `apisix/admin/plugin_metadata/batch-requests`:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/batch-requests -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "max_body_size": 4194304
+}'
+```
+
+## Metadata
+
+| Name             | Type    | Requirement | Default       | Valid   | Description                                                                              |
+| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |
+| max_body_size       | integer  | required   |  1048576  |    > 0  | the maximun of request body size in bytes |
+
+
 ## Batch API Request/Response
 The plugin will create a API in `apisix` to handle your batch request.
 
diff --git a/doc/zh-cn/plugins/batch-requests.md b/doc/zh-cn/plugins/batch-requests.md
index cdf3cef..e73c43f 100644
--- a/doc/zh-cn/plugins/batch-requests.md
+++ b/doc/zh-cn/plugins/batch-requests.md
@@ -24,6 +24,8 @@
 - [**简介**](#简介)
 - [**属性**](#属性)
 - [**如何启用**](#如何启用)
+- [**如何配置**](#如何配置)
+- [**元数据**](#元数据)
 - [**批量接口请求/响应**](#批量接口请求/响应)
 - [**测试插件**](#测试插件)
 - [**禁用插件**](#禁用插件)
@@ -49,6 +51,23 @@
 
 本插件默认启用。
 
+## 如何配置
+
+默认本插件限制请求体的大小不能大于 1 MiB。这个限制可以通过 `apisix/admin/plugin_metadata/batch-requests` 来修改。
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/batch-requests -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "max_body_size": 4194304
+}'
+```
+
+## 元数据
+
+| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |
+| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |
+| max_body_size       | integer  | 必选   |  1048576  |    > 0  | 请求体的最大大小,单位为字节。|
+
 ## 批量接口请求/响应
 插件会为 `apisix` 创建一个 `/apisix/batch-requests` 的接口来处理你的批量请求。
 
diff --git a/t/lib/test_admin.lua b/t/lib/test_admin.lua
index ac6d22e..bba93cd 100644
--- a/t/lib/test_admin.lua
+++ b/t/lib/test_admin.lua
@@ -105,13 +105,20 @@ end
 
 
 function _M.comp_tab(left_tab, right_tab)
+    local err
     dir_names = {}
 
     if type(left_tab) == "string" then
-        left_tab = json.decode(left_tab)
+        left_tab, err = json.decode(left_tab)
+        if not left_tab then
+            return false, "failed to decode expected data: " .. err
+        end
     end
     if type(right_tab) == "string" then
-        right_tab = json.decode(right_tab)
+        right_tab, err  = json.decode(right_tab)
+        if not right_tab then
+            return false, "failed to decode expected data: " .. err
+        end
     end
 
     local ok, err = com_tab(left_tab, right_tab)
diff --git a/t/plugin/batch-requests.t b/t/plugin/batch-requests.t
index c409cee..e438a1c 100644
--- a/t/plugin/batch-requests.t
+++ b/t/plugin/batch-requests.t
@@ -19,7 +19,7 @@ use t::APISIX 'no_plan';
 repeat_each(1);
 no_long_string();
 no_root_location();
-log_level("info");
+log_level("debug");
 
 run_tests;
 
@@ -778,3 +778,216 @@ GET /aggregate
 passed
 --- no_error_log
 [error]
+
+
+
+=== TEST 16: exceed default body limit size (check header)
+--- config
+    location = /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                 ngx.HTTP_POST,
+                 ("1234"):rep(1024 * 1024)
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 413
+--- response_body eval
+qr/\{"error_msg":"request size 4194304 is greater than the maximum size 1048576 allowed"\}/
+--- no_error_log
+[error]
+
+
+
+=== TEST 17: exceed default body limit size (check file size)
+--- request eval
+"POST /apisix/batch-requests
+" . ("1000\r
+" . ("11111111" x 512) . "\r
+") x 257 . "0\r
+\r
+"
+--- more_headers
+Transfer-Encoding: chunked
+--- error_code: 413
+--- response_body eval
+qr/\{"error_msg":"request size 1052672 is greater than the maximum size 1048576 allowed"\}/
+--- no_error_log
+[error]
+--- error_log
+attempt to read body from file
+
+
+
+=== TEST 18: add plugin metadata
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/plugin_metadata/batch-requests',
+                ngx.HTTP_PUT,
+                [[{
+                    "max_body_size": 2048
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 19: exceed body limit size
+--- config
+    location = /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                 ngx.HTTP_POST,
+                 ("1234"):rep(1024)
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 413
+--- response_body eval
+qr/\{"error_msg":"request size 4096 is greater than the maximum size 2048 allowed"\}/
+--- no_error_log
+[error]
+
+
+
+=== TEST 20: exceed body limit size (expected)
+--- config
+    location = /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body, res_data = t('/apisix/batch-requests',
+                 ngx.HTTP_POST,
+                 ("1234"):rep(1024),
+                 nil,
+                 {EXPECT = "100-CONTINUE", ["content-length"] = 4096}
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 413
+--- response_body eval
+qr/\{"error_msg":"request size 4096 is greater than the maximum size 2048 allowed"\}/
+--- no_error_log
+[error]
+
+
+
+=== TEST 21: don't exceed body limit size
+--- config
+    location = /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                 ngx.HTTP_POST,
+                 [=[{
+                    "headers": {
+                        "Base-Header": "base"
+                    },
+                    "pipeline":[
+                    {
+                        "path": "/a",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    }
+                    ]
+                }]=],
+                [=[[
+                {
+                    "status": 200,
+                    "body":"A",
+                    "headers": {
+                        "Base-Header": "base",
+                        "X-Res": "a",
+                        "X-Header1": "hello",
+                        "X-Header2": "world"
+                    }
+                }
+                ]]=])
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+    location = /a {
+        content_by_lua_block {
+            ngx.status = 200
+            ngx.header["Base-Header"] = ngx.req.get_headers()["Base-Header"]
+            ngx.header["X-Header1"] = ngx.req.get_headers()["Header1"]
+            ngx.header["X-Header2"] = ngx.req.get_headers()["Header2"]
+            ngx.header["X-Res"] = "a"
+            ngx.print("A")
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 22: invalid body size
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/plugin_metadata/batch-requests',
+                ngx.HTTP_PUT,
+                [[{
+                    "max_body_size": 0
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/\{"error_msg":"invalid configuration: property \\"max_body_size\\" validation failed: expected 0 to be sctrictly greater than 0"\}/
+--- no_error_log
+[error]