From 201c43105d9c43e02629f40b3a0a9df5eccbea62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?= Date: Thu, 10 Nov 2022 09:52:54 +0800 Subject: [PATCH] Add wasm plugin contribution introduction (#47) --- plugins/CONTRIBUTING.md | 46 ++ .../wasm-cpp/extensions/basic_auth/README.md | 110 +++++ .../wasm-cpp/extensions/basic_auth/VERSION | 1 + .../wasm-cpp/extensions/bot_detect/README.md | 75 ++++ .../wasm-cpp/extensions/bot_detect/VERSION | 1 + .../extensions/custom_response/README.md | 80 ++++ .../extensions/custom_response/VERSION | 1 + .../wasm-cpp/extensions/hmac_auth/README.md | 285 +++++++++++++ plugins/wasm-cpp/extensions/hmac_auth/VERSION | 1 + .../wasm-cpp/extensions/jwt_auth/README.md | 400 ++++++++++++++++++ plugins/wasm-cpp/extensions/jwt_auth/VERSION | 1 + .../wasm-cpp/extensions/key_auth/README.md | 121 ++++++ plugins/wasm-cpp/extensions/key_auth/VERSION | 1 + .../extensions/key_rate_limit/README.md | 68 +++ .../extensions/key_rate_limit/VERSION | 1 + .../extensions/request_block/README.md | 86 ++++ .../wasm-cpp/extensions/request_block/VERSION | 1 + .../extensions/sni_misdirect/README.md | 11 + .../wasm-cpp/extensions/sni_misdirect/VERSION | 1 + plugins/wasm-go/Dockerfile | 2 + plugins/wasm-go/README.md | 131 ++++-- plugins/wasm-go/README_EN.md | 103 +++++ .../hello-world/go.mod | 6 +- .../hello-world/go.sum | 0 .../hello-world/main.go | 4 +- .../http-call}/go.mod | 6 +- .../{example => extensions}/http-call/go.sum | 0 .../{example => extensions}/http-call/main.go | 4 +- .../request-block}/go.mod | 6 +- .../request-block/go.sum | 0 .../request-block/main.go | 6 +- plugins/wasm-go/go.mod | 2 +- plugins/wasm-go/pkg/wrapper/plugin_wrapper.go | 53 ++- 33 files changed, 1544 insertions(+), 70 deletions(-) create mode 100644 plugins/CONTRIBUTING.md create mode 100644 plugins/wasm-cpp/extensions/basic_auth/README.md create mode 100644 plugins/wasm-cpp/extensions/basic_auth/VERSION create mode 100644 plugins/wasm-cpp/extensions/bot_detect/README.md create mode 100644 plugins/wasm-cpp/extensions/bot_detect/VERSION create mode 100644 plugins/wasm-cpp/extensions/custom_response/README.md create mode 100644 plugins/wasm-cpp/extensions/custom_response/VERSION create mode 100644 plugins/wasm-cpp/extensions/hmac_auth/README.md create mode 100644 plugins/wasm-cpp/extensions/hmac_auth/VERSION create mode 100644 plugins/wasm-cpp/extensions/jwt_auth/README.md create mode 100644 plugins/wasm-cpp/extensions/jwt_auth/VERSION create mode 100644 plugins/wasm-cpp/extensions/key_auth/README.md create mode 100644 plugins/wasm-cpp/extensions/key_auth/VERSION create mode 100644 plugins/wasm-cpp/extensions/key_rate_limit/README.md create mode 100644 plugins/wasm-cpp/extensions/key_rate_limit/VERSION create mode 100644 plugins/wasm-cpp/extensions/request_block/README.md create mode 100644 plugins/wasm-cpp/extensions/request_block/VERSION create mode 100644 plugins/wasm-cpp/extensions/sni_misdirect/README.md create mode 100644 plugins/wasm-cpp/extensions/sni_misdirect/VERSION create mode 100644 plugins/wasm-go/Dockerfile create mode 100644 plugins/wasm-go/README_EN.md rename plugins/wasm-go/{example => extensions}/hello-world/go.mod (61%) rename plugins/wasm-go/{example => extensions}/hello-world/go.sum (100%) rename plugins/wasm-go/{example => extensions}/hello-world/main.go (84%) rename plugins/wasm-go/{example/request-block => extensions/http-call}/go.mod (60%) rename plugins/wasm-go/{example => extensions}/http-call/go.sum (100%) rename plugins/wasm-go/{example => extensions}/http-call/main.go (94%) rename plugins/wasm-go/{example/http-call => extensions/request-block}/go.mod (60%) rename plugins/wasm-go/{example => extensions}/request-block/go.sum (100%) rename plugins/wasm-go/{example => extensions}/request-block/main.go (92%) diff --git a/plugins/CONTRIBUTING.md b/plugins/CONTRIBUTING.md new file mode 100644 index 000000000..6f3cdf00e --- /dev/null +++ b/plugins/CONTRIBUTING.md @@ -0,0 +1,46 @@ +## Wasm 插件 + +目前 Higress 提供了 c++ 和 golang 两种 Wasm 插件开发框架,支持 Wasm 插件路由&域名级匹配生效。 + +同时提供了多个内置插件,用户可以基于 Higress 提供的官方镜像仓库直接使用这些插件: + +[basic-auth](./wasm-cpp/basic_auth):Basic Auth 认证鉴权 +[key-auth](./wasm-cpp/key_auth):Key 认证鉴权 +[hmac-auth](./wasm-cpp/hmac_auth):Hmac 认证鉴权 +[jwt-auth](./wasm-cpp/jwt_auth): JWT 认证鉴权 +[bot-detect](./wasm-cpp/bot_detect):防互联网爬虫 +[custom-response](./wasm-cpp/custom_response):自定义应答 +[key-rate-limit](./wasm-cpp/key_rate_limit):针对参数的限流 +[request-block](./wasm-cpp/request_block):自定义请求屏蔽 + +使用方式具体可以参考此[文档](./wasm-go/README.md) 中相关说明。 + +所有内置插件都已上传至 Higress 的官方镜像仓库:higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins + +例如用如下配置使用 request-block 插件 的 1.0.0 版本: + +```yaml +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-block + namespace: higress-system +spec: + selector: + matchLabels: + higress: higress-system-higress-gateway + pluginConfig: + block_urls: + - "swagger.html" + url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/request-block:1.0.0 +``` + +## 贡献 Wasm 插件 + +如果您想要为 Higress 贡献插件请参考下述说明。 + +根据你选择的开发语言,将插件代码放到 [wasm-cpp/extensions](./wasm-cpp/extensions) ,或者 [go-cpp/extensions](./wasm-go/extensions) 目录下。 + +除了代码以外,需要额外提供一个 README.md 文件说明插件配置方式,以及 VERSION 文件用于记录插件版本,用作推送镜像时的 tag。 + +提交 PR 后,我们将评估插件的通用性,并对代码逻辑进行审查,确认无误后,会将插件镜像推送到官方仓库,后面将出现在社区的插件市场中。 diff --git a/plugins/wasm-cpp/extensions/basic_auth/README.md b/plugins/wasm-cpp/extensions/basic_auth/README.md new file mode 100644 index 000000000..ebfe09a25 --- /dev/null +++ b/plugins/wasm-cpp/extensions/basic_auth/README.md @@ -0,0 +1,110 @@ +# 功能说明 +`basic-auth`插件实现了基于 HTTP Basic Auth 标准进行认证鉴权的功能 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------- | --------------- | -------- | ------ | ---------------------------------------------------- | +| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 | +| `_rules_` | array of object | 选填 | - | 配置特定路由或域名的访问权限列表,用于对请求进行鉴权 | + +`consumers`中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ------------ | -------- | -------- | ------ | ------------------------ | +| `credential` | string | 必填 | - | 配置该consumer的访问凭证 | +| `name` | string | 必填 | - | 配置该consumer的名称 | + +`_rules_` 中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- | +| `_match_route_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的路由名称 | +| `_match_domain_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的域名 | +| `allow` | array of string | 必填 | - | 对于符合匹配条件的请求,配置允许访问的consumer名称 | + +**注意:** +- 若不配置`_rules_`字段,则默认对当前网关实例的所有路由开启认证; +- 对于通过认证鉴权的请求,请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。 + +# 配置示例 + +## 对特定路由或域名开启认证和鉴权 + +以下配置将对网关特定路由或域名开启 Basic Auth 认证和鉴权,注意凭证信息中的用户名和密码之间使用":"分隔,`credential`字段不能重复 + +```yaml +# 使用 _rules_ 字段进行细粒度规则配置 +consumers: +- credential: 'admin:123456' + name: consumer1 +- credential: 'guest:abc' + name: consumer2 +_rules_: +# 规则一:按路由名称匹配生效 + - _match_route_: + - route-a + - route-b + allow: + - consumer1 +# 规则二:按域名匹配生效 + - _match_domain_: + - "*.example.com" + - test.com + allow: + - consumer2 +``` + +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将允许`name`为`consumer1`的调用者访问,其他调用者不允许访问; + +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将允许`name`为`consumer2`的调用者访问,其他调用者不允许访问。 + +### 根据该配置,下列请求可以允许访问: + +**请求指定用户名密码** + +```bash +# 假设以下请求将会匹配到route-a路由 +# 使用 curl 的 -u 参数指定 +curl -u admin:123456 http://xxx.hello.com/test +# 或者直接指定 Authorization 请求头,用户名密码使用 base64 编码 +curl -H 'Authorization: Basic YWRtaW46MTIzNDU2' http://xxx.hello.com/test +``` + +认证鉴权通过后,请求的header中会被添加一个`X-Mse-Consumer`字段,在此例中其值为`consumer1`,用以标识调用方的名称 + +### 下列请求将拒绝访问: + +**请求未提供用户名密码,返回401** +```bash +curl http://xxx.hello.com/test +``` +**请求提供的用户名密码错误,返回401** +```bash +curl -u admin:abc http://xxx.hello.com/test +``` +**根据请求的用户名和密码匹配到的调用者无访问权限,返回403** +```bash +# consumer2不在route-a的allow列表里 +curl -u guest:abc http://xxx.hello.com/test +``` + +## 网关实例级别开启 + +以下配置未指定`_rules_`字段,因此将对网关实例级别开启 Basic Auth 认证 + +```yaml +consumers: +- credential: 'admin:123456' + name: consumer1 +- credential: 'guest:abc' + name: consumer2 +``` + +# 相关错误码 + +| HTTP 状态码 | 出错信息 | 原因说明 | +| ----------- | ------------------------------------------------------------------------------ | ---------------------- | +| 401 | Request denied by Basic Auth check. No Basic Authentication information found. | 请求未提供凭证 | +| 401 | Request denied by Basic Auth check. Invalid username and/or password | 请求凭证无效 | +| 403 | Request denied by Basic Auth check. Unauthorized consumer | 请求的调用方无访问权限 | diff --git a/plugins/wasm-cpp/extensions/basic_auth/VERSION b/plugins/wasm-cpp/extensions/basic_auth/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/basic_auth/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/bot_detect/README.md b/plugins/wasm-cpp/extensions/bot_detect/README.md new file mode 100644 index 000000000..e3b6922b5 --- /dev/null +++ b/plugins/wasm-cpp/extensions/bot_detect/README.md @@ -0,0 +1,75 @@ +# 功能说明 +`bot-detect`插件可以用于识别并阻止互联网爬虫对站点资源的爬取 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| allow | array of string | 选填 | - | 配置匹配 User-Agent 请求头的正则表达式,匹配命中时将允许其访问 | +| deny | array of string | 选填 | - | 配置匹配 User-Agent 请求头的正则表达式,匹配命中时将屏蔽请求 | +| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 | +| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body | + +`allow` 和 `deny` 字段可以均不配置,则执行默认的爬虫判断逻辑,通过配置 `allow` 字段可以将原本命中默认爬虫判断逻辑的请求放行,通过配置 `deny` 字段可以增加额外的爬虫判断逻辑。 + +默认的爬虫判断正则表达式集合如下: + +```bash +# Bots General matcher 'name/0.0' + (?:\/[A-Za-z0-9\.]+|) {0,5}([A-Za-z0-9 \-_\!\[\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50}))[/ ](\d+)(?:\.(\d+)(?:\.(\d+)|)|) +# Bots General matcher 'name 0.0' + (?:\/[A-Za-z0-9\.]+|) {0,5}([A-Za-z0-9 \-_\!\[\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50})) (\d+)(?:\.(\d+)(?:\.(\d+)|)|) +# Bots containing spider|scrape|bot(but not CUBOT)|Crawl + ((?:[A-z0-9]{1,50}|[A-z\-]{1,50} ?|)(?: the |)(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]{0,50})(?:(?:[ /]| v)(\d+)(?:\.(\d+)|)(?:\.(\d+)|)|) +# Bots Pattern '/name-0.0' + /((?:Ant-)?Nutch|[A-z]+[Bb]ot|[A-z]+[Ss]pider|Axtaris|fetchurl|Isara|ShopSalad|Tailsweep)[ \-](\d+)(?:\.(\d+)(?:\.(\d+))?)? +# Bots Pattern 'name/0.0' + \b(008|Altresium|Argus|BaiduMobaider|BoardReader|DNSGroup|DataparkSearch|EDI|Goodzer|Grub|INGRID|Infohelfer|LinkedInBot|LOOQ|Nutch|OgScrper|PathDefender|Peew|PostPost|Steeler|Twitterbot|VSE|WebCrunch|WebZIP|Y!J-BR[A-Z]|YahooSeeker|envolk|sproose|wminer)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|) +# More bots + (CSimpleSpider|Cityreview Robot|CrawlDaddy|CrawlFire|Finderbots|Index crawler|Job Roboter|KiwiStatus Spider|Lijit Crawler|QuerySeekerSpider|ScollSpider|Trends Crawler|USyd-NLP-Spider|SiteCat Webbot|BotName\/\$BotVersion|123metaspider-Bot|1470\.net crawler|50\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]{1,30}-Agent|AdsBot-Google(?:-[a-z]{1,30}|)|altavista|AppEngine-Google|archive.{0,30}\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]{1,30})(?:-[A-Za-z]{1,30}|)|bingbot|BingPreview|blitzbot|BlogBridge|Bloglovin|BoardReader Blog Indexer|BoardReader Favicon Fetcher|boitho.com-dc|BotSeer|BUbiNG|\b\w{0,30}favicon\w{0,30}\b|\bYeti(?:-[a-z]{1,30}|)|Catchpoint(?: bot|)|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\(S\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher|)|Feed Seeker Bot|Feedbin|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]{1,30}-|)Googlebot(?:-[a-zA-Z]{1,30}|)|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile|)|IconSurf|IlTrovatore(?:-Setaccio|)|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]{1,30}Bot|jbot\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\.RU_Bot|masscan|masidani_bot|Mediapartners-Google|Microsoft .{0,30} Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media {0,2}|)|msrbot|Mtps Feed Aggregation System|netresearch|Netvibes|NewsGator[^/]{0,30}|^NING|Nutch[^/]{0,30}|Nymesis|ObjectsSearch|OgScrper|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Qwantify|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|SemrushBot|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slackbot-LinkExpanding|Slack-ImgProxy|Slurp|snappy|Speedy Spider|Squrl Java|Stringer|TheUsefulbot|ThumbShotsBot|Thumbshots\.ru|Tiny Tiny RSS|Twitterbot|WhatsApp|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]{1,30}|WhatWeb|WIRE|WordPress|Wotbox|www\.almaden\.ibm\.com|Xenu(?:.s|) Link Sleuth|Xerka [A-z]{1,30}Bot|yacy(?:bot|)|YahooSeeker|Yahoo! Slurp|Yandex\w{1,30}|YodaoBot(?:-[A-z]{1,30}|)|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\.ze\.bz|ZooShot|ZyBorg)(?:[ /]v?(\d+)(?:\.(\d+)(?:\.(\d+)|)|)|) +``` + +# 配置示例 + +## 放行原本命中爬虫规则的请求 +```yaml +allow: +- ".*Go-http-client.*" +``` + +若不作该配置,默认的 Golang 网络库请求会被视做爬虫,被禁止访问 + + +## 增加爬虫判断 +```yaml +deny: +- "spd-tools.*" +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com -H 'User-Agent: spd-tools/1.1' +curl http://exmaple.com -H 'User-Agent: spd-tools' +``` + +## 对特定路由或域名开启 +```yaml +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + allow: + - ".*Go-http-client.*" +``` +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置; +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置; +配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。 + + diff --git a/plugins/wasm-cpp/extensions/bot_detect/VERSION b/plugins/wasm-cpp/extensions/bot_detect/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/bot_detect/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/custom_response/README.md b/plugins/wasm-cpp/extensions/custom_response/README.md new file mode 100644 index 000000000..f786be01d --- /dev/null +++ b/plugins/wasm-cpp/extensions/custom_response/README.md @@ -0,0 +1,80 @@ +# 功能说明 +`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 | +| headers | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 | +| body | string | 选填 | - | 自定义 HTTP 应答 Body | +| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 | + +# 配置示例 + +## Mock 应答场景 + +```yaml +status_code: 200 +headers: +- Content-Type=application/json +- Hello=World +body: "{\"hello\":\"world\"}" + +``` + +根据该配置,请求将返回自定义应答如下: + +```text +HTTP/1.1 200 OK +Content-Type: application/json +Hello: World +Content-Length: 17 + +{"hello":"world"} +``` + +## 触发限流时自定义响应 + +```yaml +enable_on_status: +- 429 +status_code: 302 +headers: +- Location=https://example.com +``` + +触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下: + +```text +HTTP/1.1 302 Found +Location: https://example.com +``` + +从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。 + +如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。 + +## 对特定路由或域名开启 +```yaml +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + body: "{\"hello\":\"world\"}" +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + enable_on_status: + - 429 + status_code: 200 + headers: + - Content-Type=application/json + body: "{\"errmsg\": \"rate limited\"}" +``` +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置; +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置; +配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。 diff --git a/plugins/wasm-cpp/extensions/custom_response/VERSION b/plugins/wasm-cpp/extensions/custom_response/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/custom_response/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/hmac_auth/README.md b/plugins/wasm-cpp/extensions/hmac_auth/README.md new file mode 100644 index 000000000..719c3d9ef --- /dev/null +++ b/plugins/wasm-cpp/extensions/hmac_auth/README.md @@ -0,0 +1,285 @@ +# 功能说明 +`hmac-auth`插件实现了基于 HMAC 算法为 HTTP 请求生成不可伪造的签名,并基于签名实现身份认证和鉴权 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ------------- | --------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------- | +| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 | +| `date_offset` | number | 选填 | - | 配置允许的客户端最大时间偏移,单位为秒,根据请求头`Date`解析客户端 UTC 时间,可用于避免请求重放;未配置时,不做校验 | +| `_rules_` | array of object | 选填 | - | 配置特定路由或域名的访问权限列表,用于对请求进行鉴权 | + +`consumers`中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | ------ | ----------------------------------- | +| `key` | string | 必填 | - | 配置从请求的`x-ca-key`头中提取的key | +| `secret` | string | 必填 | - | 配置用于生成签名的secret | +| `name` | string | 必填 | - | 配置该consumer的名称 | + +`_rules_` 中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- | +| `_match_route_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的路由名称 | +| `_match_domain_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的域名 | +| `allow` | array of string | 必填 | - | 对于符合匹配条件的请求,配置允许访问的consumer名称 | + +**注意:** +- 若不配置`_rules_`字段,则默认对当前网关实例的所有路由开启认证; +- 对于通过认证鉴权的请求,请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。 + +# 配置示例 + +以下配置将对网关特定路由或域名开启 Hmac Auth 认证和鉴权,注意`key`字段不能重复 + +## 对特定路由或域名开启 +```yaml +consumers: +- key: appKey-example-1 + secret: appSecret-example-1 + name: consumer-1 +- key: appKey-example-2 + secret: appSecret-example-2 + name: consumer-2 +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + allow: + - consumer-1 +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + allow: + - consumer-2 +``` +每条匹配规则下的`allow`字段用于指定该匹配条件下允许访问的调用者列表; + +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将允许`name`为`consumer-1`的调用者访问,其他调用者不允许访问; + +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将允许`name`为`consumer-2`的调用者访问,其他调用者不允许访问; + +认证成功后,请求的header中会被添加一个`X-Mse-Consumer`字段,其值为调用方的名称,例如`consumer-1`。 + +## 网关实例级别开启 + +以下配置将对网关实例级别开启 Hamc Auth 认证 + +```yaml +consumers: +- key: appKey-example-1 + secret: appSecret-example-1 + name: consumer-1 +- key: appKey-example-2 + secret: appSecret-example-2 + name: consumer-2 +``` + + +# 签名机制说明 + +## 配置准备 + +如上指引,在插件配置中配置生成和验证签名需要用的凭证配置 + +- key: 用于请求头 `x-ca-key` 中设置 +- secret: 用于生成请求签名 + +## 客户端签名生成方式 + +### 流程简介 + +客户端生成签名一共分三步处理: + +1. 从原始请求中提取关键数据,得到一个用来签名的字符串 + +2. 使用加密算法和配置的 `secret` 对关键数据签名串进行加密处理,得到签名 + +3. 将签名所相关的所有头加入到原始HTTP请求中,得到最终HTTP请求 + +如下图所示: +![](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1745707061/p188113.png) + +### 签名串提取流程 + +客户端需要从Http请求中提取出关键数据,组合成一个签名串,生成的签名串的格式如下: + +```text +HTTPMethod +Accept +Content-MD5 +Content-Type +Date +Headers +PathAndParameters +``` + +以上7个字段构成整个签名串,字段之间使用\n间隔,如果Headers为空,则不需要加\n,其他字段如果为空都需要保留\n。签名大小写敏感。下面介绍下每个字段的提取规则: + +- HTTPMethod:HTTP的方法,全部大写,比如POST + +- Accept:请求中的Accept头的值,可为空。建议显式设置 Accept Header。当 Accept 为空时,部分 Http 客户端会给 Accept 设置默认值为 `*/*`,导致签名校验失败。 + +- Content-MD5:请求中的Content-MD5头的值,可为空只有在请求存在Body且Body为非Form形式时才计算Content-MD5头,下面是Java的Content-MD5值的参考计算方式: + +```java +String content-MD5 = Base64.encodeBase64(MD5(bodyStream.getbytes("UTF-8"))); +``` + +- Content-Type:请求中的Content-Type头的值,可为空 + +- Date:请求中的Date头的值,当未开启`date_offset`配置时,可为空,否则将用于时间偏移校验 + +- Headers:用户可以选取指定的header参与签名,关于header的签名串拼接方式有以下规则: + - 参与签名计算的Header的Key按照字典排序后使用如下方式拼接 + ```text + HeaderKey1 + ":" + HeaderValue1 + "\n"\+ + HeaderKey2 + ":" + HeaderValue2 + "\n"\+ + ... + HeaderKeyN + ":" + HeaderValueN + "\n" + ``` + - 某个Header的Value为空,则使用HeaderKey+":"+"\n"参与签名,需要保留Key和英文冒号 + - 所有参与签名的Header的Key的集合使用英文逗号分割放到Key为X-Ca-Signature-Headers的Header中 + - 以下Header不参与Header签名计算:X-Ca-Signature、X-Ca-Signature-Headers、Accept、Content-MD5、Content-Type、Date + +- PathAndParameters: 这个字段包含Path,Query和Form中的所有参数,具体组织形式如下 +```text +Path + "?" + Key1 + "=" + Value1 + "&" + Key2 + "=" + Value2 + ... "&" + KeyN + "=" + ValueN +``` + +注意: +1. Query和Form参数对的Key按照字典排序后使用上面的方式拼接 + +2. Query和Form参数为空时,则直接使用Path,不需要添加? + +3. 参数的Value为空时只保留Key参与签名,等号不需要再加入签名 + +4. Query和Form存在数组参数时(key相同,value不同的参数) ,取第一个Value参与签名计算 + +### 签名串提取示例 + +初始的HTTP请求: +```text +POST /http2test/test?param1=test HTTP/1.1 +host:api.aliyun.com +accept:application/json; charset=utf-8 +ca_version:1 +content-type:application/x-www-form-urlencoded; charset=utf-8 +x-ca-timestamp:1525872629832 +date:Wed, 09 May 2018 13:30:29 GMT+00:00 +user-agent:ALIYUN-ANDROID-DEMO +x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44 +content-length:33 +username=xiaoming&password=123456789 +``` + +生成的正确签名串为: +```text +POST +application/json; charset=utf-8 +application/x-www-form-urlencoded; charset=utf-8 +Wed, 09 May 2018 13:30:29 GMT+00:00 +x-ca-key:203753385 +x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44 +x-ca-signature-method:HmacSHA256 +x-ca-timestamp:1525872629832 +/http2test/test?param1=test&password=123456789&username=xiaoming +``` + +### 签名计算流程 + +客户端从HTTP请求中提取出关键数据组装成签名串后,需要对签名串进行加密及编码处理,形成最终的签名 + +具体的加密形式如下,其中 `stringToSign` 是提取出来的签名串,`secret` 就是插件配置中填写的,`sign` 是最终生成的签名: + +```java +Mac hmacSha256 = Mac.getInstance("HmacSHA256"); +byte[] secretBytes = secret.getBytes("UTF-8"); +hmacSha256.init(new SecretKeySpec(secretBytes, 0, secretBytes.length, "HmacSHA256")); +byte[] result = hmacSha256.doFinal(stringToSign.getBytes("UTF-8")); +String sign = Base64.encodeBase64String(result); +``` + +总结一下,就是将 `stringToSign` 使用UTF-8解码后得到Byte数组,然后使用加密算法对Byte数组进行加密,然后使用Base64算法进行编码,形成最终的签名。 + +### 添加签名流程 + +客户端需要将以下四个Header放在HTTP请求中传输给API网关,进行签名校验: + +- x-ca-key:取值APP Key,必选 + +- x-ca-signature-method:签名算法,取值HmacSHA256或者HmacSHA1,可选,默认值为HmacSHA256 + +- x-ca-signature-headers:所有签名头的Key的集合,使用英文逗号分隔,可选 + +- x-ca-signature:签名,必选 + +下面是携带签名的整个HTTP请求的示例: + +```text +POST /http2test/test?param1=test HTTP/1.1 +host:api.aliyun.com +accept:application/json; charset=utf-8 +ca_version:1 +content-type:application/x-www-form-urlencoded; charset=utf-8 +x-ca-timestamp:1525872629832 +date:Wed, 09 May 2018 13:30:29 GMT+00:00 +user-agent:ALIYUN-ANDROID-DEMO +x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44 +x-ca-key:203753385 +x-ca-signature-method:HmacSHA256 +x-ca-signature-headers:x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method +x-ca-signature:xfX+bZxY2yl7EB/qdoDy9v/uscw3Nnj1pgoU+Bm6xdM= +content-length:33 +username=xiaoming&password=123456789 +``` + +## 服务端签名验证方式 + +### 流程简介 + +服务器验证客户端签名一共分四步处理: + +1. 从接收到的请求中提取关键数据,得到一个用来签名的字符串 + +2. 从接收到的请求中读取 `key` ,通过 `key` 查询到对应的 `secret` + +3. 使用加密算法和 `secret` 对关键数据签名串进行加密处理,得到签名 + +4. 从接收到的请求中读取客户端签名,对比服务器端签名和客户端签名的一致性 + +如下图所示: +![](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1745707061/p188116.png) + + +## 签名排错方法 + +网关签名校验失败时,会将服务端的签名串(StringToSign)放到HTTP Response的Header中返回到客户端,Key为:X-Ca-Error-Message,用户只需要将本地计算的签名串(StringToSign)与服务端返回的签名串进行对比即可找到问题; + +如果服务端与客户端的StringToSign一致请检查用于签名计算的APP Secret是否正确; + +因为HTTP Header中无法表示换行,因此StringToSign中的换行符都被替换成`#`,如下所示: + +```text +X-Ca-Error-Message: Server StringToSign:`GET#application/json##application/json##X-Ca-Key:200000#X-Ca-Timestamp:1589458000000#/app/v1/config/keys?keys=TEST` + +``` + +# 相关错误码 + +| HTTP 状态码 | 出错信息 | 原因说明 | +| ----------- | ---------------------- | -------------------------------------------------------------------------------- | +| 401 | Invalid Key | 请求头未提供 x-ca-key,或者 x-ca-key 无效 | +| 401 | Empty Signature | 请求头未提供 x-ca-signature 签名串 | +| 400 | Invalid Signature | 请求头 x-ca-signature 签名串,与服务端计算得到签名不一致 | +| 400 | Invalid Content-MD5 | 请求头 content-md5 不正确 | +| 400 | Invalid Date | 根据请求头 date 计算时间偏移超过配置的 date_offset | +| 413 | Request Body Too Large | 请求 Body 超过限制大小:32 MB | +| 413 | Payload Too Large | 请求 Body 超过全局配置 DownstreamConnectionBufferLimits | +| 403 | Unauthorized Consumer | 请求的调用方无访问权限 | + + diff --git a/plugins/wasm-cpp/extensions/hmac_auth/VERSION b/plugins/wasm-cpp/extensions/hmac_auth/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/hmac_auth/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/jwt_auth/README.md b/plugins/wasm-cpp/extensions/jwt_auth/README.md new file mode 100644 index 000000000..a8e0da861 --- /dev/null +++ b/plugins/wasm-cpp/extensions/jwt_auth/README.md @@ -0,0 +1,400 @@ +# 功能说明 +`jwt-auth`插件实现了基于JWT(JSON Web Tokens)进行认证鉴权的功能,支持从HTTP请求的URL参数、请求头、Cookie字段解析JWT,同时验证该Token是否有权限访问。 + +本插件和`安全能力->认证鉴权`中JWT认证的区别是,额外提供了调用方身份识别的能力,支持对不同调用方配置不同的JWT凭证。 + +# 详细说明 + +## 1、基于token的认证 + +### 1.1 简介 + +很多对外开放的API需要识别请求者的身份,并据此判断所请求的资源是否可以返回给请求者。token就是一种用于身份验证的机制,基于这种机制,应用不需要在服务端保留用户的认证信息或者会话信息,可实现无状态、分布式的Web应用授权,为应用的扩展提供了便利。 + +### 1.2 流程描述 + +![](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/2336348951/p135822.png) + +上图是网关利用JWT实现认证的整个业务流程时序图,下面我们用文字来详细描述图中标注的步骤: + +1. 客户端向API网关发起认证请求,请求中一般会携带终端用户的用户名和密码; + +2. 网关将请求直接转发给后端服务; + +3. 后端服务读取请求中的验证信息(比如用户名、密码)进行验证,验证通过后使用私钥生成标准的token,返回给网关; + +4. 网关将携带token的应答返回给客户端,客户端需要将这个token缓存到本地; + +5. 客户端向API网关发送业务请求,请求中携带token; + +6. 网关使用用户设定的公钥对请求中的token进行验证,验证通过后,将请求透传给后端服务; + +7. 后端服务进行业务处理后应答; + +8. 网关将业务应答返回给客户端。 + +在这个整个过程中, 网关利用token认证机制,实现了用户使用自己的用户体系对自己API进行授权的能力。下面我们就要介绍网关实现token认证所使用的结构化令牌Json Web Token(JWT)。 + +### 1.3 JWT + +#### 1.3.1 简介 + +Json Web Toke(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准RFC7519。JWT一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。 + +#### 1.3.2 JWT的构成 + +`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ` + +如上面的例子所示,JWT就是一个字符串,由三部分构成: + +- Header(头部) +- Payload(数据) +- Signature(签名) + +**Header** + +JWT的头部承载两个信息: + +- 声明类型,这里是JWT +- 声明加密的算法 + +网关支持的加密算法如下: + +```text +ES256, ES384, ES512, +HS256, HS384, HS512, +RS256, RS384, RS512, +PS256, PS384, PS512, +EdDSA +``` + +完整的头部就像下面这样的JSON: + +```js +{ + 'typ': 'JWT', + 'alg': 'HS256' +} +``` + +然后将头部进行Base64编码(该编码是可以对称解码的),构成了第一部分。 + +`eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9` + +**Payload** + +载荷就是存放有效信息的地方。定义细节如下: + +```text +iss:令牌颁发者。表示该令牌由谁创建,该声明是一个字符串 +sub: Subject Identifier,iss提供的终端用户的标识,在iss范围内唯一,最长为255个ASCII个字符,区分大小写 +aud:Audience(s),令牌的受众,分大小写的字符串数组 +exp:Expiration time,令牌的过期时间戳。超过此时间的token会作废, 该声明是一个整数,是1970年1月1日以来的秒数 +iat: 令牌的颁发时间,该声明是一个整数,是1970年1月1日以来的秒数 +jti: 令牌的唯一标识,该声明的值在令牌颁发者创建的每一个令牌中都是唯一的,为了防止冲突,它通常是一个密码学随机值。这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击。 +``` + +也可以新增用户系统需要使用的自定义字段,比如下面的例子添加了name 用户昵称: + +```js +{ + "sub": "1234567890", + "name": "John Doe" +} +``` + +然后将其进行Base64编码,得到JWT的第二部分: + +`JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE` + +**Signature** + +这个部分需要Base64编码后的Header和Base64编码后的Payload使用 . 连接组成的字符串,然后通过Header中声明的加密方式进行加密($secret 表示用户的私钥),然后就构成了jwt的第三部分。 + +```js +var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); +var signature = HMACSHA256(encodedString, '$secret'); +``` + +将这三部分用 . 连接成一个完整的字符串,就构成了 1.3.2 节最开始的JWT示例。 + + +#### 1.3.3 时效 + +网关会验证token中的exp字段,一旦这个字段过期了,网关会认为这个token无效而将请求直接打回。过期时间这个值必须设置。 + +#### 1.3.4 JWT的几个特点 + +1. JWT 默认是不加密,不能将秘密数据写入 JWT。 +2. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。 +3. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。 +4. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。 +5. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用HTTPS 协议传输。 + +## 2、用户系统如何应用JWT插件保护API + +### 2.1 生成一对JWK(JSON Web 密钥) + +**方法一、在线生成:** + +用户可以在这个站点https://mkjwk.org 生成用于token生成与验证的私钥与公钥, 私钥用于授权服务签发JWT,公钥配置到JWT插件中用于网关对请求验签,注意网关使用的jwks格式配置,下图中Public Key需要放到keys结构体中,如:`{"keys":[{"kty":"RSA","e":"AQAB",...}]}` + + + + +**方法二、本地生成:** + +本文应用Java示例说明,其他语言用户也可以找到相关的工具生成密钥对。 新建一个Maven项目,加入如下依赖: + +```xml + + org.bitbucket.b_c + jose4j + 0.7.0 + +``` + +使用如下的代码生成一对RSA密钥: + +```java +RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); +final String publicKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY); +final String privateKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE); +``` + +### 2.2 使用JWK中的私钥实现颁发token 的认证服务 + +需要使用2.1节中在线生成的 Keypair JSON字符串(三个方框内的第一个)或者本地生成的 privateKeyString JSON字符串作为私钥来颁发token,用于授权可信的用户访问受保护的API,具体实现可以参考下方示例。 向客户颁发token的形式由用户根据具体的业务场景决定,可以将颁发token的功能部署到生产环境,配置成普通API后由访问者通过用户名密码获得,也可以直接在本地环境生成token 后,直接拷贝给指定用户使用。 + +```java +import java.security.PrivateKey; +import org.jose4j.json.JsonUtil; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jwk.RsaJwkGenerator; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.NumericDate; +import org.jose4j.lang.JoseException; +public class GenerateJwtDemo { + public static void main(String[] args) throws JoseException { + String keyId = "uniq_key"; + //使用本文2.1节生成的Keypair + String privateKeyJson = "{\n" + + " \"kty\": \"RSA\",\n" + + " \"d\": " + + + "\"O9MJSOgcjjiVMNJ4jmBAh0mRHF_TlaVva70Imghtlgwxl8BLfcf1S8ueN1PD7xV6Cnq8YenSKsfiNOhC6yZ_fjW1syn5raWfj68eR7cjHWjLOvKjwVY33GBPNOvspNhVAFzeqfWneRTBbga53Agb6jjN0SUcZdJgnelzz5JNdOGaLzhacjH6YPJKpbuzCQYPkWtoZHDqWTzCSb4mJ3n0NRTsWy7Pm8LwG_Fd3pACl7JIY38IanPQDLoighFfo-Lriv5z3IdlhwbPnx0tk9sBwQBTRdZ8JkqqYkxUiB06phwr7mAnKEpQJ6HvhZBQ1cCnYZ_nIlrX9-I7qomrlE1UoQ\",\n" + + " \"e\": \"AQAB\",\n" + + " \"alg\": \"RS256\",\n" + + " \"n\": \"vCuB8MgwPZfziMSytEbBoOEwxsG7XI3MaVMoocziP4SjzU4IuWuE_DodbOHQwb_thUru57_Efe" + + + "--sfATHEa0Odv5ny3QbByqsvjyeHk6ZE4mSAV9BsHYa6GWAgEZtnDceeeDc0y76utXK2XHhC1Pysi2KG8KAzqDa099Yh7s31AyoueoMnrYTmWfEyDsQL_OAIiwgXakkS5U8QyXmWicCwXntDzkIMh8MjfPskesyli0XQD1AmCXVV3h2Opm1Amx0ggSOOiINUR5YRD6mKo49_cN-nrJWjtwSouqDdxHYP-4c7epuTcdS6kQHiQERBd1ejdpAxV4c0t0FHF7MOy9kw\"\n" + + "}"; + JwtClaims claims = new JwtClaims(); + claims.setGeneratedJwtId(); + claims.setIssuedAtToNow(); + //过期时间一定要设置 + NumericDate date = NumericDate.now(); + date.addSeconds(120*60); + claims.setExpirationTime(date); + claims.setNotBeforeMinutesInThePast(1); + claims.setSubject("YOUR_SUBJECT"); + claims.setAudience("YOUR_AUDIENCE"); + //添加自定义参数,所有值请都使用String类型 + claims.setClaim("userId", "1213234"); + claims.setClaim("email", "userEmail@youapp.com"); + JsonWebSignature jws = new JsonWebSignature(); + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + jws.setKeyIdHeaderValue(keyId); + jws.setPayload(claims.toJson()); + PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyJson)).getPrivateKey(); + + jws.setKey(privateKey); + String jwtResult = jws.getCompactSerialization(); + System.out.println("Generate Json Web token , result is " + jwtResult); + } +} +``` + +# 插件配置说明 + +## 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------- | --------------- | ------------------------------------------- | ------ | ----------------------------------------------------------- | +| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 | +| `_rules_` | array of object | 选填 | - | 配置特定路由或域名的访问权限列表,用于对请求进行鉴权 | + +`consumers`中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------------------- | ----------------- | -------- | ------------------------------------------------- | ------------------------ | +| `name` | string | 必填 | - | 配置该consumer的名称 | +| `jwks` | string | 必填 | - | https://www.rfc-editor.org/rfc/rfc7517 指定的json格式字符串,是由验证JWT中签名的公钥(或对称密钥)组成的Json Web Key Set | +| `issuer` | string | 必填 | - | JWT的签发者,需要和payload中的iss字段保持一致 | +| `claims_to_headers` | array of object | 选填 | - | 抽取JWT的payload中指定字段,设置到指定的请求头中转发给后端 | +| `from_headers` | array of object | 选填 | {"name":"Authorization","value_prefix":"Bearer "} | 从指定的请求头中抽取JWT | +| `from_params` | array of string | 选填 | access_token | 从指定的URL参数中抽取JWT | +| `from_cookies` | array of string | 选填 | - | 从指定的cookie中抽取JWT | +| `clock_skew_seconds` | number | 选填 | 60 | 校验JWT的exp和iat字段时允许的时钟偏移量,单位为秒 | +| `keep_token` | bool | 选填 | ture | 转发给后端时是否保留JWT | + +**注意:** +- 只有当`from_headers`,`from_params`,`from_cookies`均未配置时,才会使用默认值 + +`from_headers` 中每一项的配置字段说明如下: +| 名称 | 数据类型 | 填写要求| 默认值 | 描述 | +| ---------------- | --------------- | ------- | ------ | --------------------------------------------------------- | +| `name` | string | 必填 | - | 抽取JWT的请求header | +| `value_prefix` | string | 必填 | - | 对请求header的value去除此前缀,剩余部分作为JWT | + +`claims_to_headers` 中每一项的配置字段说明如下: +| 名称 | 数据类型 | 填写要求| 默认值 | 描述 | +| ---------------- | --------------- | ------- | ------ | --------------------------------------------------------- | +| `claim` | string | 必填 | - | JWT payload中的指定字段,要求必须是字符串或无符号整数类型 | +| `header` | string | 必填 | - | 从payload取出字段的值设置到这个请求头中,转发给后端 | +| `override` | bool | 选填 | true | true时,存在同名请求头会进行覆盖;false时,追加同名请求头 | + + +`_rules_` 中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- | +| `_match_route_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的路由名称 | +| `_match_domain_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的域名 | +| `allow` | array of string | 必填 | - | 对于符合匹配条件的请求,配置允许访问的consumer名称 | + +**注意:** +- 若不配置`_rules_`字段,则默认对当前网关实例的所有路由开启认证; +- 对于通过认证鉴权的请求,请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。 + +## 配置示例 + +### 对特定路由或域名开启 + +以下配置将对网关特定路由或域名开启 Jwt Auth 认证和鉴权,注意如果一个JWT能匹配多个`jwks`,则按照配置顺序命中第一个匹配的`consumer` + +```yaml +consumers: +- name: consumer1 + issuer: abcd + jwks: | + { + "keys": [ + { + "kty": "oct", + "kid": "123", + "k": "hM0k3AbXBPpKOGg__Ql2Obcq7s60myWDpbHXzgKUQdYo7YCRp0gUqkCnbGSvZ2rGEl4YFkKqIqW7mTHdj-bcqXpNr-NOznEyMpVPOIlqG_NWVC3dydBgcsIZIdD-MR2AQceEaxriPA_VmiUCwfwL2Bhs6_i7eolXoY11EapLQtutz0BV6ZxQQ4dYUmct--7PLNb4BWJyQeWu0QfbIthnvhYllyl2dgeLTEJT58wzFz5HeNMNz8ohY5K0XaKAe5cepryqoXLhA-V-O1OjSG8lCNdKS09OY6O0fkyweKEtuDfien5tHHSsHXoAxYEHPFcSRL4bFPLZ0orTt1_4zpyfew", + "alg": "HS256" + } + ] + } +- name: consumer2 + issuer: abc + jwks: | + { + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "123", + "alg": "RS256", + "n": "i0B67f1jggT9QJlZ_8QL9QQ56LfurrqDhpuu8BxtVcfxrYmaXaCtqTn7OfCuca7cGHdrJIjq99rz890NmYFZuvhaZ-LMt2iyiSb9LZJAeJmHf7ecguXS_-4x3hvbsrgUDi9tlg7xxbqGYcrco3anmalAFxsbswtu2PAXLtTnUo6aYwZsWA6ksq4FL3-anPNL5oZUgIp3HGyhhLTLdlQcC83jzxbguOim-0OEz-N4fniTYRivK7MlibHKrJfO3xa_6whBS07HW4Ydc37ZN3Rx9Ov3ZyV0idFblU519nUdqp_inXj1eEpynlxH60Ys_aTU2POGZh_25KXGdF_ZC_MSRw" + } + ] + } +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + allow: + - consumer1 +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + allow: + - consumer2 +``` + +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将允许`name`为`consumer1`的调用者访问,其他调用者不允许访问; + +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将允许`name`为`consumer2`的调用者访问,其他调用者不允许访问。 + +#### 根据该配置,下列请求可以允许访问: + +假设以下请求会匹配到route-a这条路由 + +**将 JWT 设置在 url 参数中** +```bash +curl 'http://xxx.hello.com/test?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4' +``` +**将 JWT 设置在 http 请求头中** +```bash +curl http://xxx.hello.com/test -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4' +``` + +认证鉴权通过后,请求的header中会被添加一个`X-Mse-Consumer`字段,在此例中其值为`consumer1`,用以标识调用方的名称 + +#### 下列请求将拒绝访问: + +**请求未提供JWT,返回401** +```bash +curl http://xxx.hello.com/test +``` + +**根据请求提供的JWT匹配到的调用者无访问权限,返回403** +```bash +# consumer1不在*.example.com的allow列表里 +curl 'http://xxx.example.com/test' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4' +``` + +### 网关实例级别开启 + +以下配置未指定`_rules_`字段,因此将对网关实例级别开启 JWT Auth 认证 + +```yaml +consumers: +- name: consumer1 + issuer: abcd + jwks: | + { + "keys": [ + { + "kty": "oct", + "kid": "123", + "k": "hM0k3AbXBPpKOGg__Ql2Obcq7s60myWDpbHXzgKUQdYo7YCRp0gUqkCnbGSvZ2rGEl4YFkKqIqW7mTHdj-bcqXpNr-NOznEyMpVPOIlqG_NWVC3dydBgcsIZIdD-MR2AQceEaxriPA_VmiUCwfwL2Bhs6_i7eolXoY11EapLQtutz0BV6ZxQQ4dYUmct--7PLNb4BWJyQeWu0QfbIthnvhYllyl2dgeLTEJT58wzFz5HeNMNz8ohY5K0XaKAe5cepryqoXLhA-V-O1OjSG8lCNdKS09OY6O0fkyweKEtuDfien5tHHSsHXoAxYEHPFcSRL4bFPLZ0orTt1_4zpyfew", + "alg": "HS256" + } + ] + } +- name: consumer2 + issuer: abc + jwks: | + { + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "123", + "alg": "RS256", + "n": "i0B67f1jggT9QJlZ_8QL9QQ56LfurrqDhpuu8BxtVcfxrYmaXaCtqTn7OfCuca7cGHdrJIjq99rz890NmYFZuvhaZ-LMt2iyiSb9LZJAeJmHf7ecguXS_-4x3hvbsrgUDi9tlg7xxbqGYcrco3anmalAFxsbswtu2PAXLtTnUo6aYwZsWA6ksq4FL3-anPNL5oZUgIp3HGyhhLTLdlQcC83jzxbguOim-0OEz-N4fniTYRivK7MlibHKrJfO3xa_6whBS07HW4Ydc37ZN3Rx9Ov3ZyV0idFblU519nUdqp_inXj1eEpynlxH60Ys_aTU2POGZh_25KXGdF_ZC_MSRw" + } + ] + } +``` + +# 常见错误码说明 + +| HTTP 状态码 | 出错信息 | 原因说明 | +| ----------- | ---------------------- | -------------------------------------------------------------------------------- | +| 401 | Jwt missing | 请求头未提供JWT | +| 401 | Jwt expired | JWT已经过期 | +| 401 | Jwt verification fails | JWT payload校验失败,如iss不匹配 | +| 403 | Access Denied | 无权限访问当前路由 | + diff --git a/plugins/wasm-cpp/extensions/jwt_auth/VERSION b/plugins/wasm-cpp/extensions/jwt_auth/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/jwt_auth/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/key_auth/README.md b/plugins/wasm-cpp/extensions/key_auth/README.md new file mode 100644 index 000000000..87352f763 --- /dev/null +++ b/plugins/wasm-cpp/extensions/key_auth/README.md @@ -0,0 +1,121 @@ +# 功能说明 +`key-auth`插件实现了基于 API Key 进行认证鉴权的功能,支持从 HTTP 请求的 URL 参数或者请求头解析 API Key,同时验证该 API Key 是否有权限访问。 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------- | --------------- | ------------------------------------------- | ------ | ----------------------------------------------------------- | +| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 | +| `keys` | array of string | 必填 | - | API Key 的来源字段名称,可以是 URL 参数或者 HTTP 请求头名称 | +| `in_query` | bool | `in_query` 和 `in_header` 至少有一个为 true | true | 配置 true 时,网关会尝试从 URL 参数中解析 API Key | +| `in_header` | bool | `in_query` 和 `in_header` 至少有一个为 true | true | 配置 true 时,网关会尝试从 HTTP 请求头中解析 API Key | +| `_rules_` | array of object | 选填 | - | 配置特定路由或域名的访问权限列表,用于对请求进行鉴权 | + +`consumers`中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ------------ | -------- | -------- | ------ | ------------------------ | +| `credential` | string | 必填 | - | 配置该consumer的访问凭证 | +| `name` | string | 必填 | - | 配置该consumer的名称 | + +`_rules_` 中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- | +| `_match_route_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的路由名称 | +| `_match_domain_` | array of string | 选填,`_match_route_`,`_match_domain_`中选填一项 | - | 配置要匹配的域名 | +| `allow` | array of string | 必填 | - | 对于符合匹配条件的请求,配置允许访问的consumer名称 | + +**注意:** +- 若不配置`_rules_`字段,则默认对当前网关实例的所有路由开启认证; +- 对于通过认证鉴权的请求,请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。 + +# 配置示例 + +## 对特定路由或域名开启 + +以下配置将对网关特定路由或域名开启 Key Auth 认证和鉴权,注意`credential`字段不能重复 + +```yaml +consumers: +- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5 + name: consumer1 +- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 + name: consumer2 +keys: +- apikey +- x-api-key +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + allow: + - consumer1 +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + allow: + - consumer2 +``` + +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将允许`name`为`consumer1`的调用者访问,其他调用者不允许访问; + +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将允许`name`为`consumer2`的调用者访问,其他调用者不允许访问。 + +### 根据该配置,下列请求可以允许访问: + +假设以下请求会匹配到route-a这条路由 + +**将 API Key 设置在 url 参数中** +```bash +curl http://xxx.hello.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5 +``` +**将 API Key 设置在 http 请求头中** +```bash +curl http://xxx.hello.com/test -H 'x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5' +``` + +认证鉴权通过后,请求的header中会被添加一个`X-Mse-Consumer`字段,在此例中其值为`consumer1`,用以标识调用方的名称 + +### 下列请求将拒绝访问: + +**请求未提供 API Key,返回401** +```bash +curl http://xxx.hello.com/test +``` +**请求提供的 API Key 无权访问,返回401** +```bash +curl http://xxx.hello.com/test?apikey=926d90ac-ba2e-11ec-ab68-00163e1250b5 +``` + +**根据请求提供的 API Key匹配到的调用者无访问权限,返回403** +```bash +# consumer2不在route-a的allow列表里 +curl http://xxx.hello.com/test?apikey=c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +``` + +## 网关实例级别开启 + +以下配置未指定`_rules_`字段,因此将对网关实例级别开启 Key Auth 认证 + +```yaml +consumers: +- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5 + name: consumer1 +- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 + name: consumer2 +keys: +- apikey +- x-api-key +``` + +# 相关错误码 + +| HTTP 状态码 | 出错信息 | 原因说明 | +| ----------- | --------------------------------------------------------- | ----------------------- | +| 401 | No API key found in request | 请求未提供 API Key | +| 401 | Request denied by Key Auth check. Invalid API key | 不允许当前 API Key 访问 | +| 403 | Request denied by Basic Auth check. Unauthorized consumer | 请求的调用方无访问权限 | diff --git a/plugins/wasm-cpp/extensions/key_auth/VERSION b/plugins/wasm-cpp/extensions/key_auth/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/key_auth/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/key_rate_limit/README.md b/plugins/wasm-cpp/extensions/key_rate_limit/README.md new file mode 100644 index 000000000..9f7b8b2f4 --- /dev/null +++ b/plugins/wasm-cpp/extensions/key_rate_limit/README.md @@ -0,0 +1,68 @@ +# 功能说明 +`key-rate-limit`插件实现了基于特定键值实现限流,键值来源可以是 URL 参数、HTTP 请求头 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| limit_by_header | string | 选填,`limit_by_header`,`limit_by_param` 中选填一项 | - | 配置获取限流键值的来源 http 请求头名称 | +| limit_by_param | string | 选填,`limit_by_header`,`limit_by_param` 中选填一项 | - | 配置获取限流键值的来源 URL 参数名称 | +| limit_keys | array of object | 必填 | - | 配置匹配键值后的限流次数 | + +`limit_keys`中每一项的配置字段说明 +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| key | string | 必填 | - | 匹配的键值 | +| query_per_second | number | 选填,`query_per_second`,`query_per_minute`,`query_per_hour`,`query_per_day` 中选填一项 | - | 允许每秒请求次数 | +| query_per_minute | number | 选填,`query_per_second`,`query_per_minute`,`query_per_hour`,`query_per_day` 中选填一项 | - | 允许每分钟请求次数 | +| query_per_hour | number | 选填,`query_per_second`,`query_per_minute`,`query_per_hour`,`query_per_day` 中选填一项 | - | 允许每小时请求次数 | +| query_per_day | number | 选填,`query_per_second`,`query_per_minute`,`query_per_hour`,`query_per_day` 中选填一项 | - | 允许每天请求次数 | + +# 配置示例 + +## 识别请求参数 apikey,进行区别限流 +```yaml +limit_by_param: apikey +limit_keys: +- key: 9a342114-ba8a-11ec-b1bf-00163e1250b5 + query_per_second: 10 +- key: a6a6d7f2-ba8a-11ec-bec2-00163e1250b5 + query_per_minute: 100 +``` + +## 识别请求头 x-ca-key,进行区别限流 +```yaml +limit_by_header: x-ca-key +limit_keys: +- key: 102234 + query_per_second: 10 +- key: 308239 + query_per_hour: 10 + +``` + +## 对特定路由或域名开启 +```yaml +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + limit_by_header: x-ca-key + limit_keys: + - key: 102234 + query_per_second: 10 +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + limit_by_header: x-ca-key + limit_keys: + - key: 102234 + query_per_second: 100 + +``` +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置; +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置; +配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。 diff --git a/plugins/wasm-cpp/extensions/key_rate_limit/VERSION b/plugins/wasm-cpp/extensions/key_rate_limit/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/key_rate_limit/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/request_block/README.md b/plugins/wasm-cpp/extensions/request_block/README.md new file mode 100644 index 000000000..d334aacd0 --- /dev/null +++ b/plugins/wasm-cpp/extensions/request_block/README.md @@ -0,0 +1,86 @@ +# 功能说明 +`request-block`插件实现了基于 URL、请求头等特征屏蔽 HTTP 请求,可以用于防护部分站点资源不对外部暴露 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| block_urls | array of string | 选填,`block_urls`,`block_headers`,`block_bodys` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的字符串 | +| block_headers | array of string | 选填,`block_urls`,`block_headers`,`block_bodys` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Header 的字符串 | +| block_bodys | array of string | 选填,`block_urls`,`block_headers`,`block_bodys` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Body 的字符串 | +| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 | +| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body | +| case_sensitive | bool | 选填 | true | 配置匹配时是否区分大小写,默认区分 | + +# 配置示例 + +## 屏蔽请求 url 路径 +```yaml +block_urls: +- swagger.html +- foo=bar +case_sensitive: false +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com?foo=Bar +curl http://exmaple.com/Swagger.html +``` + +## 屏蔽请求 header +```yaml +block_headers: +- example-key +- example-value +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com -H 'example-key: 123' +curl http://exmaple.com -H 'my-header: example-value' +``` + +## 屏蔽请求 body +```yaml +block_bodys: +- "hello world" +case_sensitive: false +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com -d 'Hello World' +curl http://exmaple.com -d 'hello world' +``` + +## 对特定路由或域名开启 +```yaml +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + block_bodys: + - "hello world" +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + block_urls: + - "swagger.html" + block_bodys: + - "hello world" +``` +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置; +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置; +配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。 + +# 请求 Body 大小限制 + +当配置了 `block_bodys` 时,仅支持小于 32 MB 的请求 Body 进行匹配。若请求 Body 大于此限制,并且不存在匹配到的 `block_urls` 和 `block_headers` 项时,不会对该请求执行屏蔽操作 +当配置了 `block_bodys` 时,若请求 Body 超过全局配置 DownstreamConnectionBufferLimits,将返回 `413 Payload Too Large` diff --git a/plugins/wasm-cpp/extensions/request_block/VERSION b/plugins/wasm-cpp/extensions/request_block/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/request_block/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-cpp/extensions/sni_misdirect/README.md b/plugins/wasm-cpp/extensions/sni_misdirect/README.md new file mode 100644 index 000000000..eae1f5f53 --- /dev/null +++ b/plugins/wasm-cpp/extensions/sni_misdirect/README.md @@ -0,0 +1,11 @@ +# 功能说明 +`http2-misdirect`插件用于解决网关开启 HTTP2 时,因为浏览器复用连接导致访问出现 404 等问题。 + +# 插件原理 + +HTTP2 协议允许两个不同域名的请求,在域名解析到相同 IP,并且使用了相同证书的情况下,复用同一条连接。这在一些特殊场景会导致复用连接的请求发送给了错误的 Virtual Host 进行处理,从而导致出现 404 等问题。 +本插件基于`HTTP/2 RFC 7540`的`9.1.1`和`9.1.2`章节描述,在发现请求 SNI 与当前 Virtual Host 不匹配时,发送 HTTP 421 状态码,强制浏览器新建连接,并根据当前请求域名生成匹配的 SNI,从而让网关能正确处理路由。 + +# 浏览器兼容性 + +`Safari` 浏览器 `15.1` 版本以下不支持 HTTP 421 状态码,若有此类客户端访问场景,建议对相应域名关闭 HTTP2 的 ALPN diff --git a/plugins/wasm-cpp/extensions/sni_misdirect/VERSION b/plugins/wasm-cpp/extensions/sni_misdirect/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-cpp/extensions/sni_misdirect/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/Dockerfile b/plugins/wasm-go/Dockerfile new file mode 100644 index 000000000..9b084e059 --- /dev/null +++ b/plugins/wasm-go/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +COPY main.wasm plugin.wasm \ No newline at end of file diff --git a/plugins/wasm-go/README.md b/plugins/wasm-go/README.md index a7598499f..71fe232d0 100644 --- a/plugins/wasm-go/README.md +++ b/plugins/wasm-go/README.md @@ -1,48 +1,109 @@ -## Intro +[English](./README_EN.md) -This SDK is used to develop the WASM Plugins of Higress. -## Requirements +## 介绍 -(need support Go's type parameters) +此 SDK 用于开发 Higress 的 Wasm 插件 -Go version: >= 1.18 +## 编译环境要求 -TinyGo version: >= 0.25.0 +(需要支持 go 范型特性) + +Go 版本: >= 1.18 + +TinyGo 版本: >= 0.25.0 ## Quick Examples -### wasm plugin config +使用 [request-block](extensions/request-block) 作为例子 -```yaml -# this config will take effect globally (all incoming requests are affected) -block_urls: -- "test" -_rules_: -# matching by route name takes effect -- _match_route_: - - route-a - - route-b - block_bodys: - - "hello world" -# matching by domain takes effect -- _match_domain_: - - "*.example.com" - - test.com - block_urls: - - "swagger.html" - block_bodys: - - "hello world" -``` - -### code - -[request-block](example/request-block) - - -### compile to wasm +### step1. 编译 wasm ```bash -tinygo build -o main.wasm -scheduler=none -target=wasi ./main.go +tinygo build -o main.wasm -scheduler=none -target=wasi ./extensions/request-block/main.go ``` +### step2. 构建并推送插件的 docker 镜像 + +使用这份简单的 [dockerfile](./Dockerfile). + +```bash +docker build -t /request-block:1.0.0 . +docker push /request-block:1.0.0 +``` + +### step3. 创建 WasmPlugin 资源 + +```yaml +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-block + namespace: higress-system +spec: + selector: + matchLabels: + higress: higress-system-higress-gateway + pluginConfig: + block_urls: + - "swagger.html" + url: oci:///request-block:1.0.0 +``` + +创建上述资源后,如果请求url携带 `swagger.html`, 则这个请求就会被拒绝,例如: + +```bash +curl /api/user/swagger.html +``` + +```text +HTTP/1.1 403 Forbidden +date: Wed, 09 Nov 2022 12:12:32 GMT +server: istio-envoy +content-length: 0 +``` + +如果需要进一步控制插件的执行阶段和顺序 + +可以阅读此 [文档](https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/) 了解更多关于 wasmplugin 的配置 + + +## 路由级或域名级生效 + +```yaml +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-block + namespace: higress-system +spec: + selector: + matchLabels: + higress: higress-system-higress-gateway + pluginConfig: + # 跟上面例子一样,这个配置会全局生效,但如果被下面规则匹配到,则会改为执行命中规则的配置 + block_urls: + - "swagger.html" + _rules_: + # 路由级生效配置 + - _match_route_: + - default/foo + # default 命名空间下名为 foo 的 ingress 会执行下面这个配置 + block_bodys: + - "foo" + - _match_route_: + - default/bar + # default 命名空间下名为 bar 的 ingress 会执行下面这个配置 + block_bodys: + - "bar" + # 域名级生效配置 + - _match_domain_: + - "*.example.com" + # 若请求匹配了上面的域名, 会执行下面这个配置 + block_bodys: + - "foo" + - "bar" + url: oci:///request-block:1.0.0 +``` + +所有规则会按上面配置的顺序一次执行匹配,当有一个规则匹配时,就停止匹配,并选择匹配的配置执行插件逻辑 diff --git a/plugins/wasm-go/README_EN.md b/plugins/wasm-go/README_EN.md new file mode 100644 index 000000000..f4d640466 --- /dev/null +++ b/plugins/wasm-go/README_EN.md @@ -0,0 +1,103 @@ +## Intro + +This SDK is used to develop the WASM Plugins of Higress. +## Requirements + +(need support Go's type parameters) + +Go version: >= 1.18 + +TinyGo version: >= 0.25.0 + +## Quick Examples + +Use the [request-block](example/request-block) as an example + +### step1. compile to wasm + +```bash +tinygo build -o main.wasm -scheduler=none -target=wasi ./example/request-block/main.go +``` + +### step2. build&push docker image + +Use this [dockerfile](./Dockerfile). + +```bash +docker build -t /request-block:1.0.0 . +docker push /request-block:1.0.0 +``` + +### step3. create WasmPlugin resource + +Read this [document](https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/) to learn more about wasmplugin. + +```yaml +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-block + namespace: higress-system +spec: + selector: + matchLabels: + higress: higress-system-higress-gateway + pluginConfig: + block_urls: + - "swagger.html" + url: oci:///request-block:1.0.0 +``` + +If the url in request contains the `swagger.html`, the request will be blocked. + +```bash +curl /api/user/swagger.html +``` + +```text +HTTP/1.1 403 Forbidden +date: Wed, 09 Nov 2022 12:12:32 GMT +server: istio-envoy +content-length: 0 +``` + +## route-level & domain-level takes effect + +```yaml +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-block + namespace: higress-system +spec: + selector: + matchLabels: + higress: higress-system-higress-gateway + pluginConfig: + # this config will take effect globally (all incoming requests not matched by rules below) + block_urls: + - "swagger.html" + _rules_: + # route-level takes effect + - _match_route_: + - default/foo + # the ingress foo in namespace default will use this config + block_bodys: + - "foo" + - _match_route_: + - default/bar + # the ingress bar in namespace default will use this config + block_bodys: + - "bar" + # domain-level takes effect + - _match_domain_: + - "*.example.com" + # if the request's domain matched, this config will be used + block_bodys: + - "foo" + - "bar" + url: oci:///request-block:1.0.0 +``` + +The rules will be matched in the order of configuration. If one match is found, it will stop, and the matching configuration will take effect. + diff --git a/plugins/wasm-go/example/hello-world/go.mod b/plugins/wasm-go/extensions/hello-world/go.mod similarity index 61% rename from plugins/wasm-go/example/hello-world/go.mod rename to plugins/wasm-go/extensions/hello-world/go.mod index 7c290ad00..81dbbdc31 100644 --- a/plugins/wasm-go/example/hello-world/go.mod +++ b/plugins/wasm-go/extensions/hello-world/go.mod @@ -1,11 +1,11 @@ -module github.com/mse-group/wasm-extensions-go/example/hello-world +module github.com/alibaba/higress/plugins/wasm-go/extensions/hello-world go 1.18 -replace github.com/mse-group/wasm-extensions-go => ../.. +replace github.com/alibaba/higress/plugins/wasm-go => ../.. require ( - github.com/mse-group/wasm-extensions-go v0.0.0 + github.com/alibaba/higress/plugins/wasm-go v0.0.0 github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c ) diff --git a/plugins/wasm-go/example/hello-world/go.sum b/plugins/wasm-go/extensions/hello-world/go.sum similarity index 100% rename from plugins/wasm-go/example/hello-world/go.sum rename to plugins/wasm-go/extensions/hello-world/go.sum diff --git a/plugins/wasm-go/example/hello-world/main.go b/plugins/wasm-go/extensions/hello-world/main.go similarity index 84% rename from plugins/wasm-go/example/hello-world/main.go rename to plugins/wasm-go/extensions/hello-world/main.go index 99d10bd3d..7becbb27c 100644 --- a/plugins/wasm-go/example/hello-world/main.go +++ b/plugins/wasm-go/extensions/hello-world/main.go @@ -18,7 +18,7 @@ import ( "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" - "github.com/mse-group/wasm-extensions-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" ) func main() { @@ -31,7 +31,7 @@ func main() { type HelloWorldConfig struct { } -func onHttpRequestHeaders(contextID uint32, config HelloWorldConfig, needBody *bool, log wrapper.LogWrapper) types.Action { +func onHttpRequestHeaders(ctx *wrapper.CommonHttpCtx[HelloWorldConfig], config HelloWorldConfig, needBody *bool, log wrapper.LogWrapper) types.Action { err := proxywasm.AddHttpRequestHeader("hello", "world") if err != nil { log.Critical("failed to set request header") diff --git a/plugins/wasm-go/example/request-block/go.mod b/plugins/wasm-go/extensions/http-call/go.mod similarity index 60% rename from plugins/wasm-go/example/request-block/go.mod rename to plugins/wasm-go/extensions/http-call/go.mod index 544fad5b7..45253da84 100644 --- a/plugins/wasm-go/example/request-block/go.mod +++ b/plugins/wasm-go/extensions/http-call/go.mod @@ -1,11 +1,11 @@ -module github.com/mse-group/wasm-extensions-go/example/request-block +module github.com/alibaba/higress/plugins/wasm-go/extensions/http-call go 1.18 -replace github.com/mse-group/wasm-extensions-go => ../.. +replace github.com/alibaba/higress/plugins/wasm-go => ../.. require ( - github.com/mse-group/wasm-extensions-go v0.0.0 + github.com/alibaba/higress/plugins/wasm-go v0.0.0 github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c github.com/tidwall/gjson v1.14.3 ) diff --git a/plugins/wasm-go/example/http-call/go.sum b/plugins/wasm-go/extensions/http-call/go.sum similarity index 100% rename from plugins/wasm-go/example/http-call/go.sum rename to plugins/wasm-go/extensions/http-call/go.sum diff --git a/plugins/wasm-go/example/http-call/main.go b/plugins/wasm-go/extensions/http-call/main.go similarity index 94% rename from plugins/wasm-go/example/http-call/main.go rename to plugins/wasm-go/extensions/http-call/main.go index 54873a31c..315d72bc4 100644 --- a/plugins/wasm-go/example/http-call/main.go +++ b/plugins/wasm-go/extensions/http-call/main.go @@ -23,7 +23,7 @@ import ( "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" "github.com/tidwall/gjson" - "github.com/mse-group/wasm-extensions-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" ) func main() { @@ -96,7 +96,7 @@ func parseConfig(json gjson.Result, config *HttpCallConfig, log wrapper.LogWrapp } } -func onHttpRequestHeaders(contextID uint32, config HttpCallConfig, needBody *bool, log wrapper.LogWrapper) types.Action { +func onHttpRequestHeaders(ctx *wrapper.CommonHttpCtx[HttpCallConfig], config HttpCallConfig, needBody *bool, log wrapper.LogWrapper) types.Action { config.client.Get(config.requestPath, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { defer proxywasm.ResumeHttpRequest() diff --git a/plugins/wasm-go/example/http-call/go.mod b/plugins/wasm-go/extensions/request-block/go.mod similarity index 60% rename from plugins/wasm-go/example/http-call/go.mod rename to plugins/wasm-go/extensions/request-block/go.mod index 853fc4d04..73f73b1cd 100644 --- a/plugins/wasm-go/example/http-call/go.mod +++ b/plugins/wasm-go/extensions/request-block/go.mod @@ -1,11 +1,11 @@ -module github.com/mse-group/wasm-extensions-go/example/http-call +module github.com/alibaba/higress/plugins/wasm-go/extensions/request-block go 1.18 -replace github.com/mse-group/wasm-extensions-go => ../.. +replace github.com/alibaba/higress/plugins/wasm-go => ../.. require ( - github.com/mse-group/wasm-extensions-go v0.0.0 + github.com/alibaba/higress/plugins/wasm-go v0.0.0 github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c github.com/tidwall/gjson v1.14.3 ) diff --git a/plugins/wasm-go/example/request-block/go.sum b/plugins/wasm-go/extensions/request-block/go.sum similarity index 100% rename from plugins/wasm-go/example/request-block/go.sum rename to plugins/wasm-go/extensions/request-block/go.sum diff --git a/plugins/wasm-go/example/request-block/main.go b/plugins/wasm-go/extensions/request-block/main.go similarity index 92% rename from plugins/wasm-go/example/request-block/main.go rename to plugins/wasm-go/extensions/request-block/main.go index b0e2e1a2b..1d3097aa9 100644 --- a/plugins/wasm-go/example/request-block/main.go +++ b/plugins/wasm-go/extensions/request-block/main.go @@ -23,7 +23,7 @@ import ( "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" "github.com/tidwall/gjson" - "github.com/mse-group/wasm-extensions-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" ) func main() { @@ -93,7 +93,7 @@ func parseConfig(json gjson.Result, config *RequestBlockConfig, log wrapper.LogW return nil } -func onHttpRequestHeaders(contextID uint32, config RequestBlockConfig, needBody *bool, log wrapper.LogWrapper) types.Action { +func onHttpRequestHeaders(ctx *wrapper.CommonHttpCtx[RequestBlockConfig], config RequestBlockConfig, needBody *bool, log wrapper.LogWrapper) types.Action { if len(config.blockUrls) > 0 { requestUrl, err := proxywasm.GetHttpRequestHeader(":path") if err != nil { @@ -137,7 +137,7 @@ func onHttpRequestHeaders(contextID uint32, config RequestBlockConfig, needBody return types.ActionContinue } -func onHttpRequestBody(contextID uint32, config RequestBlockConfig, body []byte, log wrapper.LogWrapper) types.Action { +func onHttpRequestBody(ctx *wrapper.CommonHttpCtx[RequestBlockConfig], config RequestBlockConfig, body []byte, log wrapper.LogWrapper) types.Action { bodyStr := string(body) if !config.caseSensitive { bodyStr = strings.ToLower(bodyStr) diff --git a/plugins/wasm-go/go.mod b/plugins/wasm-go/go.mod index f5361fc9b..40820022c 100644 --- a/plugins/wasm-go/go.mod +++ b/plugins/wasm-go/go.mod @@ -1,4 +1,4 @@ -module github.com/mse-group/wasm-extensions-go +module github.com/alibaba/higress/plugins/wasm-go go 1.18 diff --git a/plugins/wasm-go/pkg/wrapper/plugin_wrapper.go b/plugins/wasm-go/pkg/wrapper/plugin_wrapper.go index c25d4c37e..560363853 100644 --- a/plugins/wasm-go/pkg/wrapper/plugin_wrapper.go +++ b/plugins/wasm-go/pkg/wrapper/plugin_wrapper.go @@ -21,17 +21,18 @@ import ( "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" "github.com/tidwall/gjson" - "github.com/mse-group/wasm-extensions-go/pkg/matcher" + "github.com/alibaba/higress/plugins/wasm-go/pkg/matcher" ) type ParseConfigFunc[PluginConfig any] func(json gjson.Result, config *PluginConfig, log LogWrapper) error -type onHttpHeadersFunc[PluginConfig any] func(contextID uint32, config PluginConfig, needBody *bool, log LogWrapper) types.Action -type onHttpBodyFunc[PluginConfig any] func(contextID uint32, config PluginConfig, body []byte, log LogWrapper) types.Action +type onHttpHeadersFunc[PluginConfig any] func(context *CommonHttpCtx[PluginConfig], config PluginConfig, needBody *bool, log LogWrapper) types.Action +type onHttpBodyFunc[PluginConfig any] func(context *CommonHttpCtx[PluginConfig], config PluginConfig, body []byte, log LogWrapper) types.Action type CommonVmCtx[PluginConfig any] struct { types.DefaultVMContext pluginName string log LogWrapper + hasCustomConfig bool parseConfig ParseConfigFunc[PluginConfig] onHttpRequestHeaders onHttpHeadersFunc[PluginConfig] onHttpRequestBody onHttpBodyFunc[PluginConfig] @@ -81,8 +82,9 @@ func parseEmptyPluginConfig[PluginConfig any](gjson.Result, *PluginConfig, LogWr func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) *CommonVmCtx[PluginConfig] { ctx := &CommonVmCtx[PluginConfig]{ - pluginName: pluginName, - log: LogWrapper{pluginName}, + pluginName: pluginName, + log: LogWrapper{pluginName}, + hasCustomConfig: true, } for _, set := range setFuncs { set(ctx) @@ -94,6 +96,7 @@ func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFu ctx.log.Critical(msg) panic(msg) } + ctx.hasCustomConfig = false ctx.parseConfig = parseEmptyPluginConfig[PluginConfig] } return ctx @@ -117,16 +120,20 @@ func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStart ctx.vm.log.Criticalf("error reading plugin configuration: %v", err) return types.OnPluginStartStatusFailed } + var jsonData gjson.Result if len(data) == 0 { - ctx.vm.log.Warn("need config") - return types.OnPluginStartStatusFailed - } - if !gjson.ValidBytes(data) { - ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data)) - return types.OnPluginStartStatusFailed + if ctx.vm.hasCustomConfig { + ctx.vm.log.Warn("need config") + return types.OnPluginStartStatusFailed + } + } else { + if !gjson.ValidBytes(data) { + ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data)) + return types.OnPluginStartStatusFailed + } + jsonData = gjson.ParseBytes(data) } - jsonData := gjson.ParseBytes(data) err = ctx.ParseRuleConfig(jsonData, func(js gjson.Result, cfg *PluginConfig) error { return ctx.vm.parseConfig(js, cfg, ctx.vm.log) }) @@ -139,8 +146,9 @@ func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStart func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext { httpCtx := &CommonHttpCtx[PluginConfig]{ - plugin: ctx, - contextID: contextID, + plugin: ctx, + contextID: contextID, + userContext: map[string]interface{}{}, } if ctx.vm.onHttpRequestBody != nil { httpCtx.needRequestBody = true @@ -160,6 +168,15 @@ type CommonHttpCtx[PluginConfig any] struct { requestBodySize int responseBodySize int contextID uint32 + userContext map[string]interface{} +} + +func (ctx *CommonHttpCtx[PluginConfig]) SetContext(key string, value interface{}) { + ctx.userContext[key] = value +} + +func (ctx *CommonHttpCtx[PluginConfig]) GetContext(key string) interface{} { + return ctx.userContext[key] } func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { @@ -175,7 +192,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, end if ctx.plugin.vm.onHttpRequestHeaders == nil { return types.ActionContinue } - return ctx.plugin.vm.onHttpRequestHeaders(ctx.contextID, *config, + return ctx.plugin.vm.onHttpRequestHeaders(ctx, *config, &ctx.needRequestBody, ctx.plugin.vm.log) } @@ -198,7 +215,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStr ctx.plugin.vm.log.Warnf("get request body failed: %v", err) return types.ActionContinue } - return ctx.plugin.vm.onHttpRequestBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log) + return ctx.plugin.vm.onHttpRequestBody(ctx, *ctx.config, body, ctx.plugin.vm.log) } func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action { @@ -208,7 +225,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, en if ctx.plugin.vm.onHttpResponseHeaders == nil { return types.ActionContinue } - return ctx.plugin.vm.onHttpResponseHeaders(ctx.contextID, *ctx.config, + return ctx.plugin.vm.onHttpResponseHeaders(ctx, *ctx.config, &ctx.needResponseBody, ctx.plugin.vm.log) } @@ -231,5 +248,5 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfSt ctx.plugin.vm.log.Warnf("get response body failed: %v", err) return types.ActionContinue } - return ctx.plugin.vm.onHttpResponseBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log) + return ctx.plugin.vm.onHttpResponseBody(ctx, *ctx.config, body, ctx.plugin.vm.log) }