diff --git a/services/baiduwaf__waf-web-template/README.md b/services/baiduwaf__waf-web-template/README.md new file mode 100644 index 00000000..45921941 --- /dev/null +++ b/services/baiduwaf__waf-web-template/README.md @@ -0,0 +1,174 @@ +# 百度云 WAF Web Template + +这是一个面向 OctoBus 的百度云 WAF 服务包,当前共封装 **10 个 RPC 接口**,分属以下接口族: + +- `webTemplate`(5 个) +- `whiteRules`(4 个) +- `regionRules`(1 个) + +Service root:`services/baiduwaf__waf-web-template` + +导入方式: + +```bash +octobus service import baiduwaf-waf-web-template ./services/baiduwaf__waf-web-template +``` + +## 包内文件 + +- `service.json`:OctoBus 服务清单 +- `proto/waf_web_template.proto`:gRPC 接口定义 +- `config.schema.json`:非敏感配置(API 地址、超时、TLS、额外 headers) +- `secret.schema.json`:BCE `access_key` / `secret_key` 定义及别名 +- `src/baiduwaf-waf-web-template.js`:百度云 WAF HTTP 代理实现与 BCE 签名逻辑 +- `src/service.js`:OctoBus SDK `defineService` 包装 +- `bin/baiduwaf-waf-web-template.js`:服务入口 +- `test/baiduwaf-waf-web-template.test.js`:请求校验、REST 映射、错误映射与 SDK handler 测试 +- `test/mock_upstream.js`:本地 HTTP mock + +## 接口来源 + +本包中的接口均以百度云 WAF 官方文档为准: + +- `webTemplate`(覆盖 `Detail` / `Save` / `Switch` / `List` / `Delete` 5 个方法):https://cloud.baidu.com/doc/WAF/s/lmmk40psn +- `whiteRules/detail`:https://cloud.baidu.com/doc/WAF/s/2mmj2493g +- `whiteRules/delete`:https://cloud.baidu.com/doc/WAF/s/Fmmj2daee +- `whiteRules/switch`:https://cloud.baidu.com/doc/WAF/s/Jmmj2duto +- `whiteRules/list`:https://cloud.baidu.com/doc/WAF/s/Ammj2f5lf +- `regionRules/list`:https://cloud.baidu.com/doc/WAF/s/ammk1sr1c + + +## 配置说明 + +默认上游地址: + +```json +{ + "api_base": "http://bss.wf.bj.baidubce.com" +} +``` + +支持的 base URL 字段别名: +- `api_base` +- `apiBase` +- `base_url` + +其他可选配置: + +```json +{ + "timeoutMs": 10000, + "skipTlsVerify": false, + "headers": { + "X-Extra": "demo" + } +} +``` + +额外别名: +- `timeout_ms` → `timeoutMs` +- `tlsInsecureSkipVerify` / `insecureSkipVerify` → `skipTlsVerify` + +## 密钥说明 + +通过 secret 传入 BCE 凭据: + +```json +{ + "access_key": "your-ak", + "secret_key": "your-sk" +} +``` + +支持的字段别名: +- `access_key` / `accessKey` / `ak` +- `secret_key` / `secretKey` / `sk` + +## RPC 方法 + +### webTemplate + +- `WebTemplateDetail`:`GET /v1/waf/webTemplate/detail` +- `WebTemplateSave`:`POST /v1/waf/webTemplate/save` +- `WebTemplateSwitch`:`POST /v1/waf/webTemplate/switch` +- `WebTemplateList`:`POST /v1/waf/webTemplate/list` +- `WebTemplateDelete`:`DELETE /v1/waf/webTemplate/delete` + +### whiteRules + +> 当前包保留“按路径自动截取”的 RPC 命名规则,例如 `/v1/waf/whiteRules/detail` → `WhiteRulesdetail`。 + +- `WhiteRulesdetail`:`GET /v1/waf/whiteRules/detail` +- `WhiteRulesdelete`:`DELETE /v1/waf/whiteRules/delete` +- `WhiteRulesswitch`:`POST /v1/waf/whiteRules/switch` +- `WhiteRuleslist`:`POST /v1/waf/whiteRules/list` + +### regionRules + +- `RegionRuleslist`:`POST /v1/waf/regionRules/list` + +## 请求说明 + +### `WhiteRuleslist` + +必填字段: +- `pageNo` +- `pageSize` + +可选字段: +- `switch` +- `subdomain` +- `ruleID` +- `ruleName` + +### `RegionRuleslist` + +必填字段: +- `pageNo` +- `pageSize` + +可选字段: +- `switch` +- `subdomain` +- `ruleID` +- `ruleName` +- `action` + +## 风险说明 + +按当前 package 约定,仅查询类接口视为低风险;修改类接口视为高风险。 + +高风险 RPC: +- `WebTemplateSave` +- `WebTemplateSwitch` +- `WebTemplateDelete` +- `WhiteRulesdelete` +- `WhiteRulesswitch` + +## 运行行为说明 + +- 所有请求使用 BCE AK/SK 签名 +- 默认使用 HTTP 访问上游 +- GET/DELETE 会对 query 参数参与签名 +- POST 会对 path 签名,并发送 JSON body +- HTTP 401/403 映射为 `PERMISSION_DENIED` +- 其他 HTTP 4xx 映射为 `FAILED_PRECONDITION` +- HTTP 5xx、网络异常、TLS 异常映射为 `UNAVAILABLE` +- 成功响应但返回非 JSON 时映射为 `UNKNOWN` + +## 容器内刷新说明 + +如果在运行中的 OctoBus 容器里重装了这个 service,为了让 capset 立即感知新增或删除的方法,建议刷新一次实例挂载: + +```bash +docker exec octobus sh -lc 'octobus capset remove-instance baiduwaf-dev baiduwaf-test' +docker exec octobus sh -lc 'octobus capset add-instance baiduwaf-dev baiduwaf-test' +docker exec octobus sh -lc 'octobus capset list-methods baiduwaf-dev' +``` + +## 本地检查 + +```bash +cd OctoBus/services +npm test -- --service-dir baiduwaf__waf-web-template +``` diff --git a/services/baiduwaf__waf-web-template/bin/baiduwaf-waf-web-template.js b/services/baiduwaf__waf-web-template/bin/baiduwaf-waf-web-template.js new file mode 100755 index 00000000..508272f0 --- /dev/null +++ b/services/baiduwaf__waf-web-template/bin/baiduwaf-waf-web-template.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../src/service.js"; + +runServiceMain(service); diff --git a/services/baiduwaf__waf-web-template/config.schema.json b/services/baiduwaf__waf-web-template/config.schema.json new file mode 100644 index 00000000..633b0bc3 --- /dev/null +++ b/services/baiduwaf__waf-web-template/config.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "api_base": { + "type": "string", + "description": "API 基础地址,默认值为 http://bss.wf.bj.baidubce.com" + }, + "apiBase": { + "type": "string", + "description": "api_base 的别名(camelCase)" + }, + "base_url": { + "type": "string", + "description": "api_base 的别名(snake_case)" + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 10000, + "description": "HTTP 请求超时时间(毫秒),默认 10000" + }, + "timeout_ms": { + "type": "integer", + "minimum": 1, + "description": "timeoutMs 的别名(snake_case)" + }, + "skipTlsVerify": { + "type": "boolean", + "default": false, + "description": "仅在你手动改用 HTTPS 且需要忽略证书校验时启用;默认 HTTP 场景不需要" + }, + "tlsInsecureSkipVerify": { + "type": "boolean", + "default": false, + "description": "skipTlsVerify 的别名" + }, + "insecureSkipVerify": { + "type": "boolean", + "default": false, + "description": "skipTlsVerify 的别名" + }, + "headers": { + "type": "object", + "additionalProperties": { "type": "string" }, + "description": "可选的额外 HTTP headers,会与内置 headers 合并" + } + } +} diff --git a/services/baiduwaf__waf-web-template/package.json b/services/baiduwaf__waf-web-template/package.json new file mode 100644 index 00000000..eb15dc78 --- /dev/null +++ b/services/baiduwaf__waf-web-template/package.json @@ -0,0 +1,12 @@ +{ + "name": "baiduwaf-waf-web-template", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "baiduwaf-waf-web-template": "bin/baiduwaf-waf-web-template.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0" + } +} diff --git a/services/baiduwaf__waf-web-template/proto/waf_web_template.proto b/services/baiduwaf__waf-web-template/proto/waf_web_template.proto new file mode 100644 index 00000000..00545d88 --- /dev/null +++ b/services/baiduwaf__waf-web-template/proto/waf_web_template.proto @@ -0,0 +1,236 @@ +syntax = "proto3"; + +package BaiduWAF_WAFWebTemplate; + +option go_package = "miner/grpc-service/BaiduWAF_WAFWebTemplate"; + +// BaiduWAF_WAFWebTemplate - 百度云 WAF 基础防护模板、白名单规则与区域封禁规则操作 +service BaiduWAF_WAFWebTemplate { + rpc WebTemplateDetail(WebTemplateDetailRequest) returns (WebTemplateDetailResponse); + rpc WebTemplateSave(WebTemplateSaveRequest) returns (WebTemplateSaveResponse); + rpc WebTemplateSwitch(WebTemplateSwitchRequest) returns (WebTemplateSwitchResponse); + rpc WebTemplateList(WebTemplateListRequest) returns (WebTemplateListResponse); + rpc WebTemplateDelete(WebTemplateDeleteRequest) returns (WebTemplateDeleteResponse); + rpc WhiteRulesdetail(WhiteRulesdetailRequest) returns (WhiteRulesdetailResponse); + rpc WhiteRulesdelete(WhiteRulesdeleteRequest) returns (WhiteRulesdeleteResponse); + rpc WhiteRulesswitch(WhiteRulesswitchRequest) returns (WhiteRulesswitchResponse); + rpc WhiteRuleslist(WhiteRuleslistRequest) returns (WhiteRuleslistResponse); + rpc RegionRuleslist(RegionRuleslistRequest) returns (RegionRuleslistResponse); +} + +message WebTemplateDetailRequest { + string template_key = 1 [json_name = "templateKey"]; +} + +message WebTemplateDetailResponse { + int32 status = 1 [json_name = "status"]; + WebTemplateDetailResult result = 2 [json_name = "result"]; + bool success = 3 [json_name = "success"]; +} + +message WebTemplateDetailResult { + int32 switch = 1 [json_name = "switch"]; + repeated string protection_domains = 2 [json_name = "protectionDomains"]; + string template_type = 3 [json_name = "templateType"]; + string template_key = 4 [json_name = "templateKey"]; + string action = 5 [json_name = "action"]; + string group_key = 6 [json_name = "groupKey"]; + string rule_name = 7 [json_name = "ruleName"]; + int32 rule_id = 8 [json_name = "ruleID"]; + string group_name = 9 [json_name = "groupName"]; +} + +message WebTemplateBindInfoItem { + string instance_id = 1 [json_name = "instanceID"]; + repeated string subdomains = 2 [json_name = "subdomains"]; +} + +message WebTemplateSaveRequest { + string name = 1 [json_name = "name"]; + repeated WebTemplateBindInfoItem bind_info = 2 [json_name = "bindInfo"]; + int32 switch = 3 [json_name = "switch"]; + string template_type = 4 [json_name = "templateType"]; + string action = 5 [json_name = "action"]; + string template_key = 6 [json_name = "templateKey"]; + string rules_group_id = 7 [json_name = "rulesGroupID"]; +} + +message WebTemplateSaveResponse { + int32 status = 1 [json_name = "status"]; + WebTemplateSaveResult result = 2 [json_name = "result"]; +} + +message WebTemplateSaveResult { + string template_key = 1 [json_name = "templateKey"]; +} + +message WebTemplateSwitchRequest { + string template_key = 1 [json_name = "templateKey"]; + int32 switch = 2 [json_name = "switch"]; +} + +message WebTemplateSwitchResponse { + bool success = 1 [json_name = "success"]; + int32 status = 2 [json_name = "status"]; +} + +message WebTemplateListRequest { + repeated string subdomains = 1 [json_name = "subdomains"]; + int32 page_no = 2 [json_name = "pageNo"]; + int32 page_size = 3 [json_name = "pageSize"]; + int32 rule_id = 4 [json_name = "ruleID"]; + int32 switch = 5 [json_name = "switch"]; + string action = 6 [json_name = "action"]; + string template_name = 7 [json_name = "templateName"]; +} + +message WebTemplateListResponse { + int32 status = 1 [json_name = "status"]; + bool success = 2 [json_name = "success"]; + WebTemplateListResult result = 3 [json_name = "result"]; +} + +message WebTemplateListResult { + repeated WebTemplateListItem result = 1 [json_name = "result"]; + int32 total_count = 2 [json_name = "totalCount"]; +} + +message WebTemplateListItem { + string rule_name = 1 [json_name = "ruleName"]; + string template_key = 2 [json_name = "templateKey"]; + string template_type = 3 [json_name = "templateType"]; + repeated string protection_domains = 4 [json_name = "protectionDomains"]; + string action = 5 [json_name = "action"]; + int32 switch = 6 [json_name = "switch"]; + string update_time = 7 [json_name = "updateTime"]; + int32 rule_id = 8 [json_name = "ruleID"]; +} + +message WebTemplateDeleteRequest { + string template_key = 1 [json_name = "templateKey"]; +} + +message WebTemplateDeleteResponse { + bool success = 1 [json_name = "success"]; + WebTemplateDeleteResult result = 2 [json_name = "result"]; + bool already_gone = 3 [json_name = "alreadyGone"]; +} + +message WebTemplateDeleteResult {} + +message WhiteRulesdetailRequest { + optional string rule_key = 1 [json_name = "ruleKey"]; +} + +message WhiteRulesdetailResponse { + bool success = 1 [json_name = "success"]; + int32 status = 2 [json_name = "status"]; + WhiteRulesdetailResult result = 3 [json_name = "result"]; +} + +message WhiteRulesdetailResult { + string rule_name = 1 [json_name = "ruleName"]; + int32 rule_id = 2 [json_name = "ruleID"]; + string rule_type = 3 [json_name = "ruleType"]; + repeated string protection_domains = 4 [json_name = "protectionDomains"]; + int32 switch = 5 [json_name = "switch"]; + string update_time = 6 [json_name = "updateTime"]; + repeated WhiteRulesdetailTarget targets = 7 [json_name = "targets"]; +} + +message WhiteRulesdetailTarget { + string field = 1 [json_name = "field"]; + string key = 2 [json_name = "key"]; + string match = 3 [json_name = "match"]; + repeated string value = 4 [json_name = "value"]; +} + +message WhiteRulesdeleteRequest { + optional string rule_key = 1 [json_name = "ruleKey"]; +} + +message WhiteRulesdeleteResponse { + bool success = 1 [json_name = "success"]; + repeated string result = 2 [json_name = "result"]; + bool already_gone = 3 [json_name = "alreadyGone"]; +} + +message WhiteRulesswitchRequest { + optional string rule_key = 1 [json_name = "ruleKey"]; + optional int32 switch = 2 [json_name = "switch"]; +} + +message WhiteRulesswitchResponse { + bool success = 1 [json_name = "success"]; + repeated string result = 2 [json_name = "result"]; +} + +message WhiteRuleslistRequest { + optional int32 page_no = 1 [json_name = "pageNo"]; + optional int32 page_size = 2 [json_name = "pageSize"]; + optional int32 switch = 3 [json_name = "switch"]; + repeated string subdomain = 4 [json_name = "subdomain"]; + optional string rule_id = 5 [json_name = "ruleID"]; + optional string rule_name = 6 [json_name = "ruleName"]; +} + +message WhiteRuleslistResponse { + bool success = 1 [json_name = "success"]; + int32 status = 2 [json_name = "status"]; + WhiteRuleslistResult result = 3 [json_name = "result"]; +} + +message WhiteRuleslistResult { + repeated WhiteRuleslistItem result = 1 [json_name = "result"]; + int32 total_count = 2 [json_name = "totalCount"]; +} + +message WhiteRuleslistItem { + string rule_name = 1 [json_name = "ruleName"]; + string rule_type = 2 [json_name = "ruleType"]; + repeated string protection_domains = 3 [json_name = "protectionDomains"]; + int32 switch = 4 [json_name = "switch"]; + string update_time = 5 [json_name = "updateTime"]; + string rule_key = 6 [json_name = "ruleKey"]; + int32 rule_id = 7 [json_name = "ruleID"]; + repeated string ignore_modules = 8 [json_name = "ignoreModules"]; + repeated string ignore_ids = 9 [json_name = "ignoreIds"]; +} + +message RegionRuleslistRequest { + optional int32 page_no = 1 [json_name = "pageNo"]; + optional int32 page_size = 2 [json_name = "pageSize"]; + optional int32 switch = 3 [json_name = "switch"]; + repeated string subdomain = 4 [json_name = "subdomain"]; + optional string rule_id = 5 [json_name = "ruleID"]; + optional string rule_name = 6 [json_name = "ruleName"]; + optional string action = 7 [json_name = "action"]; +} + +message RegionRuleslistResponse { + bool success = 1 [json_name = "success"]; + int32 status = 2 [json_name = "status"]; + RegionRuleslistResult result = 3 [json_name = "result"]; +} + +message RegionRuleslistResult { + repeated RegionRuleslistItem result = 1 [json_name = "result"]; + optional int32 total_count = 2 [json_name = "totalCount"]; +} + +message RegionRuleslistItem { + string rule_name = 1 [json_name = "ruleName"]; + repeated string protection_domains = 2 [json_name = "protectionDomains"]; + optional int32 switch = 3 [json_name = "switch"]; + string update_time = 4 [json_name = "updateTime"]; + string rule_key = 5 [json_name = "ruleKey"]; + int32 rule_id = 6 [json_name = "ruleID"]; + string rule_type = 7 [json_name = "ruleType"]; + string action = 8 [json_name = "action"]; + RegionRuleslistValue value = 9 [json_name = "value"]; +} + +message RegionRuleslistValue { + repeated string domestic = 1 [json_name = "domestic"]; + repeated string overseas = 2 [json_name = "overseas"]; +} diff --git a/services/baiduwaf__waf-web-template/secret.schema.json b/services/baiduwaf__waf-web-template/secret.schema.json new file mode 100644 index 00000000..ebcf37f7 --- /dev/null +++ b/services/baiduwaf__waf-web-template/secret.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "access_key": { + "type": "string", + "description": "百度云 BCE Access Key" + }, + "accessKey": { + "type": "string", + "description": "access_key 的别名(camelCase)" + }, + "ak": { + "type": "string", + "description": "access_key 的别名(简写)" + }, + "secret_key": { + "type": "string", + "description": "百度云 BCE Secret Key" + }, + "secretKey": { + "type": "string", + "description": "secret_key 的别名(camelCase)" + }, + "sk": { + "type": "string", + "description": "secret_key 的别名(简写)" + } + } +} diff --git a/services/baiduwaf__waf-web-template/service.json b/services/baiduwaf__waf-web-template/service.json new file mode 100644 index 00000000..0efa50e9 --- /dev/null +++ b/services/baiduwaf__waf-web-template/service.json @@ -0,0 +1,59 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "baiduwaf-waf-web-template", + "displayName": "百度云 WAF Web Template", + "description": "封装百度云 WAF 基础防护模板、白名单规则与区域封禁规则的详情、保存、开关切换、列表与删除接口。⚠️ 高风险操作:所有非查询类接口都会直接修改线上配置或数据。", + "runtime": { "mode": "long-running" }, + "proto": { + "roots": ["proto"], + "files": ["proto/waf_web_template.proto"] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateDetail": { + "name": "web-template-detail", + "description": "查询百度云 WAF 基础防护模板详情" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateSave": { + "name": "web-template-save", + "description": "保存百度云 WAF 基础防护模板。⚠️ 高风险操作:会直接修改模板配置" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateSwitch": { + "name": "web-template-switch", + "description": "切换百度云 WAF 基础防护模板开关。⚠️ 高风险操作:会直接修改模板状态" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateList": { + "name": "web-template-list", + "description": "分页查询百度云 WAF 基础防护模板列表" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateDelete": { + "name": "web-template-delete", + "description": "删除百度云 WAF 基础防护模板。⚠️ 高风险操作:会永久删除模板" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesdetail": { + "name": "white-rulesdetail", + "description": "查询百度云 WAF 白名单规则详情" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesdelete": { + "name": "white-rulesdelete", + "description": "删除百度云 WAF 白名单规则。⚠️ 高风险操作:会直接删除白名单规则" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesswitch": { + "name": "white-rulesswitch", + "description": "切换百度云 WAF 白名单规则开关。⚠️ 高风险操作:会直接修改规则状态" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRuleslist": { + "name": "white-ruleslist", + "description": "分页查询百度云 WAF 白名单规则列表" + }, + "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/RegionRuleslist": { + "name": "region-ruleslist", + "description": "分页查询百度云 WAF 区域封禁规则列表" + } + } + } + } +} diff --git a/services/baiduwaf__waf-web-template/src/baiduwaf-waf-web-template.js b/services/baiduwaf__waf-web-template/src/baiduwaf-waf-web-template.js new file mode 100644 index 00000000..0f9dbda7 --- /dev/null +++ b/services/baiduwaf__waf-web-template/src/baiduwaf-waf-web-template.js @@ -0,0 +1,1199 @@ +import http from "node:http"; +import https from "node:https"; +import { createHmac } from "node:crypto"; + +import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; + +// ============================================================ +// Method path constants +// ============================================================ +export const WEBTEMPLATEDETAIL_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateDetail"; +export const METHOD_WEBTEMPLATEDETAIL_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateDetail"; +export const WEBTEMPLATESAVE_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateSave"; +export const METHOD_WEBTEMPLATESAVE_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateSave"; +export const WEBTEMPLATESWITCH_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateSwitch"; +export const METHOD_WEBTEMPLATESWITCH_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateSwitch"; +export const WEBTEMPLATELIST_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateList"; +export const METHOD_WEBTEMPLATELIST_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateList"; +export const WEBTEMPLATEDELETE_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateDelete"; +export const METHOD_WEBTEMPLATEDELETE_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WebTemplateDelete"; +export const WHITERULESDETAIL_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesdetail"; +export const METHOD_WHITERULESDETAIL_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesdetail"; +export const WHITERULESDELETE_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesdelete"; +export const METHOD_WHITERULESDELETE_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesdelete"; +export const WHITERULESSWITCH_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesswitch"; +export const METHOD_WHITERULESSWITCH_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRulesswitch"; +export const WHITERULESLIST_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRuleslist"; +export const METHOD_WHITERULESLIST_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/WhiteRuleslist"; +export const REGIONRULESLIST_PATH = "/BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/RegionRuleslist"; +export const METHOD_REGIONRULESLIST_FULL = "BaiduWAF_WAFWebTemplate.BaiduWAF_WAFWebTemplate/RegionRuleslist"; +export const DEFAULT_TIMEOUT_MS = 10000; +export const DEFAULT_API_BASE = "http://bss.wf.bj.baidubce.com"; +export const BCE_CONTENT_TYPE = "application/json;charset=utf-8"; +export const BCE_EXPIRE_SECONDS = 1800; +export const BCE_SIGNED_HEADERS = "content-type;host;x-bce-date"; + +// ============================================================ +// Generic utility functions +// ============================================================ + +const grpcCodeFor = (code) => ({ + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + UNKNOWN: grpcStatus.UNKNOWN, +})[code] ?? grpcStatus.UNKNOWN; + +const errorWithCode = (code, message) => { + const err = new GrpcError(grpcCodeFor(code), message); + err.legacyCode = code; + return err; +}; + +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key); +const firstDefined = (...vals) => vals.find((val) => val !== undefined && val !== null); + +const unwrapString = (value) => { + if (value === undefined || value === null) return ""; + if (typeof value === "object" && value !== null && hasOwn(value, "value")) { + return unwrapString(value.value); + } + return String(value); +}; + +const pickString = (obj, keys) => { + for (const key of keys) { + if (!hasOwn(obj, key)) continue; + const value = unwrapString(obj[key]).trim(); + if (value) return value; + } + return ""; +}; + +const mapStringArray = (value) => Array.isArray(value) ? value.map(unwrapString).filter((v) => v) : []; +const pickStringArray = (obj, keys) => { + for (const key of keys) { + if (!hasOwn(obj, key)) continue; + return mapStringArray(obj[key]); + } + return []; +}; + +const normalizeBaseUrl = (value) => { + const raw = unwrapString(value).trim(); + if (!/^https?:\/\//i.test(raw)) return ""; + return raw.replace(/\/+$/, ""); +}; + +const mergedBindings = (ctx = {}) => ({ + ...(ctx?.config ?? {}), + ...(ctx?.secret ?? {}), + ...(ctx?.bindings ?? {}), +}); + +const resolveSdkReq = (ctx = {}) => { + if (ctx?.req && Object.keys(ctx.req).length) return ctx.req; + return ctx?.request ?? {}; +}; + +const resolveCallContext = (ctx = {}) => ({ + ...ctx, + bindings: mergedBindings(ctx), + limits: ctx.limits ?? {}, + meta: ctx.meta ?? {}, + req: resolveSdkReq(ctx), +}); + +const resolveTimeoutMs = (ctx) => { + const bindings = ctx?.bindings ?? mergedBindings(ctx); + const raw = Number(firstDefined(ctx?.limits?.timeoutMs, bindings.timeoutMs, bindings.timeout_ms, DEFAULT_TIMEOUT_MS)); + return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS; +}; + +const resolveAccessKey = (bindings = {}) => pickString(bindings, ["access_key", "accessKey", "ak"]); +const resolveSecretKey = (bindings = {}) => pickString(bindings, ["secret_key", "secretKey", "sk"]); + +const requireBceCredentials = (ctx) => { + const bindings = mergedBindings(ctx); + const accessKey = resolveAccessKey(bindings); + const secretKey = resolveSecretKey(bindings); + if (!accessKey) throw errorWithCode("INVALID_ARGUMENT", "access_key is required in bindings (config or secret)"); + if (!secretKey) throw errorWithCode("INVALID_ARGUMENT", "secret_key is required in bindings (config or secret)"); + return { accessKey, secretKey }; +}; + +// ---- TLS ---- + +const buildTlsOptions = (bindings = {}) => { + const skip = bindings.skipTlsVerify || bindings.tlsInsecureSkipVerify || bindings.insecureSkipVerify; + if (!skip) return {}; + return { skipTlsVerify: true, tlsInsecureSkipVerify: true, insecureSkipVerify: true }; +}; + +// ---- HTTP headers ---- + +const buildHeaders = (ctx, extra = {}) => { + const meta = ctx?.meta || {}; + return { + ...(ctx?.bindings?.headers || {}), + "x-engine-instance": meta.instance_id || meta.instanceId || "unknown", + "x-request-id": meta.request_id || meta.requestId || "unknown", + ...extra, + }; +}; + +// ---- URL helpers ---- + +const buildUrl = (baseUrl, path, queryParams = {}) => { + const base = String(baseUrl || "").replace(/\/+$/, ""); + const normalizedPath = String(path || "").replace(/^\/+/, ""); + const url = `${base}/${normalizedPath}`; + const parts = []; + for (const [key, value] of Object.entries(queryParams)) { + if (value === undefined || value === null || value === "") continue; + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + return parts.length ? `${url}?${parts.join("&")}` : url; +}; + +// ---- HTTP status → gRPC code ---- + +const mapHttpStatusToCode = (status) => { + if (status === 401 || status === 403) return "PERMISSION_DENIED"; + if (status >= 400 && status < 500) return "FAILED_PRECONDITION"; + return "UNAVAILABLE"; +}; + +const MAX_HTTP_BODY_CHARS = 200; +const truncateHttpBody = (value, limit = MAX_HTTP_BODY_CHARS) => String(value || "").slice(0, limit); + +// ---- Structured error helper ---- + +const throwStructuredError = (code, message, options = {}) => { + const payload = { + code, + message, + http_status: Number(options.httpStatus ?? 0), + http_body: truncateHttpBody(options.httpBody), + }; + if (options.reason) payload.reason = String(options.reason); + throw errorWithCode(code, JSON.stringify(payload)); +}; + +// ---- 日志 ---- + +const logFlow = (ctx, action, details) => { + const meta = ctx?.meta || {}; + const trace = []; + if (meta.instance_id || meta.instanceId) trace.push(`inst=${meta.instance_id || meta.instanceId}`); + if (meta.request_id || meta.requestId) trace.push(`req=${meta.request_id || meta.requestId}`); + const prefix = `[BaiduWAF_WAFWebTemplate][${action}]${trace.length ? `[${trace.join(" ")}]` : ""}`; + try { + console.log(prefix, JSON.stringify(details)); + } catch { + console.log(prefix, details); + } +}; + +// ---- JSON 解析 ---- + +const parseJsonBody = (text, action = "parse") => { + try { + return JSON.parse(text); + } catch { + throwStructuredError("UNKNOWN", `BaiduWAF_WAFWebTemplate ${action} response is not valid JSON`, { + httpStatus: 0, + httpBody: text, + reason: "response is not valid JSON", + }); + } +}; + +const fetchWithTimeout = async (url, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(new Error("request timed out")), timeoutMs); + try { + return await fetch(url, { + ...init, + signal: controller.signal, + }); + } catch (err) { + if (err?.name === "AbortError") { + throw new Error("request timed out"); + } + throw err; + } finally { + clearTimeout(timeoutId); + } +}; + +const requestTextWithNodeTransport = async (url, init = {}, options = {}) => new Promise((resolve, reject) => { + const target = new URL(url); + const transport = target.protocol === "https:" ? https : http; + const headers = { ...(init.headers || {}) }; + if (init.body !== undefined && init.body !== null && headers["Content-Length"] === undefined && headers["content-length"] === undefined) { + headers["Content-Length"] = Buffer.byteLength(String(init.body)); + } + const req = transport.request(target, { + method: init.method || "GET", + headers, + rejectUnauthorized: options.rejectUnauthorized !== false, + }, (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + res.on("end", () => { + resolve({ + status: res.statusCode ?? 0, + headers: res.headers, + text: Buffer.concat(chunks).toString("utf8"), + }); + }); + }); + + req.on("error", reject); + req.setTimeout(options.timeoutMs || DEFAULT_TIMEOUT_MS, () => { + req.destroy(new Error("request timed out")); + }); + + if (init.body !== undefined && init.body !== null) req.write(init.body); + req.end(); +}); + +// ---- 网络请求 ---- + +const fetchText = async (ctx, url, init = {}) => { + const callCtx = resolveCallContext(ctx); + const tlsOptions = buildTlsOptions(callCtx.bindings || {}); + const method = String(init.method || "GET").toUpperCase(); + const hasBody = init.body !== undefined && init.body !== null; + const useNodeTransport = tlsOptions.skipTlsVerify || (hasBody && (method === "GET" || method === "HEAD")); + let status; + let headers; + let text; + try { + if (useNodeTransport) { + const response = await requestTextWithNodeTransport(url, init, { + timeoutMs: resolveTimeoutMs(callCtx), + rejectUnauthorized: !tlsOptions.skipTlsVerify, + }); + status = response.status; + headers = response.headers; + text = response.text; + } else { + const res = await fetchWithTimeout(url, { + ...init, + }, resolveTimeoutMs(callCtx)); + status = res.status; + headers = res.headers; + try { + text = await res.text(); + } catch (err) { + throwStructuredError("UNKNOWN", "BaiduWAF_WAFWebTemplate upstream response body read failed", { + httpStatus: res.status, + httpBody: "", + reason: err?.message || "response body read failed", + }); + } + } + } catch (err) { + throwStructuredError("UNAVAILABLE", "BaiduWAF_WAFWebTemplate upstream request failed", { + httpStatus: 0, + httpBody: "", + reason: err?.cause?.message || err?.message || "fetch failed", + }); + } + + if (status < 200 || status >= 300) { + throwStructuredError(mapHttpStatusToCode(status), `upstream HTTP ${status}`, { + httpStatus: status, + httpBody: text, + reason: `upstream http ${status}`, + }); + } + + return { + http_status: status, + http_body: text, + headers, + }; +}; + +// ---- BCE 签名 ---- + +const formatBceDate = (date = new Date()) => date.toISOString().replace(/\.\d{3}Z$/, "Z"); + +const bceEncode = (value, safe = "-_.~") => { + const safeChars = new Set(String(safe).split("")); + let result = ""; + for (const ch of String(value)) { + if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9") || safeChars.has(ch)) { + result += ch; + continue; + } + for (const byte of Buffer.from(ch)) { + result += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`; + } + } + return result; +}; + +const buildCanonicalQueryString = (queryParams = {}) => Object.entries(queryParams) + .filter(([, value]) => value !== undefined && value !== null && value !== "") + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${bceEncode(key)}=${bceEncode(value)}`) + .join("&"); + +const buildCanonicalHeaders = (headersToSign = {}) => Object.entries(headersToSign) + .sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase())) + .map(([key, value]) => `${bceEncode(key.toLowerCase(), "-_.~")}:${bceEncode(value)}`) + .join("\n"); + +const buildCanonicalRequest = (method, uri, queryParams, headersToSign) => [ + String(method || "GET").toUpperCase(), + bceEncode(uri, "/-_.~"), + buildCanonicalQueryString(queryParams), + buildCanonicalHeaders(headersToSign), +].join("\n"); + +const buildBceAuthorization = ({ accessKey, secretKey, method, uri, queryParams = {}, host, contentType = BCE_CONTENT_TYPE, xBceDate = formatBceDate() }) => { + const headersToSign = { + Host: host, + "Content-Type": contentType, + "x-bce-date": xBceDate, + }; + const canonicalRequest = buildCanonicalRequest(method, uri, queryParams, headersToSign); + const authStringPrefix = `bce-auth-v1/${accessKey}/${xBceDate}/${BCE_EXPIRE_SECONDS}`; + const signingKey = createHmac("sha256", secretKey).update(authStringPrefix).digest(); + const signature = createHmac("sha256", signingKey).update(canonicalRequest).digest("hex"); + return `${authStringPrefix}/${BCE_SIGNED_HEADERS}/${signature}`; +}; + +const buildBceSignedHeaders = (ctx, { method, apiBase, path, queryParams = {} }) => { + const { accessKey, secretKey } = requireBceCredentials(ctx); + const baseUrl = new URL(resolveApiBase(mergedBindings({ bindings: { api_base: apiBase } })) || apiBase); + const xBceDate = formatBceDate(); + const authorization = buildBceAuthorization({ + accessKey, + secretKey, + method, + uri: path, + queryParams, + host: baseUrl.host, + contentType: BCE_CONTENT_TYPE, + xBceDate, + }); + return { + Host: baseUrl.host, + "Content-Type": BCE_CONTENT_TYPE, + "x-bce-date": xBceDate, + Authorization: authorization, + }; +}; + +// ---- resolve* 函数 ---- + +const resolveApiBase = (bindings = {}) => { + const base = normalizeBaseUrl(firstDefined( + bindings.api_base, + bindings.apiBase, + bindings.base_url, + )); + return base || DEFAULT_API_BASE; +}; + +// ---- 数值转换 ---- + +const toInteger = (value, fallback = 0) => { + const num = Number(value); + if (!Number.isFinite(num) || Number.isNaN(num)) return fallback; + return Math.trunc(num); +}; + +// ---- 校验函数 ---- + +const requireString = (value, field) => { + const text = unwrapString(value).trim(); + if (!text) throw errorWithCode("INVALID_ARGUMENT", `${field} is required`); + return text; +}; + +const requireIntegerField = (value, field) => { + if (value === undefined || value === null || unwrapString(value).trim() === "") { + throw errorWithCode("INVALID_ARGUMENT", `${field} is required`); + } + return toInteger(value, 0); +}; + +const requirePresent = (value, field) => { + if (value === undefined || value === null) { + throw errorWithCode("INVALID_ARGUMENT", `${field} is required`); + } + return value; +}; + +// ---- 映射层 ---- + +const mapWebTemplateDetailResult = (item) => { + if (!item) return undefined; + return { + switch: toInteger(item.switch, 0), + protectionDomains: pickStringArray(item, ["protectionDomains", "protection_domains"]), + templateType: unwrapString(firstDefined(item.templateType, item.template_type)), + templateKey: unwrapString(firstDefined(item.templateKey, item.template_key)), + action: unwrapString(item.action), + groupKey: unwrapString(firstDefined(item.groupKey, item.group_key)), + ruleName: unwrapString(firstDefined(item.ruleName, item.rule_name)), + ruleID: toInteger(firstDefined(item.ruleID, item.rule_id), 0), + groupName: unwrapString(firstDefined(item.groupName, item.group_name)), + }; +}; + +const mapWebTemplateSaveResult = (item) => { + if (!item) return undefined; + return { + templateKey: unwrapString(firstDefined(item.templateKey, item.template_key)), + }; +}; + +const mapWebTemplateListItem = (item) => { + if (!item) return undefined; + return { + ruleName: unwrapString(firstDefined(item.ruleName, item.rule_name)), + templateKey: unwrapString(firstDefined(item.templateKey, item.template_key)), + templateType: unwrapString(firstDefined(item.templateType, item.template_type)), + protectionDomains: pickStringArray(item, ["protectionDomains", "protection_domains"]), + action: unwrapString(item.action), + switch: toInteger(item.switch, 0), + updateTime: unwrapString(firstDefined(item.updateTime, item.update_time)), + ruleID: toInteger(firstDefined(item.ruleID, item.rule_id), 0), + }; +}; + +const mapWebTemplateListResult = (item) => { + if (!item) return undefined; + const rawItems = Array.isArray(item.result) ? item.result : []; + return { + result: rawItems.map(mapWebTemplateListItem).filter(Boolean), + totalCount: toInteger(firstDefined(item.totalCount, item.total_count), 0), + }; +}; + +const mapWhiteRulesdetailTarget = (item) => { + if (!item) return undefined; + return { + field: unwrapString(item.field), + key: unwrapString(item.key), + match: unwrapString(item.match), + value: Array.isArray(item.value) ? item.value.map(unwrapString).filter((v) => v) : [], + }; +}; + +const mapWhiteRulesdetailResult = (item) => { + if (!item) return undefined; + return { + ruleName: unwrapString(firstDefined(item.ruleName, item.rule_name)), + ruleID: toInteger(firstDefined(item.ruleID, item.rule_id), 0), + ruleType: unwrapString(firstDefined(item.ruleType, item.rule_type)), + protectionDomains: pickStringArray(item, ["protectionDomains", "protection_domains"]), + switch: toInteger(item.switch, 0), + updateTime: unwrapString(firstDefined(item.updateTime, item.update_time)), + targets: Array.isArray(item.targets) ? item.targets.map(mapWhiteRulesdetailTarget).filter(Boolean) : [], + }; +}; + +const mapWhiteRuleslistItem = (item) => { + if (!item) return undefined; + return { + ruleName: unwrapString(firstDefined(item.ruleName, item.rule_name)), + ruleType: unwrapString(firstDefined(item.ruleType, item.rule_type)), + protectionDomains: pickStringArray(item, ["protectionDomains", "protection_domains"]), + switch: toInteger(item.switch, 0), + updateTime: unwrapString(firstDefined(item.updateTime, item.update_time)), + ruleKey: unwrapString(firstDefined(item.ruleKey, item.rule_key)), + ruleID: toInteger(firstDefined(item.ruleID, item.rule_id), 0), + ignoreModules: pickStringArray(item, ["ignoreModules", "ignore_modules"]), + ignoreIds: pickStringArray(item, ["ignoreIds", "ignore_ids"]), + }; +}; + +const mapWhiteRuleslistResult = (item) => { + if (!item) return undefined; + const rawItems = Array.isArray(item.result) ? item.result : []; + return { + result: rawItems.map(mapWhiteRuleslistItem).filter(Boolean), + totalCount: toInteger(firstDefined(item.totalCount, item.total_count), 0), + }; +}; + +const mapRegionRuleslistValue = (item) => { + if (!item) return undefined; + return { + domestic: Array.isArray(item.domestic) ? item.domestic.map(unwrapString).filter((v) => v) : [], + overseas: Array.isArray(item.overseas) ? item.overseas.map(unwrapString).filter((v) => v) : [], + }; +}; + +const mapRegionRuleslistItem = (item) => { + if (!item) return undefined; + return { + ruleName: unwrapString(firstDefined(item.ruleName, item.rule_name)), + protectionDomains: pickStringArray(item, ["protectionDomains", "protection_domains"]), + switch: hasOwn(item, "switch") ? toInteger(item.switch, 0) : undefined, + updateTime: unwrapString(firstDefined(item.updateTime, item.update_time)), + ruleKey: unwrapString(firstDefined(item.ruleKey, item.rule_key)), + ruleID: toInteger(firstDefined(item.ruleID, item.rule_id), 0), + ruleType: unwrapString(firstDefined(item.ruleType, item.rule_type)), + action: unwrapString(item.action), + value: item.value ? mapRegionRuleslistValue(item.value) : undefined, + }; +}; + +const mapRegionRuleslistResult = (item) => { + if (!item) return undefined; + const rawItems = Array.isArray(item.result) ? item.result : []; + return { + result: rawItems.map(mapRegionRuleslistItem).filter(Boolean), + totalCount: hasOwn(item, "totalCount") || hasOwn(item, "total_count") + ? toInteger(firstDefined(item.totalCount, item.total_count), 0) + : undefined, + }; +}; + +const buildRulesListBody = (req, { includeAction = false } = {}) => { + const pageNo = requireIntegerField(firstDefined(req?.pageNo, req?.page_no), "pageNo"); + const pageSize = requireIntegerField(firstDefined(req?.pageSize, req?.page_size), "pageSize"); + const switchRaw = firstDefined(req?.switch); + const subdomainRaw = firstDefined(req?.subdomain, req?.subdomains); + const ruleIDRaw = firstDefined(req?.ruleID, req?.ruleId, req?.rule_id); + const ruleNameRaw = firstDefined(req?.ruleName, req?.rule_name); + const actionRaw = firstDefined(req?.action); + + const body = { + pageNo, + pageSize, + }; + + if (switchRaw !== undefined && switchRaw !== null && unwrapString(switchRaw).trim() !== "") { + body.switch = toInteger(switchRaw, 0); + } + + if (Array.isArray(subdomainRaw)) { + const subdomain = mapStringArray(subdomainRaw); + if (subdomain.length) body.subdomain = subdomain; + } + + if (ruleIDRaw !== undefined && ruleIDRaw !== null && unwrapString(ruleIDRaw).trim() !== "") { + body.ruleID = unwrapString(ruleIDRaw).trim(); + } + + if (ruleNameRaw !== undefined && ruleNameRaw !== null) { + body.ruleName = unwrapString(ruleNameRaw); + } + + if (includeAction && actionRaw !== undefined && actionRaw !== null) { + body.action = unwrapString(actionRaw); + } + + return body; +}; + +// ---- 请求层 ---- + +const runWebTemplateDetail = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const templateKey = requireString(firstDefined(req?.templateKey, req?.template_key), "templateKey"); + const queryParams = { templateKey }; + const url = buildUrl(apiBase, "/v1/waf/webTemplate/detail", queryParams); + + const response = await fetchText(callCtx, url, { + method: "GET", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "GET", + apiBase, + path: "/v1/waf/webTemplate/detail", + queryParams, + })), + }); + const json = parseJsonBody(response.http_body, "WebTemplateDetail"); + return { json, httpStatus: response.http_status }; +}; + +const runWebTemplateSave = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + + const name = requireString(firstDefined(req?.name), "name"); + const switchValue = requireIntegerField(firstDefined(req?.switch), "switch"); + const templateType = requireString(firstDefined(req?.templateType, req?.template_type), "templateType"); + const action = requireString(firstDefined(req?.action), "action"); + const rulesGroupID = requireString(firstDefined(req?.rulesGroupID, req?.rulesGroupId, req?.rules_group_id), "rulesGroupID"); + + const bindInfo = Array.isArray(firstDefined(req?.bindInfo, req?.bind_info)) + ? firstDefined(req?.bindInfo, req?.bind_info).map((item) => ({ + instanceID: unwrapString(firstDefined(item?.instanceID, item?.instanceId, item?.instance_id)), + subdomains: Array.isArray(item?.subdomains) ? item.subdomains.map(unwrapString).filter((v) => v) : [], + })) + : []; + + const body = { + name, + bindInfo, + switch: switchValue, + templateType, + action, + rulesGroupID, + }; + + const templateKey = unwrapString(firstDefined(req?.templateKey, req?.template_key)).trim(); + if (templateKey) body.templateKey = templateKey; + + const response = await fetchText(callCtx, `${apiBase}/v1/waf/webTemplate/save`, { + method: "POST", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "POST", + apiBase, + path: "/v1/waf/webTemplate/save", + queryParams: {}, + })), + body: JSON.stringify(body), + }); + const json = parseJsonBody(response.http_body, "WebTemplateSave"); + return { json, httpStatus: response.http_status }; +}; + +const runWebTemplateSwitch = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const templateKey = requireString(firstDefined(req?.templateKey, req?.template_key), "templateKey"); + const switchValue = requireIntegerField(firstDefined(req?.switch), "switch"); + + const response = await fetchText(callCtx, `${apiBase}/v1/waf/webTemplate/switch`, { + method: "POST", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "POST", + apiBase, + path: "/v1/waf/webTemplate/switch", + queryParams: {}, + })), + body: JSON.stringify({ + templateKey, + switch: switchValue, + }), + }); + const json = parseJsonBody(response.http_body, "WebTemplateSwitch"); + return { json, httpStatus: response.http_status }; +}; + +const runWebTemplateList = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const pageNo = requireIntegerField(firstDefined(req?.pageNo, req?.page_no), "pageNo"); + const pageSize = requireIntegerField(firstDefined(req?.pageSize, req?.page_size), "pageSize"); + const switchValue = requireIntegerField(firstDefined(req?.switch), "switch"); + const actionValue = requirePresent(firstDefined(req?.action), "action"); + const templateNameValue = requirePresent(firstDefined(req?.templateName, req?.template_name), "templateName"); + const subdomains = Array.isArray(firstDefined(req?.subdomains)) + ? firstDefined(req?.subdomains).map(unwrapString).filter((v) => v) + : []; + const ruleIDRaw = firstDefined(req?.ruleID, req?.ruleId, req?.rule_id); + + const body = { + pageNo, + pageSize, + switch: switchValue, + action: unwrapString(actionValue), + templateName: unwrapString(templateNameValue), + }; + + if (subdomains.length) body.subdomains = subdomains; + if (ruleIDRaw !== undefined && ruleIDRaw !== null && unwrapString(ruleIDRaw).trim() !== "") { + body.ruleID = toInteger(ruleIDRaw, 0); + } + + const response = await fetchText(callCtx, `${apiBase}/v1/waf/webTemplate/list`, { + method: "POST", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "POST", + apiBase, + path: "/v1/waf/webTemplate/list", + queryParams: {}, + })), + body: JSON.stringify(body), + }); + const json = parseJsonBody(response.http_body, "WebTemplateList"); + return { json, httpStatus: response.http_status }; +}; + +const runWebTemplateDelete = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const templateKey = requireString(firstDefined(req?.templateKey, req?.template_key), "templateKey"); + const queryParams = { templateKey }; + const url = buildUrl(apiBase, "/v1/waf/webTemplate/delete", queryParams); + const tlsOptions = buildTlsOptions(callCtx.bindings || {}); + + let httpStatus; + let text; + try { + if (tlsOptions.skipTlsVerify) { + const response = await requestTextWithNodeTransport(url, { + method: "DELETE", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "DELETE", + apiBase, + path: "/v1/waf/webTemplate/delete", + queryParams, + })), + }, { + timeoutMs: resolveTimeoutMs(callCtx), + rejectUnauthorized: false, + }); + httpStatus = response.status; + text = response.text; + } else { + const res = await fetchWithTimeout(url, { + method: "DELETE", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "DELETE", + apiBase, + path: "/v1/waf/webTemplate/delete", + queryParams, + })), + }, resolveTimeoutMs(callCtx)); + httpStatus = res.status; + text = await res.text(); + } + } catch (err) { + throwStructuredError("UNAVAILABLE", "BaiduWAF_WAFWebTemplate upstream request failed", { + httpStatus: 0, + httpBody: "", + reason: err?.cause?.message || err?.message || "fetch failed", + }); + } + + if (httpStatus === 404) { + logFlow(callCtx, "WebTemplateDelete:already-gone", { templateKey }); + return { alreadyGone: true, json: { success: true, result: {} }, httpStatus }; + } + + if (httpStatus < 200 || httpStatus >= 300) { + throwStructuredError(mapHttpStatusToCode(httpStatus), `upstream HTTP ${httpStatus}`, { + httpStatus, + httpBody: text, + reason: `upstream http ${httpStatus}`, + }); + } + + const json = parseJsonBody(text, "WebTemplateDelete"); + return { alreadyGone: false, json, httpStatus }; +}; + +const runWhiteRulesdetail = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const ruleKey = requireString(firstDefined(req?.ruleKey, req?.rule_key), "ruleKey"); + const queryParams = { ruleKey }; + const url = buildUrl(apiBase, "/v1/waf/whiteRules/detail", queryParams); + + const response = await fetchText(callCtx, url, { + method: "GET", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "GET", + apiBase, + path: "/v1/waf/whiteRules/detail", + queryParams, + })), + }); + const json = parseJsonBody(response.http_body, "WhiteRulesdetail"); + return { json, httpStatus: response.http_status }; +}; + +const runWhiteRulesdelete = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const ruleKey = requireString(firstDefined(req?.ruleKey, req?.rule_key), "ruleKey"); + const queryParams = { ruleKey }; + const url = buildUrl(apiBase, "/v1/waf/whiteRules/delete", queryParams); + const tlsOptions = buildTlsOptions(callCtx.bindings || {}); + + let httpStatus; + let text; + try { + if (tlsOptions.skipTlsVerify) { + const response = await requestTextWithNodeTransport(url, { + method: "DELETE", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "DELETE", + apiBase, + path: "/v1/waf/whiteRules/delete", + queryParams, + })), + }, { + timeoutMs: resolveTimeoutMs(callCtx), + rejectUnauthorized: false, + }); + httpStatus = response.status; + text = response.text; + } else { + const res = await fetchWithTimeout(url, { + method: "DELETE", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "DELETE", + apiBase, + path: "/v1/waf/whiteRules/delete", + queryParams, + })), + }, resolveTimeoutMs(callCtx)); + httpStatus = res.status; + text = await res.text(); + } + } catch (err) { + throwStructuredError("UNAVAILABLE", "BaiduWAF_WAFWebTemplate upstream request failed", { + httpStatus: 0, + httpBody: "", + reason: err?.cause?.message || err?.message || "fetch failed", + }); + } + + if (httpStatus === 404) { + logFlow(callCtx, "WhiteRulesdelete:already-gone", { ruleKey }); + return { alreadyGone: true, json: { success: true, result: [] }, httpStatus }; + } + + if (httpStatus < 200 || httpStatus >= 300) { + throwStructuredError(mapHttpStatusToCode(httpStatus), `upstream HTTP ${httpStatus}`, { + httpStatus, + httpBody: text, + reason: `upstream http ${httpStatus}`, + }); + } + + const json = parseJsonBody(text, "WhiteRulesdelete"); + return { alreadyGone: false, json, httpStatus }; +}; + +const runWhiteRulesswitch = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const ruleKey = requireString(firstDefined(req?.ruleKey, req?.rule_key), "ruleKey"); + const switchValue = requireIntegerField(firstDefined(req?.switch), "switch"); + + const response = await fetchText(callCtx, `${apiBase}/v1/waf/whiteRules/switch`, { + method: "POST", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "POST", + apiBase, + path: "/v1/waf/whiteRules/switch", + queryParams: {}, + })), + body: JSON.stringify({ + ruleKey, + switch: switchValue, + }), + }); + const json = parseJsonBody(response.http_body, "WhiteRulesswitch"); + return { json, httpStatus: response.http_status }; +}; + +const runWhiteRuleslist = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const body = buildRulesListBody(req); + + const response = await fetchText(callCtx, `${apiBase}/v1/waf/whiteRules/list`, { + method: "POST", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "POST", + apiBase, + path: "/v1/waf/whiteRules/list", + queryParams: {}, + })), + body: JSON.stringify(body), + }); + const json = parseJsonBody(response.http_body, "WhiteRuleslist"); + return { json, httpStatus: response.http_status }; +}; + +const runRegionRuleslist = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const apiBase = resolveApiBase(callCtx.bindings || {}); + const body = buildRulesListBody(req, { includeAction: true }); + + const response = await fetchText(callCtx, `${apiBase}/v1/waf/regionRules/list`, { + method: "POST", + headers: buildHeaders(callCtx, buildBceSignedHeaders(callCtx, { + method: "POST", + apiBase, + path: "/v1/waf/regionRules/list", + queryParams: {}, + })), + body: JSON.stringify(body), + }); + const json = parseJsonBody(response.http_body, "RegionRuleslist"); + return { json, httpStatus: response.http_status }; +}; +// ---- 编排层 ---- + +const handleWebTemplateDetail = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const templateKey = firstDefined(req?.templateKey, req?.template_key); + logFlow(callCtx, "WebTemplateDetail:start", { templateKey }); + const { json } = await runWebTemplateDetail(req, callCtx); + logFlow(callCtx, "WebTemplateDetail:success", {}); + + return { + status: toInteger(json.status, 0), + result: json.result ? mapWebTemplateDetailResult(json.result) : undefined, + success: Boolean(json.success), + }; +}; + +const handleWebTemplateSave = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + logFlow(callCtx, "WebTemplateSave:start", {}); + const { json } = await runWebTemplateSave(req, callCtx); + logFlow(callCtx, "WebTemplateSave:success", {}); + + return { + status: toInteger(json.status, 0), + result: json.result ? mapWebTemplateSaveResult(json.result) : undefined, + }; +}; + +const handleWebTemplateSwitch = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const templateKey = firstDefined(req?.templateKey, req?.template_key); + logFlow(callCtx, "WebTemplateSwitch:start", { templateKey }); + const { json } = await runWebTemplateSwitch(req, callCtx); + logFlow(callCtx, "WebTemplateSwitch:success", {}); + + return { + success: Boolean(json.success), + status: toInteger(json.status, 0), + }; +}; + +const handleWebTemplateList = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + logFlow(callCtx, "WebTemplateList:start", { + pageNo: firstDefined(req?.pageNo, req?.page_no), + pageSize: firstDefined(req?.pageSize, req?.page_size), + }); + const { json } = await runWebTemplateList(req, callCtx); + logFlow(callCtx, "WebTemplateList:success", {}); + + return { + status: toInteger(json.status, 0), + success: Boolean(json.success), + result: json.result ? mapWebTemplateListResult(json.result) : undefined, + }; +}; + +const handleWebTemplateDelete = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const templateKey = firstDefined(req?.templateKey, req?.template_key); + logFlow(callCtx, "WebTemplateDelete:start", { templateKey }); + const { alreadyGone, json } = await runWebTemplateDelete(req, callCtx); + + if (alreadyGone) { + return { success: true, result: {}, alreadyGone: true }; + } + + logFlow(callCtx, "WebTemplateDelete:success", {}); + return { + success: Boolean(json.success), + result: json.result && typeof json.result === "object" ? {} : undefined, + alreadyGone: false, + }; +}; + +const handleWhiteRulesdetail = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const ruleKey = firstDefined(req?.ruleKey, req?.rule_key); + logFlow(callCtx, "WhiteRulesdetail:start", { ruleKey }); + const { json } = await runWhiteRulesdetail(req, callCtx); + logFlow(callCtx, "WhiteRulesdetail:success", {}); + + return { + success: Boolean(json.success), + status: toInteger(json.status, 0), + result: json.result ? mapWhiteRulesdetailResult(json.result) : undefined, + }; +}; + +const handleWhiteRulesdelete = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const ruleKey = firstDefined(req?.ruleKey, req?.rule_key); + logFlow(callCtx, "WhiteRulesdelete:start", { ruleKey }); + const { alreadyGone, json } = await runWhiteRulesdelete(req, callCtx); + + if (alreadyGone) { + return { success: true, result: [], alreadyGone: true }; + } + + logFlow(callCtx, "WhiteRulesdelete:success", {}); + return { + success: Boolean(json.success), + result: Array.isArray(json.result) ? json.result.map(unwrapString) : [], + alreadyGone: false, + }; +}; + +const handleWhiteRulesswitch = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const ruleKey = firstDefined(req?.ruleKey, req?.rule_key); + logFlow(callCtx, "WhiteRulesswitch:start", { ruleKey }); + const { json } = await runWhiteRulesswitch(req, callCtx); + logFlow(callCtx, "WhiteRulesswitch:success", {}); + + return { + success: Boolean(json.success), + result: Array.isArray(json.result) ? json.result.map(unwrapString) : [], + }; +}; + +const handleWhiteRuleslist = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + logFlow(callCtx, "WhiteRuleslist:start", { + pageNo: firstDefined(req?.pageNo, req?.page_no), + pageSize: firstDefined(req?.pageSize, req?.page_size), + }); + const { json } = await runWhiteRuleslist(req, callCtx); + logFlow(callCtx, "WhiteRuleslist:success", {}); + + return { + success: Boolean(json.success), + status: toInteger(json.status, 0), + result: json.result ? mapWhiteRuleslistResult(json.result) : undefined, + }; +}; + +const handleRegionRuleslist = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + logFlow(callCtx, "RegionRuleslist:start", { + pageNo: firstDefined(req?.pageNo, req?.page_no), + pageSize: firstDefined(req?.pageSize, req?.page_size), + }); + const { json } = await runRegionRuleslist(req, callCtx); + logFlow(callCtx, "RegionRuleslist:success", {}); + + return { + success: Boolean(json.success), + status: toInteger(json.status, 0), + result: json.result ? mapRegionRuleslistResult(json.result) : undefined, + }; +}; + +const registerHandlers = (ctx = {}) => { + const callCtx = resolveCallContext(ctx); + return { + [WEBTEMPLATEDETAIL_PATH]: (req = callCtx.req) => handleWebTemplateDetail(req ?? {}, callCtx), + [WEBTEMPLATESAVE_PATH]: (req = callCtx.req) => handleWebTemplateSave(req ?? {}, callCtx), + [WEBTEMPLATESWITCH_PATH]: (req = callCtx.req) => handleWebTemplateSwitch(req ?? {}, callCtx), + [WEBTEMPLATELIST_PATH]: (req = callCtx.req) => handleWebTemplateList(req ?? {}, callCtx), + [WEBTEMPLATEDELETE_PATH]: (req = callCtx.req) => handleWebTemplateDelete(req ?? {}, callCtx), + [WHITERULESDETAIL_PATH]: (req = callCtx.req) => handleWhiteRulesdetail(req ?? {}, callCtx), + [WHITERULESDELETE_PATH]: (req = callCtx.req) => handleWhiteRulesdelete(req ?? {}, callCtx), + [WHITERULESSWITCH_PATH]: (req = callCtx.req) => handleWhiteRulesswitch(req ?? {}, callCtx), + [WHITERULESLIST_PATH]: (req = callCtx.req) => handleWhiteRuleslist(req ?? {}, callCtx), + [REGIONRULESLIST_PATH]: (req = callCtx.req) => handleRegionRuleslist(req ?? {}, callCtx), + }; +}; + +export function rpcdef(ctx = {}) { + return registerHandlers(ctx); +} + +const callSdkHandler = (ctx, path) => registerHandlers(ctx)[path](resolveSdkReq(ctx)); + +export const handlers = { + [METHOD_WEBTEMPLATEDETAIL_FULL]: (ctx) => callSdkHandler(ctx, WEBTEMPLATEDETAIL_PATH), + [METHOD_WEBTEMPLATESAVE_FULL]: (ctx) => callSdkHandler(ctx, WEBTEMPLATESAVE_PATH), + [METHOD_WEBTEMPLATESWITCH_FULL]: (ctx) => callSdkHandler(ctx, WEBTEMPLATESWITCH_PATH), + [METHOD_WEBTEMPLATELIST_FULL]: (ctx) => callSdkHandler(ctx, WEBTEMPLATELIST_PATH), + [METHOD_WEBTEMPLATEDELETE_FULL]: (ctx) => callSdkHandler(ctx, WEBTEMPLATEDELETE_PATH), + [METHOD_WHITERULESDETAIL_FULL]: (ctx) => callSdkHandler(ctx, WHITERULESDETAIL_PATH), + [METHOD_WHITERULESDELETE_FULL]: (ctx) => callSdkHandler(ctx, WHITERULESDELETE_PATH), + [METHOD_WHITERULESSWITCH_FULL]: (ctx) => callSdkHandler(ctx, WHITERULESSWITCH_PATH), + [METHOD_WHITERULESLIST_FULL]: (ctx) => callSdkHandler(ctx, WHITERULESLIST_PATH), + [METHOD_REGIONRULESLIST_FULL]: (ctx) => callSdkHandler(ctx, REGIONRULESLIST_PATH), +}; + +rpcdef.__test__ = { + BCE_CONTENT_TYPE, + BCE_EXPIRE_SECONDS, + BCE_SIGNED_HEADERS, + bceEncode, + buildBceAuthorization, + buildBceSignedHeaders, + buildCanonicalHeaders, + buildCanonicalQueryString, + buildCanonicalRequest, + buildHeaders, + buildTlsOptions, + buildUrl, + errorWithCode, + fetchText, + fetchWithTimeout, + firstDefined, + formatBceDate, + grpcCodeFor, + handleWebTemplateDelete, + handleWebTemplateDetail, + handleWebTemplateList, + handleWebTemplateSave, + handleWebTemplateSwitch, + handleWhiteRulesdelete, + handleWhiteRulesdetail, + handleWhiteRuleslist, + handleWhiteRulesswitch, + handleRegionRuleslist, + hasOwn, + logFlow, + mapHttpStatusToCode, + MAX_HTTP_BODY_CHARS, + mapWebTemplateDetailResult, + mapWebTemplateListItem, + mapWebTemplateListResult, + mapWebTemplateSaveResult, + mapWhiteRulesdetailResult, + mapWhiteRulesdetailTarget, + mapWhiteRuleslistItem, + mapWhiteRuleslistResult, + mapRegionRuleslistValue, + mapRegionRuleslistItem, + mapRegionRuleslistResult, + mergedBindings, + normalizeBaseUrl, + parseJsonBody, + pickString, + registerHandlers, + requestTextWithNodeTransport, + requireBceCredentials, + requireIntegerField, + requirePresent, + requireString, + resolveAccessKey, + resolveApiBase, + resolveCallContext, + resolveSecretKey, + resolveTimeoutMs, + runWebTemplateDelete, + runWebTemplateDetail, + runWebTemplateList, + runWebTemplateSave, + runWebTemplateSwitch, + runWhiteRulesdelete, + runWhiteRulesdetail, + runWhiteRuleslist, + runWhiteRulesswitch, + runRegionRuleslist, + throwStructuredError, + toInteger, + truncateHttpBody, + unwrapString, +}; + +export const _test = rpcdef.__test__; diff --git a/services/baiduwaf__waf-web-template/src/service.js b/services/baiduwaf__waf-web-template/src/service.js new file mode 100644 index 00000000..9a814eca --- /dev/null +++ b/services/baiduwaf__waf-web-template/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from "@chaitin-ai/octobus-sdk"; + +import { handlers } from "./baiduwaf-waf-web-template.js"; + +export { handlers } from "./baiduwaf-waf-web-template.js"; + +export const service = defineService({ handlers }); diff --git a/services/baiduwaf__waf-web-template/test/baiduwaf-waf-web-template.test.js b/services/baiduwaf__waf-web-template/test/baiduwaf-waf-web-template.test.js new file mode 100644 index 00000000..80c8387d --- /dev/null +++ b/services/baiduwaf__waf-web-template/test/baiduwaf-waf-web-template.test.js @@ -0,0 +1,1048 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createHmac } from "node:crypto"; +import { readFile } from "node:fs/promises"; + +import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; + +import { + METHOD_WEBTEMPLATEDELETE_FULL, + METHOD_WEBTEMPLATEDETAIL_FULL, + METHOD_WEBTEMPLATELIST_FULL, + METHOD_WEBTEMPLATESAVE_FULL, + METHOD_WEBTEMPLATESWITCH_FULL, + METHOD_WHITERULESDELETE_FULL, + METHOD_WHITERULESDETAIL_FULL, + METHOD_WHITERULESLIST_FULL, + METHOD_WHITERULESSWITCH_FULL, + METHOD_REGIONRULESLIST_FULL, + WEBTEMPLATEDELETE_PATH, + WEBTEMPLATEDETAIL_PATH, + WEBTEMPLATELIST_PATH, + WEBTEMPLATESAVE_PATH, + WEBTEMPLATESWITCH_PATH, + WHITERULESDELETE_PATH, + WHITERULESDETAIL_PATH, + WHITERULESLIST_PATH, + WHITERULESSWITCH_PATH, + REGIONRULESLIST_PATH, + _test, + handlers, + rpcdef, +} from "../src/baiduwaf-waf-web-template.js"; +import { service } from "../src/service.js"; +import { createMockServer } from "./mock_upstream.js"; + +const originalFetch = globalThis.fetch; +const originalConsoleLog = console.log; +const protoPath = new URL("../proto/waf_web_template.proto", import.meta.url); + +const textResponse = (status, body) => ({ + status, + ok: status >= 200 && status < 300, + headers: { get: () => "application/json" }, + text: async () => body, +}); + +const jsonResponse = (status, body) => textResponse(status, JSON.stringify(body)); + +const setFetch = (impl) => { + globalThis.fetch = impl; +}; + +const expectGrpcError = async (fn, legacyCode, checker = () => {}) => { + let caught; + try { + await fn(); + } catch (err) { + caught = err; + } + assert.ok(caught, "expected function to reject"); + assert.ok(caught instanceof GrpcError); + assert.equal(caught.legacyCode, legacyCode); + assert.equal(caught.code, ({ + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + UNKNOWN: grpcStatus.UNKNOWN, + })[legacyCode]); + checker(caught); +}; + +const expectRejectPayload = async (fn, code, httpStatus, reasonPattern, options = {}) => { + await expectGrpcError(fn, code, (err) => { + const payload = JSON.parse(err.message); + assert.equal(payload.code, code); + assert.equal(payload.http_status, httpStatus); + assert.equal(typeof payload.http_body, "string"); + if (reasonPattern) assert.match(payload.reason, reasonPattern); + if (options.httpBodyLength !== undefined) assert.equal(payload.http_body.length, options.httpBodyLength); + if (options.httpBody !== undefined) assert.equal(payload.http_body, options.httpBody); + }); +}; + +const buildCtx = (overrides = {}) => ({ + config: { + api_base: "http://api.example.com", + ...(overrides.config || {}), + }, + secret: { + access_key: "test-ak", + secret_key: "test-sk", + ...(overrides.secret || {}), + }, + bindings: { ...(overrides.bindings || {}) }, + limits: { timeoutMs: 5000, ...(overrides.limits || {}) }, + meta: { instance_id: "inst", request_id: "req", ...(overrides.meta || {}) }, + req: overrides.req || {}, + request: overrides.request, +}); + +test.beforeEach(() => { + console.log = () => {}; +}); + +test.afterEach(() => { + globalThis.fetch = originalFetch; + console.log = originalConsoleLog; +}); + +test("service exports handlers and rpcdef paths", () => { + assert.equal(typeof service, "object"); + assert.equal(typeof handlers[METHOD_WEBTEMPLATEDETAIL_FULL], "function"); + assert.equal(typeof handlers[METHOD_WEBTEMPLATESAVE_FULL], "function"); + assert.equal(typeof handlers[METHOD_WEBTEMPLATESWITCH_FULL], "function"); + assert.equal(typeof handlers[METHOD_WEBTEMPLATELIST_FULL], "function"); + assert.equal(typeof handlers[METHOD_WEBTEMPLATEDELETE_FULL], "function"); + assert.equal(typeof handlers[METHOD_WHITERULESDETAIL_FULL], "function"); + assert.equal(typeof handlers[METHOD_WHITERULESDELETE_FULL], "function"); + assert.equal(typeof handlers[METHOD_WHITERULESSWITCH_FULL], "function"); + assert.equal(typeof handlers[METHOD_WHITERULESLIST_FULL], "function"); + assert.equal(typeof handlers[METHOD_REGIONRULESLIST_FULL], "function"); + const routes = rpcdef(buildCtx()); + assert.equal(typeof routes[WEBTEMPLATEDETAIL_PATH], "function"); + assert.equal(typeof routes[WEBTEMPLATESAVE_PATH], "function"); + assert.equal(typeof routes[WEBTEMPLATESWITCH_PATH], "function"); + assert.equal(typeof routes[WEBTEMPLATELIST_PATH], "function"); + assert.equal(typeof routes[WEBTEMPLATEDELETE_PATH], "function"); + assert.equal(typeof routes[WHITERULESDETAIL_PATH], "function"); + assert.equal(typeof routes[WHITERULESDELETE_PATH], "function"); + assert.equal(typeof routes[WHITERULESSWITCH_PATH], "function"); + assert.equal(typeof routes[WHITERULESLIST_PATH], "function"); + assert.equal(typeof routes[REGIONRULESLIST_PATH], "function"); +}); + +test("WebTemplateDetail rejects missing credentials", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { templateKey: "tpl-1" }, secret: { access_key: "", secret_key: "test-sk" } }))[WEBTEMPLATEDETAIL_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /access_key is required/), + ); +}); + +test("WebTemplateDetail rejects missing templateKey", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: {} }))[WEBTEMPLATEDETAIL_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /templateKey is required/), + ); +}); + +test("WebTemplateSave rejects missing required fields", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { name: "demo" } }))[WEBTEMPLATESAVE_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /switch is required/), + ); +}); + +test("WebTemplateSwitch rejects missing templateKey", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { switch: 1 } }))[WEBTEMPLATESWITCH_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /templateKey is required/), + ); +}); + +test("WebTemplateList rejects missing required fields", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { pageNo: 1, pageSize: 10, switch: -1, action: "" } }))[WEBTEMPLATELIST_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /templateName is required/), + ); +}); + +test("WebTemplateDelete rejects missing templateKey", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: {} }))[WEBTEMPLATEDELETE_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /templateKey is required/), + ); +}); + +test("WhiteRulesdetail rejects missing ruleKey", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: {} }))[WHITERULESDETAIL_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /ruleKey is required/), + ); +}); + +test("WhiteRulesdelete rejects missing ruleKey", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: {} }))[WHITERULESDELETE_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /ruleKey is required/), + ); +}); + +test("WhiteRulesswitch rejects missing switch", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { ruleKey: "rule-1" } }))[WHITERULESSWITCH_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /switch is required/), + ); +}); + +test("WhiteRuleslist rejects missing pageSize", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { pageNo: 1 } }))[WHITERULESLIST_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /pageSize is required/), + ); +}); + +test("RegionRuleslist rejects missing pageSize", async () => { + await expectGrpcError( + () => rpcdef(buildCtx({ req: { pageNo: 1 } }))[REGIONRULESLIST_PATH](), + "INVALID_ARGUMENT", + (err) => assert.match(err.message, /pageSize is required/), + ); +}); + +test("WebTemplateDetail returns success on HTTP 200", async () => { + let capturedUrl = ""; + let capturedInit; + + setFetch(async (url, init) => { + capturedUrl = String(url); + capturedInit = init; + return jsonResponse(200, { + status: 0, + success: true, + result: { + switch: 1, + protectionDomains: ["example.com"], + templateType: "high", + templateKey: "tpl-1", + action: "block", + groupKey: "high", + ruleName: "SQL注入防护", + ruleID: 10001, + groupName: "高", + }, + }); + }); + + const res = await rpcdef(buildCtx({ req: { templateKey: "tpl-1" } }))[WEBTEMPLATEDETAIL_PATH](); + + assert.equal(res.success, true); + assert.equal(res.result.templateKey, "tpl-1"); + assert.match(capturedUrl, /\/v1\/waf\/webTemplate\/detail\?templateKey=tpl-1/); + assert.equal(capturedInit.method, "GET"); + assert.match(capturedInit.headers.Authorization, /^bce-auth-v1\/test-ak\//); + assert.equal(capturedInit.headers["x-engine-instance"], "inst"); + assert.equal(capturedInit.headers["x-request-id"], "req"); +}); + +test("WebTemplateSave sends JSON body and returns templateKey", async () => { + let capturedInit; + + setFetch(async (_url, init) => { + capturedInit = init; + return jsonResponse(200, { + status: 200, + result: { templateKey: "template-abc123" }, + }); + }); + + const res = await rpcdef(buildCtx({ + req: { + name: "我的防护模板", + bindInfo: [{ instanceID: "instance-001", subdomains: ["www.example.com"] }], + switch: 1, + templateType: "saas", + action: "log", + rulesGroupID: "middle", + }, + }))[WEBTEMPLATESAVE_PATH](); + + const body = JSON.parse(capturedInit.body); + assert.equal(capturedInit.method, "POST"); + assert.equal(body.name, "我的防护模板"); + assert.equal(body.rulesGroupID, "middle"); + assert.deepEqual(body.bindInfo, [{ instanceID: "instance-001", subdomains: ["www.example.com"] }]); + assert.equal(res.result.templateKey, "template-abc123"); +}); + +test("WebTemplateSave accepts runtime-decoded rulesGroupId alias", async () => { + let capturedInit; + + setFetch(async (_url, init) => { + capturedInit = init; + return jsonResponse(200, { + status: 200, + result: { templateKey: "template-xyz" }, + }); + }); + + const res = await rpcdef(buildCtx({ + req: { + name: "我的防护模板", + bindInfo: [], + switch: 1, + templateType: "saas", + action: "log", + rulesGroupId: "middle", + }, + }))[WEBTEMPLATESAVE_PATH](); + + const body = JSON.parse(capturedInit.body); + assert.equal(body.rulesGroupID, "middle"); + assert.equal(res.result.templateKey, "template-xyz"); +}); + +test("WebTemplateSwitch sends JSON body and returns success", async () => { + let capturedInit; + + setFetch(async (_url, init) => { + capturedInit = init; + return jsonResponse(200, { + success: true, + status: 200, + }); + }); + + const res = await rpcdef(buildCtx({ req: { templateKey: "template-001", switch: 1 } }))[WEBTEMPLATESWITCH_PATH](); + + const body = JSON.parse(capturedInit.body); + assert.equal(capturedInit.method, "POST"); + assert.equal(body.templateKey, "template-001"); + assert.equal(body.switch, 1); + assert.equal(res.success, true); + assert.equal(res.status, 200); +}); + +test("WebTemplateList sends JSON body and maps nested result list", async () => { + let capturedInit; + + setFetch(async (_url, init) => { + capturedInit = init; + return jsonResponse(200, { + status: 200, + success: true, + result: { + result: [ + { + ruleName: "规则名称", + templateKey: "web-list-001", + templateType: "saas", + protectionDomains: ["example.com"], + action: "deny", + switch: 1, + updateTime: "2023-01-01 00:00:00", + ruleID: 123, + } + ], + totalCount: 1, + } + }); + }); + + const res = await rpcdef(buildCtx({ + req: { + pageNo: 1, + pageSize: 10, + switch: -1, + action: "", + templateName: "", + subdomains: ["example.com"], + ruleID: 123, + }, + }))[WEBTEMPLATELIST_PATH](); + + const body = JSON.parse(capturedInit.body); + assert.equal(body.pageNo, 1); + assert.equal(body.pageSize, 10); + assert.equal(body.switch, -1); + assert.equal(body.action, ""); + assert.equal(body.templateName, ""); + assert.deepEqual(body.subdomains, ["example.com"]); + assert.equal(body.ruleID, 123); + assert.equal(res.success, true); + assert.equal(res.result.totalCount, 1); + assert.equal(res.result.result[0].templateKey, "web-list-001"); +}); + +test("WebTemplateList accepts ruleId alias", async () => { + let capturedInit; + + setFetch(async (_url, init) => { + capturedInit = init; + return jsonResponse(200, { + status: 200, + success: true, + result: { + result: [], + totalCount: 0, + } + }); + }); + + await rpcdef(buildCtx({ + req: { + pageNo: 1, + pageSize: 10, + switch: -1, + action: "", + templateName: "", + ruleId: 321, + }, + }))[WEBTEMPLATELIST_PATH](); + + const body = JSON.parse(capturedInit.body); + assert.equal(body.ruleID, 321); +}); + +test("WebTemplateDelete returns idempotent success on HTTP 404", async () => { + setFetch(async () => jsonResponse(404, { success: false, message: "resource not found" })); + + const res = await rpcdef(buildCtx({ req: { templateKey: "gone-key" } }))[WEBTEMPLATEDELETE_PATH](); + + assert.equal(res.success, true); + assert.equal(res.alreadyGone, true); + assert.deepEqual(res.result, {}); +}); + +test("WhiteRulesdetail uses GET query and maps targets", async () => { + let capturedUrl = ""; + let capturedInit; + + setFetch(async (url, init) => { + capturedUrl = String(url); + capturedInit = init; + return jsonResponse(200, { + success: true, + status: 200, + result: { + ruleName: "白名单规则", + ruleID: 1001, + ruleType: "saas", + protectionDomains: ["example.com"], + switch: 1, + updateTime: "2026-03-10T00:00:00Z", + targets: [{ field: "header", key: "User-Agent", match: "contains", value: ["curl"] }], + }, + }); + }); + + const res = await rpcdef(buildCtx({ req: { ruleKey: "rule-1" } }))[WHITERULESDETAIL_PATH](); + + assert.match(capturedUrl, /\/v1\/waf\/whiteRules\/detail\?ruleKey=rule-1/); + assert.equal(capturedInit.method, "GET"); + assert.equal(res.success, true); + assert.equal(res.result.ruleName, "白名单规则"); + assert.equal(res.result.targets[0].key, "User-Agent"); + assert.deepEqual(res.result.targets[0].value, ["curl"]); +}); + +test("WhiteRulesdelete uses DELETE query and returns result array", async () => { + let capturedUrl = ""; + let capturedInit; + + setFetch(async (url, init) => { + capturedUrl = String(url); + capturedInit = init; + return jsonResponse(200, { + success: true, + result: [], + }); + }); + + const res = await rpcdef(buildCtx({ req: { ruleKey: "rule-1" } }))[WHITERULESDELETE_PATH](); + + assert.match(capturedUrl, /\/v1\/waf\/whiteRules\/delete\?ruleKey=rule-1/); + assert.equal(capturedInit.method, "DELETE"); + assert.equal(res.success, true); + assert.equal(res.alreadyGone, false); + assert.deepEqual(res.result, []); +}); + +test("WhiteRulesdelete returns idempotent success on HTTP 404", async () => { + setFetch(async () => jsonResponse(404, { success: false, message: "resource not found" })); + + const res = await rpcdef(buildCtx({ req: { ruleKey: "gone-rule" } }))[WHITERULESDELETE_PATH](); + + assert.equal(res.success, true); + assert.equal(res.alreadyGone, true); + assert.deepEqual(res.result, []); +}); + +test("WhiteRulesdelete proto declares alreadyGone field", async () => { + const proto = await readFile(protoPath, "utf8"); + assert.match(proto, /message WhiteRulesdeleteResponse \{[\s\S]*bool already_gone = 3 \[json_name = "alreadyGone"\];[\s\S]*\}/); +}); + +test("WhiteRulesdelete returns alreadyGone false on HTTP 200", async () => { + setFetch(async () => jsonResponse(200, { success: true, result: [] })); + + const res = await rpcdef(buildCtx({ req: { ruleKey: "rule-1" } }))[WHITERULESDELETE_PATH](); + + assert.equal(res.alreadyGone, false); +}); + +test("WhiteRulesswitch sends JSON body and returns array", async () => { + let capturedInit; + + setFetch(async (_url, init) => { + capturedInit = init; + return jsonResponse(200, { + success: true, + result: ["example"], + }); + }); + + const res = await rpcdef(buildCtx({ req: { ruleKey: "rule-1", switch: 1 } }))[WHITERULESSWITCH_PATH](); + + const body = JSON.parse(capturedInit.body); + assert.equal(capturedInit.method, "POST"); + assert.equal(body.ruleKey, "rule-1"); + assert.equal(body.switch, 1); + assert.equal(res.success, true); + assert.deepEqual(res.result, ["example"]); +}); + +test("WhiteRuleslist sends optional filters only when present", async () => { + const capturedBodies = []; + + setFetch(async (_url, init) => { + capturedBodies.push(JSON.parse(init.body)); + return jsonResponse(200, { + success: true, + status: 200, + result: { + result: [ + { + ruleName: "示例规则", + ruleType: "saas", + protectionDomains: ["example.com"], + switch: 1, + updateTime: "2023-10-27T10:00:00Z", + ruleKey: "rule-key-123", + ruleID: 1001, + ignoreModules: ["base"], + ignoreIds: [], + } + ], + totalCount: 1, + }, + }); + }); + + const res1 = await rpcdef(buildCtx({ + req: { + pageNo: 1, + pageSize: 10, + }, + }))[WHITERULESLIST_PATH](); + + const res2 = await rpcdef(buildCtx({ + req: { + pageNo: 2, + pageSize: 20, + switch: 0, + subdomain: ["www.example.com"], + ruleId: "rule-id-2", + ruleName: "白名单", + }, + }))[WHITERULESLIST_PATH](); + + assert.deepEqual(capturedBodies[0], { pageNo: 1, pageSize: 10 }); + assert.deepEqual(capturedBodies[1], { + pageNo: 2, + pageSize: 20, + switch: 0, + subdomain: ["www.example.com"], + ruleID: "rule-id-2", + ruleName: "白名单", + }); + assert.equal(res1.result.totalCount, 1); + assert.equal(res2.result.result[0].ruleKey, "rule-key-123"); + assert.deepEqual(res2.result.result[0].ignoreModules, ["base"]); +}); + +test("RegionRuleslist sends optional filters only when present", async () => { + const capturedBodies = []; + + setFetch(async (_url, init) => { + capturedBodies.push(JSON.parse(init.body)); + return jsonResponse(200, { + success: true, + status: 200, + result: { + result: [ + { + ruleName: "封禁海外访问", + protectionDomains: ["www.example.com"], + switch: 1, + updateTime: "2023-10-01 12:00:00", + ruleKey: "rule_key_001", + ruleID: 1001, + ruleType: "saas", + action: "deny", + value: { domestic: [], overseas: ["US", "JP"] }, + } + ], + totalCount: 1, + }, + }); + }); + + const res1 = await rpcdef(buildCtx({ req: { pageNo: 1, pageSize: 10 } }))[REGIONRULESLIST_PATH](); + const res2 = await rpcdef(buildCtx({ + req: { + pageNo: 2, + pageSize: 20, + switch: 1, + subdomain: ["example.com"], + ruleID: "rule123", + ruleName: "测试规则", + action: "deny", + }, + }))[REGIONRULESLIST_PATH](); + + assert.deepEqual(capturedBodies[0], { pageNo: 1, pageSize: 10 }); + assert.deepEqual(capturedBodies[1], { + pageNo: 2, + pageSize: 20, + switch: 1, + subdomain: ["example.com"], + ruleID: "rule123", + ruleName: "测试规则", + action: "deny", + }); + assert.deepEqual(res1.result.result[0].value.overseas, ["US", "JP"]); + assert.equal(res2.result.result[0].action, "deny"); +}); + +test("SDK handlers use ctx.request", async () => { + setFetch(async () => jsonResponse(200, { + status: 0, + success: true, + result: { + switch: 1, + protectionDomains: ["example.com"], + templateType: "high", + templateKey: "tpl-from-request", + action: "block", + groupKey: "high", + ruleName: "SQL注入防护", + ruleID: 10001, + groupName: "高", + }, + })); + + const res = await handlers[METHOD_WEBTEMPLATEDETAIL_FULL](buildCtx({ request: { templateKey: "tpl-from-request" } })); + assert.equal(res.result.templateKey, "tpl-from-request"); +}); + +test("HTTP 401 maps to PERMISSION_DENIED", async () => { + setFetch(async () => jsonResponse(401, { message: "unauthorized" })); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { templateKey: "tpl-1" } }))[WEBTEMPLATEDETAIL_PATH](), + "PERMISSION_DENIED", + 401, + /upstream http 401/, + ); +}); + +test("HTTP 400 maps to FAILED_PRECONDITION", async () => { + setFetch(async () => jsonResponse(400, { message: "bad request" })); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { templateKey: "template-001", switch: 1 } }))[WEBTEMPLATESWITCH_PATH](), + "FAILED_PRECONDITION", + 400, + /upstream http 400/, + ); +}); + +test("HTTP 500 maps to UNAVAILABLE", async () => { + setFetch(async () => jsonResponse(500, { message: "server error" })); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { templateKey: "server-error-key" } }))[WEBTEMPLATEDETAIL_PATH](), + "UNAVAILABLE", + 500, + /upstream http 500/, + ); +}); + +test("WhiteRules invalid JSON response maps to UNKNOWN", async () => { + setFetch(async () => textResponse(200, "not-valid-json!!!")); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { ruleKey: "bad-json-key" } }))[WHITERULESDETAIL_PATH](), + "UNKNOWN", + 0, + /not valid JSON/, + ); +}); + +test("HTTP error body is truncated", async () => { + const longBody = "x".repeat(_test.MAX_HTTP_BODY_CHARS + 50); + setFetch(async () => textResponse(500, longBody)); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { templateKey: "tpl-1" } }))[WEBTEMPLATEDETAIL_PATH](), + "UNAVAILABLE", + 500, + /upstream http 500/, + { httpBody: "x".repeat(_test.MAX_HTTP_BODY_CHARS) }, + ); +}); + +test("native fetch timeout maps to UNAVAILABLE", async () => { + setFetch((_url, init) => new Promise((resolve, reject) => { + init.signal.addEventListener("abort", () => reject(Object.assign(new Error("The operation was aborted"), { name: "AbortError" }))); + void resolve; + })); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { templateKey: "tpl-1" }, limits: { timeoutMs: 10 } }))[WEBTEMPLATEDETAIL_PATH](), + "UNAVAILABLE", + 0, + /request timed out/, + ); +}); + +test("network error maps to UNAVAILABLE", async () => { + setFetch(async () => { + throw Object.assign(new Error("fetch failed"), { cause: new Error("connection refused") }); + }); + await expectRejectPayload( + () => rpcdef(buildCtx({ req: { templateKey: "tpl-1" } }))[WEBTEMPLATEDETAIL_PATH](), + "UNAVAILABLE", + 0, + /connection refused/, + ); +}); + +test("helper functions cover edge cases", () => { + assert.equal(_test.unwrapString(undefined), ""); + assert.equal(_test.unwrapString(null), ""); + assert.equal(_test.unwrapString({ value: { value: "x" } }), "x"); + assert.equal(_test.normalizeBaseUrl("https://api.example.com///"), "https://api.example.com"); + assert.equal(_test.resolveAccessKey({ ak: "ak1" }), "ak1"); + assert.equal(_test.resolveSecretKey({ sk: "sk1" }), "sk1"); + + assert.deepEqual(_test.buildTlsOptions({ skipTlsVerify: true }), { + skipTlsVerify: true, + tlsInsecureSkipVerify: true, + insecureSkipVerify: true, + }); + assert.deepEqual(_test.buildTlsOptions({}), {}); + + assert.equal(_test.mapHttpStatusToCode(401), "PERMISSION_DENIED"); + assert.equal(_test.mapHttpStatusToCode(404), "FAILED_PRECONDITION"); + assert.equal(_test.mapHttpStatusToCode(500), "UNAVAILABLE"); + assert.equal(_test.MAX_HTTP_BODY_CHARS, 200); + assert.equal(_test.truncateHttpBody("abcdef", 3), "abc"); + + assert.equal( + _test.buildUrl("https://api.example.com", "/v1/waf/webTemplate/detail", { templateKey: "abc" }), + "https://api.example.com/v1/waf/webTemplate/detail?templateKey=abc", + ); + assert.equal(_test.pickString({ a: "", b: "hello" }, ["a", "b", "c"]), "hello"); + assert.equal(_test.toInteger("12.9", 0), 12); + assert.equal(_test.toInteger(NaN, 0), 0); + assert.equal(_test.toInteger(undefined, 5), 5); + assert.equal(_test.bceEncode("/v1/waf/webTemplate/detail", "/-_.~"), "/v1/waf/webTemplate/detail"); + assert.equal(_test.buildCanonicalQueryString({ b: 2, a: 1 }), "a=1&b=2"); + const expectedCanonicalRequest = _test.buildCanonicalRequest( + "GET", + "/v1/waf/webTemplate/detail", + { templateKey: "abc" }, + { + Host: "api.example.com", + "Content-Type": _test.BCE_CONTENT_TYPE, + "x-bce-date": "2026-06-29T00:00:00Z", + }, + ); + const authStringPrefix = `bce-auth-v1/test-ak/2026-06-29T00:00:00Z/${_test.BCE_EXPIRE_SECONDS}`; + const expectedSignature = createHmac("sha256", createHmac("sha256", "test-sk").update(authStringPrefix).digest()) + .update(expectedCanonicalRequest) + .digest("hex"); + assert.equal( + _test.buildBceAuthorization({ + accessKey: "test-ak", + secretKey: "test-sk", + method: "GET", + uri: "/v1/waf/webTemplate/detail", + queryParams: { templateKey: "abc" }, + host: "api.example.com", + xBceDate: "2026-06-29T00:00:00Z", + }), + `bce-auth-v1/test-ak/2026-06-29T00:00:00Z/${_test.BCE_EXPIRE_SECONDS}/${_test.BCE_SIGNED_HEADERS}/${expectedSignature}`, + ); + assert.match( + _test.buildBceAuthorization({ + accessKey: "test-ak", + secretKey: "test-sk", + method: "GET", + uri: "/v1/waf/webTemplate/detail", + queryParams: { templateKey: "abc" }, + host: "api.example.com", + xBceDate: "2026-06-29T00:00:00Z", + }), + /^bce-auth-v1\/test-ak\/2026-06-29T00:00:00Z\/1800\/content-type;host;x-bce-date\//, + ); + assert.deepEqual(_test.mapWhiteRulesdetailTarget({ field: "header", key: "k", match: "contains", value: ["a"] }), { + field: "header", + key: "k", + match: "contains", + value: ["a"], + }); + assert.deepEqual(_test.mapWhiteRuleslistItem({ + ruleName: "demo", + ruleType: "saas", + protectionDomains: ["example.com"], + switch: 1, + updateTime: "2026-03-10T00:00:00Z", + ruleKey: "rule-1", + ruleID: 1001, + ignoreModules: ["base"], + ignoreIds: ["1"], + }), { + ruleName: "demo", + ruleType: "saas", + protectionDomains: ["example.com"], + switch: 1, + updateTime: "2026-03-10T00:00:00Z", + ruleKey: "rule-1", + ruleID: 1001, + ignoreModules: ["base"], + ignoreIds: ["1"], + }); + assert.deepEqual(_test.mapRegionRuleslistValue({ domestic: [], overseas: ["US"] }), { + domestic: [], + overseas: ["US"], + }); +}); + +test("logFlow handles circular references", () => { + const calls = []; + console.log = (...args) => calls.push(args); + const circular = {}; + circular.self = circular; + _test.logFlow({ meta: { instance_id: "inst" } }, "test", circular); + assert.equal(calls.length, 1); + assert.match(calls[0][0], /BaiduWAF_WAFWebTemplate.*test.*inst=inst/); +}); + +test("mock upstream detail end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { templateKey: "template_key_example" }, + meta: { instance_id: "mock-inst" }, + }); + const res = await rpcdef(ctx)[WEBTEMPLATEDETAIL_PATH](); + assert.equal(res.success, true); + assert.equal(res.result.templateKey, "template_key_example"); + assert.ok(mockServer.requests.length >= 1); + assert.equal(mockServer.requests[0].method, "GET"); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream save end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { + name: "我的防护模板", + bindInfo: [{ instanceID: "instance-001", subdomains: ["www.example.com"] }], + switch: 1, + templateType: "saas", + action: "log", + rulesGroupID: "middle", + }, + }); + const res = await rpcdef(ctx)[WEBTEMPLATESAVE_PATH](); + assert.equal(res.status, 200); + assert.equal(res.result.templateKey, "template-abc123"); + assert.ok(mockServer.requests.some((req) => req.method === "POST" && req.url.startsWith("/v1/waf/webTemplate/save"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream switch end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { templateKey: "template-abc123", switch: 1 }, + }); + const res = await rpcdef(ctx)[WEBTEMPLATESWITCH_PATH](); + assert.equal(res.success, true); + assert.equal(res.status, 200); + assert.ok(mockServer.requests.some((req) => req.method === "POST" && req.url.startsWith("/v1/waf/webTemplate/switch"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream list end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { + pageNo: 1, + pageSize: 10, + switch: -1, + action: "", + templateName: "", + }, + }); + const res = await rpcdef(ctx)[WEBTEMPLATELIST_PATH](); + assert.equal(res.success, true); + assert.equal(res.result.totalCount, 1); + assert.ok(mockServer.requests.some((req) => req.method === "POST" && req.url.startsWith("/v1/waf/webTemplate/list"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream delete end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { templateKey: "template-abc123" }, + }); + const res = await rpcdef(ctx)[WEBTEMPLATEDELETE_PATH](); + assert.equal(res.success, true); + assert.equal(res.alreadyGone, false); + assert.ok(mockServer.requests.some((req) => req.method === "DELETE" && req.url.startsWith("/v1/waf/webTemplate/delete?templateKey=template-abc123"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream whiteRulesdetail end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { ruleKey: "rule-key-123" }, + }); + const res = await rpcdef(ctx)[WHITERULESDETAIL_PATH](); + assert.equal(res.success, true); + assert.equal(res.result.ruleID, 1001); + assert.ok(mockServer.requests.some((req) => req.method === "GET" && req.url.startsWith("/v1/waf/whiteRules/detail?ruleKey=rule-key-123"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream whiteRulesdelete end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { ruleKey: "rule-key-123" }, + }); + const res = await rpcdef(ctx)[WHITERULESDELETE_PATH](); + assert.equal(res.success, true); + assert.equal(res.alreadyGone, false); + assert.deepEqual(res.result, []); + assert.ok(mockServer.requests.some((req) => req.method === "DELETE" && req.url.startsWith("/v1/waf/whiteRules/delete?ruleKey=rule-key-123"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream whiteRulesdelete returns idempotent success on 404", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { ruleKey: "gone-rule" }, + }); + const res = await rpcdef(ctx)[WHITERULESDELETE_PATH](); + assert.equal(res.success, true); + assert.equal(res.alreadyGone, true); + assert.deepEqual(res.result, []); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream whiteRulesswitch end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { ruleKey: "rule-key-123", switch: 1 }, + }); + const res = await rpcdef(ctx)[WHITERULESSWITCH_PATH](); + assert.equal(res.success, true); + assert.deepEqual(res.result, ["example"]); + assert.ok(mockServer.requests.some((req) => req.method === "POST" && req.url.startsWith("/v1/waf/whiteRules/switch"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream whiteRuleslist end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { + pageNo: 1, + pageSize: 10, + }, + }); + const res = await rpcdef(ctx)[WHITERULESLIST_PATH](); + assert.equal(res.success, true); + assert.equal(res.result.totalCount, 1); + assert.equal(res.result.result[0].ruleKey, "rule-key-123"); + assert.ok(mockServer.requests.some((req) => req.method === "POST" && req.url.startsWith("/v1/waf/whiteRules/list"))); + } finally { + await mockServer.close(); + } +}); + +test("mock upstream regionRuleslist end-to-end", async () => { + const mockServer = await createMockServer(); + try { + const ctx = buildCtx({ + config: { api_base: mockServer.url }, + req: { pageNo: 1, pageSize: 10 }, + }); + const res = await rpcdef(ctx)[REGIONRULESLIST_PATH](); + assert.equal(res.success, true); + assert.equal(res.result.totalCount, 1); + assert.ok(mockServer.requests.some((req) => req.method === "POST" && req.url.startsWith("/v1/waf/regionRules/list"))); + } finally { + await mockServer.close(); + } +}); + +test("service object is not empty", () => { + assert.ok(service); + assert.equal(typeof service, "object"); +}); diff --git a/services/baiduwaf__waf-web-template/test/mock_upstream.js b/services/baiduwaf__waf-web-template/test/mock_upstream.js new file mode 100644 index 00000000..6d18714b --- /dev/null +++ b/services/baiduwaf__waf-web-template/test/mock_upstream.js @@ -0,0 +1,472 @@ +/* node:coverage disable */ +import http from "node:http"; +import { createHmac, timingSafeEqual } from "node:crypto"; + +const BCE_CONTENT_TYPE = "application/json;charset=utf-8"; +const BCE_EXPIRE_SECONDS = 1800; +const BCE_SIGNED_HEADERS = "content-type;host;x-bce-date"; + +const readBody = (req) => new Promise((resolve, reject) => { + const chunks = []; + req.on("data", (chunk) => chunks.push(chunk)); + req.on("end", () => resolve(Buffer.concat(chunks).toString())); + req.on("error", reject); +}); + +const sendJson = (res, status, body) => { + res.writeHead(status, { "content-type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(body)); +}; + +const bceEncode = (value, safe = "-_.~") => { + const safeChars = new Set(String(safe).split("")); + let result = ""; + for (const ch of String(value)) { + if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9") || safeChars.has(ch)) { + result += ch; + continue; + } + for (const byte of Buffer.from(ch)) { + result += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`; + } + } + return result; +}; + +const buildCanonicalQueryString = (queryParams = {}) => Object.entries(queryParams) + .filter(([, value]) => value !== undefined && value !== null && value !== "") + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${bceEncode(key)}=${bceEncode(value)}`) + .join("&"); + +const buildCanonicalHeaders = (headersToSign = {}) => Object.entries(headersToSign) + .sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase())) + .map(([key, value]) => `${bceEncode(key.toLowerCase(), "-_.~")}:${bceEncode(value)}`) + .join("\n"); + +const buildCanonicalRequest = (method, uri, queryParams, headersToSign) => [ + String(method || "GET").toUpperCase(), + bceEncode(uri, "/-_.~"), + buildCanonicalQueryString(queryParams), + buildCanonicalHeaders(headersToSign), +].join("\n"); + +const buildExpectedAuthorization = ({ accessKey, secretKey, method, pathname, queryParams, host, xBceDate }) => { + const headersToSign = { + Host: host, + "Content-Type": BCE_CONTENT_TYPE, + "x-bce-date": xBceDate, + }; + const canonicalRequest = buildCanonicalRequest(method, pathname, queryParams, headersToSign); + const authStringPrefix = `bce-auth-v1/${accessKey}/${xBceDate}/${BCE_EXPIRE_SECONDS}`; + const signingKey = createHmac("sha256", secretKey).update(authStringPrefix).digest(); + const signature = createHmac("sha256", signingKey).update(canonicalRequest).digest("hex"); + return `${authStringPrefix}/${BCE_SIGNED_HEADERS}/${signature}`; +}; + +const verifyAuthorization = (req, url) => { + const authHeader = String(req.headers.authorization || ""); + const xBceDate = String(req.headers["x-bce-date"] || ""); + if (!authHeader.startsWith("bce-auth-v1/test-ak/")) { + return "unauthorized: invalid Authorization header"; + } + if (!xBceDate) { + return "unauthorized: missing x-bce-date"; + } + + const queryParams = Object.fromEntries(url.searchParams.entries()); + const expected = buildExpectedAuthorization({ + accessKey: "test-ak", + secretKey: "test-sk", + method: req.method || "GET", + pathname: url.pathname, + queryParams, + host: String(req.headers.host || ""), + xBceDate, + }); + + const actualBuffer = Buffer.from(authHeader); + const expectedBuffer = Buffer.from(expected); + if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) { + return "unauthorized: signature mismatch"; + } + + return ""; +}; + +export const createMockServer = async (options = {}) => { + const requests = []; + + const server = http.createServer((req, res) => { + (async () => { + const url = new URL(req.url || "/", "http://localhost"); + const body = await readBody(req); + requests.push({ method: req.method, url: String(req.url), body, headers: req.headers }); + + const authError = verifyAuthorization(req, url); + if (authError) { + sendJson(res, 401, { success: false, message: authError }); + return; + } + + if (req.method === "GET" && url.pathname === "/v1/waf/webTemplate/detail") { + const templateKey = url.searchParams.get("templateKey")?.trim(); + if (!templateKey) { + sendJson(res, 400, { success: false, message: "templateKey is required" }); + return; + } + if (templateKey === "not-found-key") { + sendJson(res, 404, { success: false, message: "resource not found" }); + return; + } + if (templateKey === "server-error-key") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (templateKey === "bad-json-key") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + status: 0, + result: { + switch: 1, + protectionDomains: ["example.com"], + templateType: "high", + templateKey, + action: "block", + groupKey: "high", + ruleName: "SQL注入防护", + ruleID: 10001, + groupName: "高" + }, + success: true + }); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/waf/webTemplate/save") { + let payload; + try { + payload = JSON.parse(body || "{}"); + } catch { + sendJson(res, 400, { success: false, message: "request body must be JSON" }); + return; + } + if (!payload.name || payload.switch === undefined || !payload.templateType || !payload.action || !payload.rulesGroupID) { + sendJson(res, 400, { success: false, message: "missing required fields" }); + return; + } + if (payload.name === "server-error") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (payload.name === "bad-json") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + status: 200, + result: { + templateKey: payload.templateKey || "template-abc123" + } + }); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/waf/webTemplate/switch") { + let payload; + try { + payload = JSON.parse(body || "{}"); + } catch { + sendJson(res, 400, { success: false, message: "request body must be JSON" }); + return; + } + if (!payload.templateKey || payload.switch === undefined) { + sendJson(res, 400, { success: false, message: "missing required fields" }); + return; + } + if (payload.templateKey === "server-error-key") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (payload.templateKey === "bad-json-key") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + status: 200, + }); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/waf/webTemplate/list") { + let payload; + try { + payload = JSON.parse(body || "{}"); + } catch { + sendJson(res, 400, { success: false, message: "request body must be JSON" }); + return; + } + if (payload.pageNo === undefined || payload.pageSize === undefined || payload.switch === undefined || payload.action === undefined || payload.templateName === undefined) { + sendJson(res, 400, { success: false, message: "missing required fields" }); + return; + } + if (payload.templateName === "server-error") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (payload.templateName === "bad-json") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + status: 200, + success: true, + result: { + result: [ + { + ruleName: "规则名称", + templateKey: "web-list-001", + templateType: "saas", + protectionDomains: ["example.com"], + action: "deny", + switch: 1, + updateTime: "2023-01-01 00:00:00", + ruleID: payload.ruleID ?? 123, + } + ], + totalCount: 1, + } + }); + return; + } + + if (req.method === "DELETE" && url.pathname === "/v1/waf/webTemplate/delete") { + const templateKey = url.searchParams.get("templateKey")?.trim(); + if (!templateKey) { + sendJson(res, 400, { success: false, message: "templateKey is required" }); + return; + } + if (templateKey === "gone-key") { + sendJson(res, 404, { success: false, message: "resource not found" }); + return; + } + if (templateKey === "server-error-key") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (templateKey === "bad-json-key") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + result: {} + }); + return; + } + + if (req.method === "GET" && url.pathname === "/v1/waf/whiteRules/detail") { + const ruleKey = url.searchParams.get("ruleKey")?.trim(); + if (!ruleKey) { + sendJson(res, 400, { success: false, message: "ruleKey is required" }); + return; + } + if (ruleKey === "server-error-key") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (ruleKey === "bad-json-key") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + status: 200, + result: { + ruleName: "白名单规则", + ruleID: 1001, + ruleType: "saas", + protectionDomains: ["example.com"], + switch: 1, + updateTime: "2026-03-10T00:00:00Z", + targets: [ + { + field: "header", + key: "User-Agent", + match: "contains", + value: ["curl"] + } + ] + } + }); + return; + } + + if (req.method === "DELETE" && url.pathname === "/v1/waf/whiteRules/delete") { + const ruleKey = url.searchParams.get("ruleKey")?.trim(); + if (!ruleKey) { + sendJson(res, 400, { success: false, message: "ruleKey is required" }); + return; + } + if (ruleKey === "gone-rule") { + sendJson(res, 404, { success: false, message: "resource not found" }); + return; + } + if (ruleKey === "server-error-key") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (ruleKey === "bad-json-key") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + result: [] + }); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/waf/whiteRules/switch") { + let payload; + try { + payload = JSON.parse(body || "{}"); + } catch { + sendJson(res, 400, { success: false, message: "request body must be JSON" }); + return; + } + if (!payload.ruleKey || payload.switch === undefined) { + sendJson(res, 400, { success: false, message: "missing required fields" }); + return; + } + if (payload.ruleKey === "server-error-key") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (payload.ruleKey === "bad-json-key") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + result: ["example"] + }); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/waf/whiteRules/list") { + let payload; + try { + payload = JSON.parse(body || "{}"); + } catch { + sendJson(res, 400, { success: false, message: "request body must be JSON" }); + return; + } + if (payload.pageNo === undefined || payload.pageSize === undefined) { + sendJson(res, 400, { success: false, message: "missing required fields" }); + return; + } + if (payload.ruleName === "server-error") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (payload.ruleName === "bad-json") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + status: 200, + result: { + result: [ + { + ruleName: "示例规则", + ruleType: "saas", + protectionDomains: ["example.com"], + switch: 1, + updateTime: "2023-10-27T10:00:00Z", + ruleKey: payload.ruleID ? `rule-${payload.ruleID}` : "rule-key-123", + ruleID: 1001, + ignoreModules: ["base"], + ignoreIds: [] + } + ], + totalCount: 1 + } + }); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/waf/regionRules/list") { + let payload; + try { + payload = JSON.parse(body || "{}"); + } catch { + sendJson(res, 400, { success: false, message: "request body must be JSON" }); + return; + } + if (payload.pageNo === undefined || payload.pageSize === undefined) { + sendJson(res, 400, { success: false, message: "missing required fields" }); + return; + } + if (payload.ruleName === "server-error") { + sendJson(res, 500, { success: false, message: "internal server error" }); + return; + } + if (payload.ruleName === "bad-json") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("not-valid-json{{{"); + return; + } + sendJson(res, 200, { + success: true, + status: 200, + result: { + result: [ + { + ruleName: "封禁海外访问", + protectionDomains: ["www.example.com"], + switch: 1, + updateTime: "2023-10-01 12:00:00", + ruleKey: payload.ruleID ? `region-${payload.ruleID}` : "rule_key_001", + ruleID: 1001, + ruleType: "saas", + action: payload.action ?? "deny", + value: { + domestic: [], + overseas: ["US", "JP"] + } + } + ], + totalCount: 1 + } + }); + return; + } + + sendJson(res, 404, { success: false, message: "not found" }); + })().catch((err) => { + sendJson(res, 500, { message: err?.message || "internal error" }); + }); + }); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address(); + return { + requests, + url: `http://127.0.0.1:${port}`, + close: () => new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))) + }; +}; diff --git a/services/bin/baiduwaf-waf-web-template.js b/services/bin/baiduwaf-waf-web-template.js new file mode 100755 index 00000000..c9b8eb23 --- /dev/null +++ b/services/bin/baiduwaf-waf-web-template.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../baiduwaf__waf-web-template/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../baiduwaf__waf-web-template/bin/baiduwaf-waf-web-template.js", import.meta.url)), +}); diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 4124b804..9514f9da 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -17,6 +17,9 @@ const services = { entryFile: "../chaitin__safeline-waf-eliminate-false-positive/bin/safeline-waf-eliminate-false-positive.js", serviceModule: "../chaitin__safeline-waf-eliminate-false-positive/src/service.js", }, + "baiduwaf-waf-web-template": { + entryFile: "../baiduwaf__waf-web-template/bin/baiduwaf-waf-web-template.js", + serviceModule: "../baiduwaf__waf-web-template/src/service.js", "cloudatlas": { entryFile: "../chaitin__cloudatlas/bin/cloudatlas.js", serviceModule: "../chaitin__cloudatlas/src/service.js", diff --git a/services/package.json b/services/package.json index 2fbea088..13860946 100644 --- a/services/package.json +++ b/services/package.json @@ -6,6 +6,7 @@ "bin": { "octobus-tentacles": "bin/octobus-tentacles.js", "alibaba-cloud-simple-application-server-firewall": "bin/alibaba-cloud-simple-application-server-firewall.js", + "baiduwaf-waf-web-template": "bin/baiduwaf-waf-web-template.js", "cloudatlas": "bin/cloudatlas.js", "das-gateway-v3": "bin/das-gateway-v3.js", "das-tgfw-v6": "bin/das-tgfw-v6.js", @@ -70,6 +71,7 @@ }, "files": [ "bin/alibaba-cloud-simple-application-server-firewall.js", + "bin/baiduwaf-waf-web-template.js", "bin/cloudatlas.js", "bin/das-gateway-v3.js", "bin/das-tgfw-v6.js", @@ -132,6 +134,7 @@ "bin/opencti.js", "bin/wangsu-label-ip.js", "alibaba-cloud__simple-application-server-firewall", + "baiduwaf__waf-web-template", "chaitin__cloudatlas", "das__gateway_v3", "das__tgfw_v6",