From f3281336e0bc5c37deec92408cd577b9c27882d8 Mon Sep 17 00:00:00 2001 From: "Ling Samuel (WSL)" Date: Thu, 20 Jul 2023 09:01:05 +0800 Subject: [PATCH] support test server Signed-off-by: Ling Samuel (WSL) --- apisix/plugins/chaitin-waf.lua | 29 +++-- docs/zh/latest/plugins/chaitin-waf.md | 56 ++++++-- t/lib/chaitin_waf_server.lua | 60 +++++++++ t/plugin/chaitin-waf.t | 178 ++++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 26 deletions(-) create mode 100644 t/lib/chaitin_waf_server.lua create mode 100644 t/plugin/chaitin-waf.t diff --git a/apisix/plugins/chaitin-waf.lua b/apisix/plugins/chaitin-waf.lua index 4b53f874b639..484ca296ed8d 100644 --- a/apisix/plugins/chaitin-waf.lua +++ b/apisix/plugins/chaitin-waf.lua @@ -333,17 +333,19 @@ end local function check_match(conf, ctx) local match_passed = true - for _, match in ipairs(conf.match) do - local exp, err = expr.new(match.vars) - if err then - local msg = "failed to create match expression for " .. tostring(match.vars) .. ", err: " .. tostring(err) - core.log.error(msg) - return false, msg - end + if conf.match then + for _, match in ipairs(conf.match) do + local exp, err = expr.new(match.vars) + if err then + local msg = "failed to create match expression for " .. tostring(match.vars) .. ", err: " .. tostring(err) + core.log.error(msg) + return false, msg + end - match_passed = exp:eval(ctx.var) - if match_passed then - break + match_passed = exp:eval(ctx.var) + if match_passed then + break + end end end @@ -424,16 +426,19 @@ local function do_access(conf, ctx) extra_headers[HEADER_CHAITIN_WAF_TIME] = ngx_now() * 1000 - start_time local code = 200 + extra_headers[HEADER_CHAITIN_WAF_STATUS] = code if result then - code = result.status if result.status then + code = result.status extra_headers[HEADER_CHAITIN_WAF_STATUS] = code extra_headers[HEADER_CHAITIN_WAF_ACTION] = "reject" return tonumber(code), fmt(blocked_message, code, result.event_id), extra_headers end end - extra_headers[HEADER_CHAITIN_WAF_STATUS] = code + if not ok then + extra_headers[HEADER_CHAITIN_WAF_STATUS] = nil + end return nil, nil, extra_headers end diff --git a/docs/zh/latest/plugins/chaitin-waf.md b/docs/zh/latest/plugins/chaitin-waf.md index aafe98436f1a..2c4dd08c90b8 100644 --- a/docs/zh/latest/plugins/chaitin-waf.md +++ b/docs/zh/latest/plugins/chaitin-waf.md @@ -246,11 +246,42 @@ curl http://127.0.0.1:9180/apisix/admin/plugin_metadata/chaitin-waf -H 'X-API-KE }' ``` -在没有配置健康检查的情况下,一部分请求会转发到不可用的 WAF 服务器上,从而导致不可用: +在没有配置健康检查的情况下,一部分请求会转发到不可用的 WAF 服务器上,从而导致不可用(该输出开启了 `add_debug_header` 选项): -``` +```bash +curl -H "Host: httpbun.org" -H "waf: true" http://127.0.0.1:9080/get -i +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 427 +Connection: keep-alive +X-APISIX-CHAITIN-WAF: waf-err +X-APISIX-CHAITIN-WAF-SERVER: 127.0.0.1 +X-APISIX-CHAITIN-WAF-TIME: 1 +X-APISIX-CHAITIN-WAF-ACTION: pass +X-APISIX-CHAITIN-WAF-ERROR: failed to connect to t1k server 127.0.0.1:1551: connection refused +Date: Wed, 19 Jul 2023 09:41:20 GMT +X-Powered-By: httpbun/3c0dc05883dd9212ac38b04705037d50b02f2596 +Server: APISIX/3.3.0 +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Connection": "close", + "Host": "httpbun.org", + "User-Agent": "curl/8.1.2", + "Waf": "true", + "X-Forwarded-For": "127.0.0.1", + "X-Forwarded-Host": "httpbun.org", + "X-Forwarded-Port": "9080", + "X-Forwarded-Proto": "http", + "X-Real-Ip": "127.0.0.1" + }, + "method": "GET", + "origin": "127.0.0.1, 122.231.76.178", + "url": "http://httpbun.org/get" +} ``` 添加了健康检查的示例配置如下,此时健康检查将会过滤掉不可用的 WAF 服务器: @@ -291,20 +322,17 @@ curl http://127.0.0.1:9180/apisix/admin/plugin_metadata/chaitin-waf -H 'X-API-KE ## 禁用插件 -当您要禁用 `tencent-waf` 插件时,这很简单,您可以在插件配置中删除相应的 json 配置,无需重新启动服务,它将立即生效: +需要禁用 `tencent-waf` 插件时,在插件配置中删除相应的插件配置即可: ```bash -$ curl http://127.0.0.1:9080/apisix/admin/routes/1 \ --H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { - "methods": ["GET"], - "uri": "/hello", - "plugins": {}, - "upstream": { - "type": "roundrobin", - "nodes": { - "127.0.0.1:1980": 1 - } - } + "uri": "/*", + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbun.org:80": 1 + } + } }' ``` diff --git a/t/lib/chaitin_waf_server.lua b/t/lib/chaitin_waf_server.lua new file mode 100644 index 000000000000..09366403ecc7 --- /dev/null +++ b/t/lib/chaitin_waf_server.lua @@ -0,0 +1,60 @@ +local _M = {} + +function _M.pass(sock) + sock:send({ string.char(65), string.char(1), string.char(0), string.char(0), string.char(0) }) + sock:send(".") + sock:send({ string.char(165), string.char(77), string.char(0), string.char(0), string.char(0) }) + sock:send("{\"event_id\":\"1e902e84bf5a4ead8f7760a0fe2c7719\",\"request_hit_whitelist\":false}") +end + +-- 返回值会被分为如下的形式 + +-- 长度为 5 bytes 的 packet +-- 紧跟一段 body + +-- 其中: +-- 第一个 packet 的 第一个 byte 需要 & 0x40 == 0x40 +-- 最后一个 packet 的 第一个 byte 需要 & 0x80 == 0x80 + +-- tag 为 packet 的第一个 byte & !0x40 & !0x80 + +function _M.ip(sock) + sock:send("HTTP/1.1 200\r\nserver:nginx\r\ncontent-type: application/json\r\ncontent-length: 27\r\n\r\n{\"origin\":\"122.231.76.178\"}") +end + + +function _M.reject(sock) + sock:send({ string.char(65), string.char(1), string.char(0), string.char(0), string.char(0) }) + sock:send("?") + sock:send({ string.char(2), string.char(3), string.char(0), string.char(0), string.char(0) }) + sock:send("403") + sock:send({ string.char(37), string.char(77), string.char(0), string.char(0), string.char(0) }) + sock:send("{\"event_id\":\"b3c6ce574dc24f09a01f634a39dca83b\",\"request_hit_whitelist\":false}") + sock:send({ string.char(35), string.char(79), string.char(0), string.char(0), string.char(0) }) + sock:send("Set-Cookie:sl-session=ulgbPfMSuWRNsi/u7Aj9aA==; Domain=; Path=/; Max-Age=86400\n") + sock:send({ string.char(164), string.char(51), string.char(0), string.char(0), string.char(0) }) + sock:send("") +end + +function _M.go() + local action = "pass" + + local timeout = ngx.var.arg_timeout + if timeout then + ngx.sleep(tonumber(timeout)) + end + + --ngx.log(ngx.ERR, action .. ": waf recv request body size: ", ngx.var.http_content_length) + + ngx.flush(true) + local sock, err = ngx.req.socket(true) + if not sock then + core.log.error("failed to get the request socket: ", err) + return + end + + _M[action](sock) + ngx.exit(200) +end + +return _M diff --git a/t/plugin/chaitin-waf.t b/t/plugin/chaitin-waf.t new file mode 100644 index 000000000000..2101d72fb51e --- /dev/null +++ b/t/plugin/chaitin-waf.t @@ -0,0 +1,178 @@ +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $stream_default_server = <<_EOC_; + listen 8088; + listen 8089; + content_by_lua_block { + require("apisix.chaitin_waf_server").go() + } +_EOC_ + + $block->set_value("stream_server_config", $stream_default_server); + + # setup default conf.yaml + my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_; +apisix: + stream_proxy: # TCP/UDP L4 proxy + only: true # Enable L4 proxy only without L7 proxy. + tcp: + - addr: 9100 # Set the TCP proxy listening ports. + tls: true + - addr: "127.0.0.1:9101" + udp: # Set the UDP proxy listening ports. + - 9200 + - "127.0.0.1:9201" +plugins: + - chaitin-waf +_EOC_ + + $block->set_value("extra_yaml_config", $extra_yaml_config); + + if (!$block->request) { + # use /do instead of /t because stream server will inject a default /t location + $block->set_value("request", "GET /do"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: wrong schema: nodes empty +--- config + location /do { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf', + ngx.HTTP_PUT, + [[{ + "nodes": [] + } + ]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: property \"nodes\" validation failed: expect array to have at least 1 items"} + + + +=== TEST 2: wrong schema: nodes misses host +--- config + location /do { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf', + ngx.HTTP_PUT, + [[{ + "nodes": [ + {} + ] + } + ]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: property \"nodes\" validation failed: failed to validate item 1: property \"host\" is required"} + + + +=== TEST 4: sanity +--- config + location /do { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf', + ngx.HTTP_PUT, + [[{ + "nodes": [ + { + "host": "127.0.0.1", + "port": 8088 + }, + { + "host": "127.0.0.1", + "port": 8089 + } + ] + }]] + ) + + if code >= 300 then + ngx.status = code + return ngx.print(body) + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "plugins": { + "chaitin-waf": { + "upstream": { + "servers": ["httpbun.org"] + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + + +=== test 5: pass +--- request +GET /hello +--- error_code: 200 +--- response_body +hello world +--- error_log +--- response_headers +X-APISIX-CHAITIN-WAF: yes +X-APISIX-CHAITIN-WAF-STATUS: 200 +X-APISIX-CHAITIN-WAF-ACTION: pass +--- response_headers_like +X-APISIX-CHAITIN-WAF-TIME: + + +