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 4346782  feat(limit-count): support multiple variables as key (#5378)
4346782 is described below

commit 4346782a878d5525310a2919f847e7249ad25348
Author: Xunzhuo <[email protected]>
AuthorDate: Thu Nov 4 15:51:37 2021 +0800

    feat(limit-count): support multiple variables as key (#5378)
---
 apisix/plugins/limit-count.lua        |  33 ++++-
 docs/en/latest/plugins/limit-count.md |  34 ++++-
 docs/zh/latest/plugins/limit-count.md |  31 ++++-
 t/control/services.t                  |   2 +-
 t/plugin/limit-count-redis-cluster.t  |   6 +-
 t/plugin/limit-count.t                |   5 +-
 t/plugin/limit-count2.t               | 236 ++++++++++++++++++++++++++++++++++
 7 files changed, 325 insertions(+), 22 deletions(-)

diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua
index d2831c3..1a52ef4 100644
--- a/apisix/plugins/limit-count.lua
+++ b/apisix/plugins/limit-count.lua
@@ -36,11 +36,10 @@ local schema = {
     properties = {
         count = {type = "integer", exclusiveMinimum = 0},
         time_window = {type = "integer",  exclusiveMinimum = 0},
-        key = {
-            type = "string",
-            enum = {"remote_addr", "server_addr", "http_x_real_ip",
-                    "http_x_forwarded_for", "consumer_name", "service_id"},
-            default = "remote_addr",
+        key = {type = "string", default = "remote_addr"},
+        key_type = {type = "string",
+            enum = {"var", "var_combination"},
+            default = "var",
         },
         rejected_code = {
             type = "integer", minimum = 200, maximum = 599, default = 503
@@ -171,7 +170,29 @@ function _M.access(conf, ctx)
         return 500
     end
 
-    local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
+    local conf_key = conf.key
+    local key
+    if conf.key_type == "var_combination" then
+        local err, n_resolved
+        key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var);
+        if err then
+            core.log.error("could not resolve vars in ", conf_key, " error: ", 
err)
+        end
+
+        if n_resolved == 0 then
+            key = nil
+        end
+    else
+        key = ctx.var[conf_key]
+    end
+
+    if key == nil then
+        core.log.info("bypass the limit count as the key is empty")
+        -- Bypass the limit count when the key is empty.
+        -- This behavior is the same as Nginx
+        return
+    end
+    key = key .. ctx.conf_type .. ctx.conf_version
     core.log.info("limit key: ", key)
 
     local delay, remaining = lim:incoming(key, true)
diff --git a/docs/en/latest/plugins/limit-count.md 
b/docs/en/latest/plugins/limit-count.md
index 2a4302e..2c26760 100644
--- a/docs/en/latest/plugins/limit-count.md
+++ b/docs/en/latest/plugins/limit-count.md
@@ -39,7 +39,8 @@ Limit request rate by a fixed number of requests in a given 
time window.
 | ------------------- | ------- | --------------------------------------- | 
------------- | 
-------------------------------------------------------------------------------------------------------
 | 
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | count               | integer | required                                |    
           | count > 0                                                          
                                     | the specified number of requests 
threshold.                                                                      
                                                                                
                                                                                
                          |
 | time_window         | integer | required                    |               
| time_window > 0                                                               
                          | the time window in seconds before the request count 
is reset.                                                                       
                                                                                
                                                                                
       |
-| key                 | string  | optional                                | 
"remote_addr" | ["remote_addr", "server_addr", "http_x_real_ip", 
"http_x_forwarded_for", "consumer_name", "service_id"] | The user specified key 
to limit the count. <br /> Now accept those as key: "remote_addr"(client's IP), 
"server_addr"(server's IP), "X-Forwarded-For/X-Real-IP" in request header, 
"consumer_name"(consumer's username) and "service_id".                          
                                         |
+| key_type      | string  | optional    |   "var"   | ["var", 
"var_combination"] | the type of key. |
+| key           | string  | optional    |     "remote_addr"    |  | the user 
specified key to limit the rate. If the `key_type` is "var", the key will be 
treated as a name of variable. If the `key_type` is "var_combination", the key 
will be a combination of variables. For example, if we use "$remote_addr 
$consumer_name" as keys, plugin will be restricted by two keys which are 
"remote_addr" and "consumer_name". |
 | rejected_code       | integer | optional                                | 
503           | [200,...,599]                                                   
                                        | The HTTP status code returned when 
the request exceeds the threshold is rejected, default 503.                     
                                                                                
                                                                                
                        |
 | rejected_msg       | string | optional                                |      
      | non-empty                                                               
                            | The response body returned when the request 
exceeds the threshold is rejected.                                              
                                                                                
                                                                               |
 | policy              | string  | optional                                | 
"local"       | ["local", "redis", "redis-cluster"]                             
                                        | The rate-limiting policies to use for 
retrieving and incrementing the limits. Available values are `local`(the 
counters will be stored locally in-memory on the node), `redis`(counters are 
stored on a Redis server and will be shared across the nodes, usually use it to 
do the global speed limit) [...]
@@ -53,11 +54,9 @@ Limit request rate by a fixed number of requests in a given 
time window.
 | redis_cluster_nodes | array   | required when policy is `redis-cluster` |    
           |                                                                    
                                     | When using `redis-cluster` policy,This 
property is a list of addresses of Redis cluster service nodes (at least two).  
                                                                                
                                                                                
                [...]
 | redis_cluster_name  | string  | required when policy is `redis-cluster` |    
           |                                                                    
                                     | When using `redis-cluster` policy, this 
property is the name of Redis cluster service nodes.                            
                                                                                
                                                                                
               [...]
 
-**Key can be customized by the user, only need to modify a line of code of the 
plug-in to complete. It is a security consideration that is not open in the 
plugin.**
-
 ## How To Enable
 
-Here's an example, enable the `limit count` plugin on the specified route:
+Here's an example, enable the `limit count` plugin on the specified route when 
setting `key_type` to `var` :
 
 ```shell
 curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
@@ -68,13 +67,38 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 
'X-API-KEY: edd1c9f034335
             "count": 2,
             "time_window": 60,
             "rejected_code": 503,
+            "key_type": "var",
             "key": "remote_addr"
         }
     },
     "upstream": {
         "type": "roundrobin",
         "nodes": {
-            "39.97.63.215:80": 1
+            "127.0.0.1:9001": 1
+        }
+    }
+}'
+```
+
+Here's an example, enable the `limit count` plugin on the specified route when 
setting `key_type` to `var_combination` :
+
+```shell
+curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/index.html",
+    "plugins": {
+        "limit-count": {
+            "count": 2,
+            "time_window": 60,
+            "rejected_code": 503,
+            "key_type": "var_combination",
+            "key": "$consumer_name $remote_addr"
+        }
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:9001": 1
         }
     }
 }'
diff --git a/docs/zh/latest/plugins/limit-count.md 
b/docs/zh/latest/plugins/limit-count.md
index d8e505c..5117364 100644
--- a/docs/zh/latest/plugins/limit-count.md
+++ b/docs/zh/latest/plugins/limit-count.md
@@ -42,7 +42,8 @@ title: limit-count
 | ------------------- | ------- | --------------------------------- | 
------------- | 
-------------------------------------------------------------------------------------------------------
 | 
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [...]
 | count               | integer | 必须                               |           
    | count > 0                                                                 
                              | 指定时间窗口内的请求数量阈值                                  
                                                                                
                                                                                
                                                                                
              [...]
 | time_window         | integer | 必须                               |           
    | time_window > 0                                                           
                              | 时间窗口的大小(以秒为单位),超过这个时间就会重置                       
                                                                                
                                                                                
                                                                                
              [...]
-| key                 | string  | 可选                               | 
"remote_addr" | ["remote_addr", "server_addr", "http_x_real_ip", 
"http_x_forwarded_for", "consumer_name", "service_id"] | 用来做请求计数的有效值。<br 
/>例如,可以使用主机名(或服务器区域)作为关键字,以便限制每个主机名规定时间内的请求次数。我们也可以使用客户端地址作为关键字,这样我们就可以避免单个客户端规定时间内多次的连接我们的服务。<br
 />当前接受的 key 有:"remote_addr"(客户端 IP 地址), "server_addr"(服务端 IP 地址), 
请求头中的"X-Forwarded-For" 或 "X-Real-IP", "consumer_name"(consumer 的 username), 
"service_id" 。 |
+| key_type      | string | 可选   |  "var"      | ["var", "var_combination"]     
                                     | key 的类型 |
+| key           | string  | 可选   |    "remote_addr"    |  | 用来做请求计数的依据。如果 
`key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 
会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 keys,那么插件会同时受 remote_addr 和 
consumer_name 两个 key 的约束。 |
 | rejected_code       | integer | 可选                               | 503       
    | [200,...,599]                                                             
                              | 当请求超过阈值被拒绝时,返回的 HTTP 状态码                        
                                                                                
                                                                                
                                                                                
              [...]
 | rejected_msg       | string | 可选                                |            
| 非空                                                                            
               | 当请求超过阈值被拒绝时,返回的响应体。                                            
                                                                                
                                                                                
 |
 | policy              | string  | 可选                               | "local"   
    | ["local", "redis", "redis-cluster"]                                       
                              | 
用于检索和增加限制的速率限制策略。可选的值有:`local`(计数器被以内存方式保存在节点本地,默认选项) 和 `redis`(计数器保存在 Redis 
服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速);以及`redis-cluster`,跟 redis 功能一样,只是使用 redis 集群方式。  
                                                                                
                                                                      |
@@ -56,13 +57,11 @@ title: limit-count
 | redis_cluster_nodes | array   | 当 policy 为 `redis-cluster` 时必填|              
 |                                                                              
                           | 当使用 `redis-cluster` 限速策略时,该属性是 Redis 
集群服务节点的地址列表(至少需要两个地址)。                                                          
                                                                                
                                                                                
                            [...]
 | redis_cluster_name  | string  | 当 policy 为 `redis-cluster` 时必填 |             
  |                                                                             
                            | 当使用 `redis-cluster` 限速策略时,该属性是 Redis 集群服务节点的名称。   
                                                                                
                                                                                
                                                                                
              [...]
 
-**key 是可以被用户自定义的,只需要修改插件的一行代码即可完成。并没有在插件中放开是处于安全的考虑。**
-
 ## 如何使用
 
 ### 开启插件
 
-下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件:
+下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件,并设置 `key_type` 为 `var`:
 
 ```shell
 curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
@@ -85,6 +84,30 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 
'X-API-KEY: edd1c9f034335
 }'
 ```
 
+下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件,并设置  `key_type` 为 `var_combination`:
+
+```shell
+curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/index.html",
+    "plugins": {
+        "limit-count": {
+            "count": 2,
+            "time_window": 60,
+            "rejected_code": 503,
+            "key_type": "var_combination",
+            "key": "$consumer_name $remote_addr"
+        }
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:9001": 1
+        }
+    }
+}'
+```
+
 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-count 插件:
 ![添加插件](../../../assets/images/plugin/limit-count-1.png)
 
diff --git a/t/control/services.t b/t/control/services.t
index 734afcc..c702a7c 100644
--- a/t/control/services.t
+++ b/t/control/services.t
@@ -155,7 +155,7 @@ services:
         }
     }
 --- response_body
-{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}}
+{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","key_type":"var","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}}
 
 
 
diff --git a/t/plugin/limit-count-redis-cluster.t 
b/t/plugin/limit-count-redis-cluster.t
index 0eb2d48..7b2b04f 100644
--- a/t/plugin/limit-count-redis-cluster.t
+++ b/t/plugin/limit-count-redis-cluster.t
@@ -238,7 +238,7 @@ unlock with key route#1#redis-cluster
                         "limit-count": {
                             "count": 9999,
                             "time_window": 60,
-                            "key": "http_x_real_ip",
+                            "key": "remote_addr",
                             "policy": "redis-cluster",
                             "redis_cluster_nodes": [
                                 "127.0.0.1:5000",
@@ -328,7 +328,7 @@ code: 200
                             "limit-count": {
                                 "count": ]] .. count .. [[,
                                 "time_window": 60,
-                                "key": "http_x_real_ip",
+                                "key": "remote_addr",
                                 "policy": "redis-cluster",
                                 "redis_cluster_nodes": [
                                     "127.0.0.1:5000",
@@ -393,7 +393,7 @@ code: 503
                         "limit-count": {
                             "count": 9999,
                             "time_window": 60,
-                            "key": "http_x_real_ip",
+                            "key": "remote_addr",
                             "policy": "redis-cluster",
                             "allow_degradation": true,
                             "redis_cluster_nodes": [
diff --git a/t/plugin/limit-count.t b/t/plugin/limit-count.t
index 298dbcf..b04642a 100644
--- a/t/plugin/limit-count.t
+++ b/t/plugin/limit-count.t
@@ -56,12 +56,12 @@ done
 
 
 
-=== TEST 2: wrong value of key
+=== TEST 2: set key empty
 --- config
     location /t {
         content_by_lua_block {
             local plugin = require("apisix.plugins.limit-count")
-            local ok, err = plugin.check_schema({count = 2, time_window = 60, 
rejected_code = 503, key = 'host'})
+            local ok, err = plugin.check_schema({count = 2, time_window = 60, 
rejected_code = 503})
             if not ok then
                 ngx.say(err)
             end
@@ -72,7 +72,6 @@ done
 --- request
 GET /t
 --- response_body
-property "key" validation failed: matches none of the enum values
 done
 --- no_error_log
 [error]
diff --git a/t/plugin/limit-count2.t b/t/plugin/limit-count2.t
index e3a2aa0..016fb29 100644
--- a/t/plugin/limit-count2.t
+++ b/t/plugin/limit-count2.t
@@ -178,3 +178,239 @@ GET /hello
 --- error_code: 503
 --- response_body
 {"error_msg":"Requests are too frequent, please try again later."}
+
+
+
+=== TEST 6: update route, use new limit configuration
+--- 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,
+                 [[{
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "http_a",
+                            "key_type": "var"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 7: exceed the burst when key_type is var
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require "t.toolkit.json"
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/hello"
+            local ress = {}
+            for i = 1, 4 do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(uri, {headers = {a = 1}})
+                if not res then
+                    ngx.say(err)
+                    return
+                end
+                table.insert(ress, res.status)
+            end
+            ngx.say(json.encode(ress))
+        }
+    }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200,503,503]
+
+
+
+=== TEST 8: bypass empty key when key_type is var
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require "t.toolkit.json"
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/hello"
+            local ress = {}
+            for i = 1, 4 do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(uri)
+                if not res then
+                    ngx.say(err)
+                    return
+                end
+                table.insert(ress, res.status)
+            end
+            ngx.say(json.encode(ress))
+        }
+    }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200,200,200]
+
+
+
+=== TEST 9: update route, set key type to var_combination
+--- 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,
+                 [[{
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "$http_a $http_b",
+                            "key_type": "var_combination"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: exceed the burst when key_type is var_combination
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require "t.toolkit.json"
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/hello"
+            local ress = {}
+            for i = 1, 4 do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(uri, {headers = {a = 1}})
+                if not res then
+                    ngx.say(err)
+                    return
+                end
+                table.insert(ress, res.status)
+            end
+            ngx.say(json.encode(ress))
+        }
+    }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200,503,503]
+
+
+
+=== TEST 11: don`t exceed the burst when key_type is var_combination
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require "t.toolkit.json"
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/hello"
+            local ress = {}
+            for i = 1, 2 do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(uri, {headers = {a = i}})
+                if not res then
+                    ngx.say(err)
+                    return
+                end
+                table.insert(ress, res.status)
+            end
+            ngx.say(json.encode(ress))
+        }
+    }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[503,200]
+
+
+
+=== TEST 12: bypass empty key when key_type is var_combination
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require "t.toolkit.json"
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port
+                        .. "/hello"
+            local ress = {}
+            for i = 1, 2 do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(uri)
+                if not res then
+                    ngx.say(err)
+                    return
+                end
+                table.insert(ress, res.status)
+            end
+            ngx.say(json.encode(ress))
+        }
+    }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200]
+--- error_log
+bypass the limit count as the key is empty

Reply via email to