From 7e74eeb333cb3c9314620880375eb99ec2cec7a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E8=B4=A4=E6=B6=9B?= <601803023@qq.com> Date: Mon, 1 Sep 2025 14:07:57 +0800 Subject: [PATCH] feat(wasm-plugin): add tests and docs for hmac-auth-apisix (#2842) --- .../extensions/hmac-auth-apisix/README.md | 209 +++ .../extensions/hmac-auth-apisix/README_EN.md | 215 +++ .../hmac-auth-apisix/config/config.go | 26 +- .../extensions/hmac-auth-apisix/go.mod | 6 +- .../extensions/hmac-auth-apisix/go.sum | 13 +- .../extensions/hmac-auth-apisix/main.go | 58 +- .../extensions/hmac-auth-apisix/main_test.go | 1279 +++++++++++++++++ .../replay-protection/config/config.go | 3 +- .../extensions/replay-protection/main.go | 6 +- 9 files changed, 1769 insertions(+), 46 deletions(-) create mode 100644 plugins/wasm-go/extensions/hmac-auth-apisix/README.md create mode 100644 plugins/wasm-go/extensions/hmac-auth-apisix/README_EN.md create mode 100644 plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/README.md b/plugins/wasm-go/extensions/hmac-auth-apisix/README.md new file mode 100644 index 000000000..6ed560847 --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/README.md @@ -0,0 +1,209 @@ +--- +title: APISIX HMAC 认证 +keywords: [higress,hmac auth,apisix] +description: APISIX HMAC 认证插件配置参考 +--- + +## 功能说明 + +`hmac-auth-apisix` 插件兼容 Apache APISIX 的 HMAC 认证机制,通过 HMAC 算法为 HTTP 请求生成防篡改的数字签名,实现请求的身份认证和权限控制。该插件完全兼容 Apache APISIX HMAC 认证插件的配置和签名算法,签名生成方法可参考 [Apache APISIX HMAC 认证文档](https://apisix.apache.org/docs/apisix/plugins/hmac-auth/) + +## 运行属性 + +插件执行阶段:`认证阶段` +插件执行优先级:`330` + +## 配置字段 + +**注意:** + +- 在一个规则里,鉴权配置和认证配置不可同时存在 +- 对于通过认证鉴权的请求,请求的 header 会被添加一个 `X-Mse-Consumer` 字段,用以标识调用者的名称 + +### 认证配置 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------------------- | --------------- | -------------------------- | ------------------------------------------- | ------------------------------------------------------------ | +| `global_auth` | bool | 选填(**仅实例级别配置**) | - | 只能在实例级别配置,若配置为 true,则全局生效认证机制;若配置为 false,则只对做了配置的域名和路由生效认证机制,若不配置则仅当没有域名和路由配置时全局生效(兼容老用户使用习惯) | +| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 | +| `allowed_algorithms` | array of string | 选填 | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | 允许的 HMAC 算法列表。有效值为 "hmac-sha1"、"hmac-sha256" 和 "hmac-sha512" 的组合 | +| `clock_skew` | number | 选填 | 300 | 客户端请求的时间戳与 Higress 服务器当前时间之间允许的最大时间差(以秒为单位)。这有助于解决客户端和服务器之间的时间同步差异,并防止重放攻击。时间戳将根据 Date 头中的时间(必须为 GMT 格式)进行计算。如果配置为0,会跳过该校验 | +| `signed_headers` | array of string | 选填 | - | 客户端请求的 HMAC 签名中应包含的 HMAC 签名头列表 | +| `validate_request_body` | boolean | 选填 | false | 如果为 true,则验证请求正文的完整性,以确保在传输过程中没有被篡改。具体来说,插件会创建一个 SHA-256 的 base64 编码 digest,并将其与 `Digest` 头进行比较。如果 `Digest` 头丢失或 digest 不匹配,验证将失败 | +| `hide_credentials` | boolean | 选填 | false | 如果为 true,则不会将授权请求头传递给上游服务 | +| `anonymous_consumer` | string | 选填 | - | 匿名消费者名称。如果已配置,则允许匿名用户绕过身份验证 | + + +`consumers`中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ------------ | -------- | -------- | ------------ | ---------------------------------------------- | +| `access_key` | string | 必填 | - | 消费者的唯一标识符,用于标识相关配置,例如密钥 | +| `secret_key` | string | 必填 | - | 用于生成 HMAC 的密钥 | +| `name` | string | 选填 | `access_key` | 配置该 consumer 的名称 | + +### 鉴权配置(非必需) + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ------- | --------------- | ------------------------ | ------ | ------------------------------------------------------------ | +| `allow` | array of string | 选填(**非实例级别配置**) | - | 只能在路由或域名等细粒度规则上配置,对于符合匹配条件的请求,配置允许访问的 consumer,从而实现细粒度的权限控制 | + +## 配置示例 + +### 全局配置认证和路由粒度鉴权 + +以下配置用于对网关特定路由或域名开启 Hmac Auth 认证和鉴权。**注意:access_key 字段不可重复** + +#### 示例1:基础路由与域名鉴权配置 + +**实例级别插件配置**: +```yaml +global_auth: false +consumers: +- name: consumer1 + access_key: consumer1-key + secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5 +- name: consumer2 + access_key: consumer2-key + secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +``` + +**路由级配置**(适用于 route-a 和 route-b): +```yaml +allow: +- consumer1 # 仅允许consumer1访问 +``` + +**域名级配置**(适用于 `*.example.com` 和 `test.com`): +```yaml +allow: +- consumer2 # 仅允许consumer2访问 +``` + +**配置说明**: +- 路由名称(如 route-a、route-b)对应网关路由创建时定义的名称,匹配时仅允许consumer1访问 +- 域名匹配(如 `*.example.com`、`test.com`)用于过滤请求域名,匹配时仅允许consumer2访问 +- 未在allow列表中的调用者将被拒绝访问 + +**请求与响应示例**: + +1. **验证通过场景** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \ +-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- 响应:返回后端服务正常响应 +- 附加信息:认证通过后会自动添加请求头 `X-Mse-Consumer: consumer1` 传递给后端 + +2. **请求方法修改导致验签失败** +```shell +curl -X PUT 'http://localhost:8082/foo' \ # 此处将POST改为PUT +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \ +-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- 响应:`401 Unauthorized` +- 错误信息:`{"message":"client request can't be validated: Invalid signature"}` + +3. **不在允许列表中的调用者** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer2-key",algorithm="hmac-sha256",headers="@request-target date",signature="5sqSbDX9b91dQsfQra2hpluM7O6/yhS7oLcKPQylyCo="' \ +-H 'Date:Sat, 30 Aug 2025 00:54:18 GMT' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- 响应:`401 Unauthorized` +- 错误信息:`{"message":"client request can't be validated: consumer 'consumer2' is not allowed"}` + +4. **时间戳过期** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization: Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="gvIUwoYNiK57w6xX2g1Ntpk8lfgD7z+jgom434r5qwg="' \ +-H 'Date: Sat, 30 Aug 2025 00:40:21 GMT' \ # 过期的时间戳 +-H 'Content-Type: application/json' \ +-d '{}' +``` +- 响应:`401 Unauthorized` +- 错误信息:`{"message":"client request can't be validated: Clock skew exceeded"}` + +#### 示例2:带自定义签名头与请求体验证的配置 + +**实例级别插件配置**: +```yaml +global_auth: false +consumers: +- name: consumer1 + access_key: consumer1-key + secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5 +- name: consumer2 + access_key: consumer2-key + secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +signed_headers: # 需要纳入签名的自定义请求头 +- X-Custom-Header-A +- X-Custom-Header-B +validate_request_body: true # 启用请求体签名校验 +``` + +**请求与响应示例**: + +1. **验证通过场景** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \ +-H 'Date:Sat, 30 Aug 2025 01:04:06 GMT' \ +-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # 请求体摘要 +-H 'X-Custom-Header-A:test1' \ +-H 'X-Custom-Header-B:test2' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- 响应:返回后端服务正常响应 + +2. **缺少签名头** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \ +-H 'Date:Sat, 30 Aug 2025 01:04:06 GMT' \ +-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ +-H 'X-Custom-Header-B:test2' \ # 缺少X-Custom-Header-A +-H 'Content-Type: application/json' \ +-d '{}' +``` +- 响应:`401 Unauthorized` +- 错误信息:`{"message":"client request can't be validated: expected header "X-Custom-Header-A" missing in signing"}` + +3. **请求体被篡改** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="dSbv6pdQOcgkN89TmSxiT8F9nypbPUqAR2E7ELL8K2s="' \ +-H 'Date:Sat, 30 Aug 2025 01:10:17 GMT' \ +-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # 与实际body不匹配 +-H 'X-Custom-Header-A:test1' \ +-H 'X-Custom-Header-B:test2' \ +-H 'Content-Type: application/json' \ +-d '{"key":"value"}' # 篡改后的请求体 +``` +- 响应:`401 Unauthorized` +- 错误信息:`{"message":"client request can't be validated: Invalid digest"}` + +### 网关实例级别开启全局认证 + +以下配置将在网关实例级别开启 Hmac Auth 认证,**所有请求必须经过认证才能访问**: + +```yaml +global_auth: true # 开启全局认证 +consumers: +- name: consumer1 + access_key: consumer1-key + secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5 +- name: consumer2 + access_key: consumer2-key + secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +``` + +**说明**:当 `global_auth: true` 时,所有访问网关的请求都需要携带有效的认证信息,未认证的请求将被直接拒绝 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/README_EN.md b/plugins/wasm-go/extensions/hmac-auth-apisix/README_EN.md new file mode 100644 index 000000000..5d5ebbc53 --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/README_EN.md @@ -0,0 +1,215 @@ +# APISIX HMAC Authentication +keywords: [higress, hmac auth, apisix] +description: Configuration Reference for APISIX HMAC Authentication Plugin +--- + +## Feature Description +The `hmac-auth-apisix` plugin is compatible with Apache APISIX's HMAC authentication mechanism. It generates tamper-proof digital signatures for HTTP requests using the HMAC algorithm, enabling request identity authentication and permission control. This plugin is fully compatible with the configuration and signature algorithm of the Apache APISIX HMAC Authentication Plugin. For signature generation methods, please refer to the [Apache APISIX HMAC Authentication Documentation](https://apisix.apache.org/docs/apisix/plugins/hmac-auth/). + + +## Operational Attributes +- Plugin Execution Phase: `Authentication Phase` +- Plugin Execution Priority: `330` + + +## Configuration Fields +**Note:** +- In a single rule, authentication configuration and authorization configuration cannot coexist. +- For requests that pass authentication and authorization, a `X-Mse-Consumer` field will be added to the request header to identify the caller's name. + + +### Authentication Configuration + +| Name | Data Type | Requirements | Default Value | Description | +| ----------------------- | ---------------- | ----------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `global_auth` | bool | Optional (**Instance-level configuration only**) | - | Can only be configured at the instance level. If set to `true`, the authentication mechanism takes effect globally; if set to `false`, authentication only applies to domains and routes with specific configurations. If not configured, it takes effect globally only when there are no domain or route configurations (to maintain compatibility with legacy user habits). | +| `consumers` | array of object | Required | - | Configures service callers for request authentication. | +| `allowed_algorithms` | array of string | Optional | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | List of allowed HMAC algorithms. Valid values are combinations of "hmac-sha1", "hmac-sha256", and "hmac-sha512". | +| `clock_skew` | number | Optional | 300 | Maximum allowed time difference (in seconds) between the timestamp of the client request and the current time of the Higress server. This helps resolve time synchronization differences between the client and server and prevents replay attacks. The timestamp is calculated based on the time in the `Date` header (must be in GMT format). If set to `0`, this check is skipped. | +| `signed_headers` | array of string | Optional | - | List of HTTP headers that should be included in the HMAC signature of the client request. | +| `validate_request_body` | boolean | Optional | false | If set to `true`, the integrity of the request body is verified to ensure no tampering during transmission. Specifically, the plugin creates a SHA-256 base64-encoded digest and compares it with the `Digest` header. Verification fails if the `Digest` header is missing or the digest does not match. | +| `hide_credentials` | boolean | Optional | false | If set to `true`, the authorization request header will not be passed to the upstream service. | +| `anonymous_consumer` | string | Optional | - | Name of the anonymous consumer. If configured, anonymous users are allowed to bypass identity authentication. | + + +### Configuration Fields for Each Item in `consumers` + +| Name | Data Type | Requirements | Default Value | Description | +|--------------|-----------|--------------|---------------|-----------------------------------------------------------------------------| +| `access_key` | string | Required | - | A unique identifier for the consumer, used to reference configurations such as the secret key. | +| `secret_key` | string | Required | - | Secret key used to generate the HMAC signature. | +| `name` | string | Optional | `access_key` | Name of the consumer. | + + +### Authorization Configuration (Non-essential) + +| Name | Data Type | Requirements | Default Value | Description | +|---------|------------------| ----------------------------------------- |---------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `allow` | array of string | Optional (**Non-instance-level configuration only**) | - | Can only be configured in fine-grained rules such as routes or domains. For requests that match the criteria, it configures the consumers allowed to access, enabling fine-grained permission control. | + + +## Configuration Examples + +### Global Authentication Configuration and Route-level Authorization +The following configuration enables HMAC Auth authentication and authorization for specific routes or domains of the gateway. **Note: The `access_key` field must be unique.** + + +#### Example 1: Basic Route and Domain Authorization Configuration +**Instance-level Plugin Configuration**: +```yaml +global_auth: false +consumers: +- name: consumer1 + access_key: consumer1-key + secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5 +- name: consumer2 + access_key: consumer2-key + secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +``` + +**Route-level Configuration** (Applicable to `route-a` and `route-b`): +```yaml +allow: +- consumer1 # Only consumer1 is allowed to access +``` + +**Domain-level Configuration** (Applicable to `*.example.com` and `test.com`): +```yaml +allow: +- consumer2 # Only consumer2 is allowed to access +``` + +**Configuration Description**: +- Route names (e.g., `route-a`, `route-b`) correspond to the names defined when creating gateway routes. Only `consumer1` is allowed to access when the route matches. +- Domain matching (e.g., `*.example.com`, `test.com`) is used to filter request domains. Only `consumer2` is allowed to access when the domain matches. +- Callers not in the `allow` list will be denied access. + + +**Request and Response Examples**: + +1. **Successful Verification Scenario** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \ +-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- Response: Returns a normal response from the backend service. +- Additional Info: After successful authentication, the request header `X-Mse-Consumer: consumer1` is automatically added and passed to the backend. + + +2. **Signature Verification Failure Due to Modified Request Method** +```shell +curl -X PUT 'http://localhost:8082/foo' \ # Changed from POST to PUT here +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \ +-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- Response: `401 Unauthorized` +- Error Message: `{"message":"client request can't be validated: Invalid signature"}` + + +3. **Caller Not in the Allow List** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer2-key",algorithm="hmac-sha256",headers="@request-target date",signature="5sqSbDX9b91dQsfQra2hpluM7O6/yhS7oLcKPQylyCo="' \ +-H 'Date:Sat, 30 Aug 2025 00:54:18 GMT' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- Response: `401 Unauthorized` +- Error Message: `{"message":"client request can't be validated: consumer 'consumer2' is not allowed"}` + + +4. **Expired Timestamp** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization: Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="gvIUwoYNiK57w6xX2g1Ntpk8lfgD7z+jgom434r5qwg="' \ +-H 'Date: Sat, 30 Aug 2025 00:40:21 GMT' \ # Expired timestamp +-H 'Content-Type: application/json' \ +-d '{}' +``` +- Response: `401 Unauthorized` +- Error Message: `{"message":"client request can't be validated: Clock skew exceeded"}` + + +#### Example 2: Configuration with Custom Signature Headers and Request Body Verification +**Instance-level Plugin Configuration**: +```yaml +global_auth: false +consumers: +- name: consumer1 + access_key: consumer1-key + secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5 +- name: consumer2 + access_key: consumer2-key + secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +signed_headers: # Custom request headers to be included in the signature +- X-Custom-Header-A +- X-Custom-Header-B +validate_request_body: true # Enable request body signature verification +``` + + +**Request and Response Examples**: + +1. **Successful Verification Scenario** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \ +-H 'Date:Sat, 30 Aug 2025 01:04:06 GMT' \ +-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # Request body digest +-H 'X-Custom-Header-A:test1' \ +-H 'X-Custom-Header-B:test2' \ +-H 'Content-Type: application/json' \ +-d '{}' +``` +- Response: Returns a normal response from the backend service. + + +2. **Missing Signature Header** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \ +-H 'Date:Sat, 30 Aug 2025 01:04:06 GMT' \ +-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ +-H 'X-Custom-Header-B:test2' \ # Missing X-Custom-Header-A +-H 'Content-Type: application/json' \ +-d '{}' +``` +- Response: `401 Unauthorized` +- Error Message: `{"message":"client request can't be validated: expected header \"X-Custom-Header-A\" missing in signing"}` + + +3. **Tampered Request Body** +```shell +curl -X POST 'http://localhost:8082/foo' \ +-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="dSbv6pdQOcgkN89TmSxiT8F9nypbPUqAR2E7ELL8K2s="' \ +-H 'Date:Sat, 30 Aug 2025 01:10:17 GMT' \ +-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # Mismatches the actual body +-H 'X-Custom-Header-A:test1' \ +-H 'X-Custom-Header-B:test2' \ +-H 'Content-Type: application/json' \ +-d '{"key":"value"}' # Tampered request body +``` +- Response: `401 Unauthorized` +- Error Message: `{"message":"client request can't be validated: Invalid digest"}` + + +### Enabling Global Authentication at the Gateway Instance Level +The following configuration enables HMAC Auth authentication at the gateway instance level. **All requests must pass authentication to access**: + +```yaml +global_auth: true # Enable global authentication +consumers: +- name: consumer1 + access_key: consumer1-key + secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5 +- name: consumer2 + access_key: consumer2-key + secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35 +``` + +**Description**: When `global_auth: true`, all requests accessing the gateway must carry valid authentication information. Unauthenticated requests will be directly rejected. \ No newline at end of file diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go b/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go index 60883c73c..ea09c0b9e 100644 --- a/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/config/config.go @@ -8,19 +8,15 @@ import ( "github.com/tidwall/gjson" ) -var ( - // RuleSet 插件是否至少在一个 domain 或 route 上生效 - RuleSet bool - // allowed_algorithms 配置中允许的算法 - validAlgorithms = map[string]bool{ - "hmac-sha1": true, - "hmac-sha256": true, - "hmac-sha512": true, - } -) +// validAlgorithms allowed_algorithms 配置中允许的算法 +var validAlgorithms = map[string]bool{ + "hmac-sha1": true, + "hmac-sha256": true, + "hmac-sha512": true, +} type HmacAuthConfig struct { - Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"` + Consumers []Consumer `json:"consumers" yaml:"consumers"` GlobalAuth *bool `json:"global_auth,omitempty" yaml:"global_auth,omitempty"` AllowedAlgorithms []string `json:"allowed_algorithms,omitempty" yaml:"allowed_algorithms,omitempty"` ClockSkew int `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` @@ -28,7 +24,9 @@ type HmacAuthConfig struct { ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"` HideCredentials bool `json:"hide_credentials,omitempty" yaml:"hide_credentials,omitempty"` AnonymousConsumer string `json:"anonymous_consumer,omitempty" yaml:"anonymous_consumer,omitempty"` - Allow []string `json:"allow" yaml:"allow"` + Allow []string `json:"allow,omitempty" yaml:"allow,omitempty"` + // RuleSet 插件是否至少在一个 domain 或 route 上生效 + RuleSet bool `json:"-" yaml:"-"` } type Consumer struct { @@ -39,7 +37,7 @@ type Consumer struct { func ParseGlobalConfig(jsonData gjson.Result, global *HmacAuthConfig) error { log.Debug("global config") - RuleSet = false + global.RuleSet = false // 处理 consumers 配置 consumers := jsonData.Get("consumers") @@ -170,7 +168,7 @@ func ParseOverrideRuleConfig(jsonData gjson.Result, global HmacAuthConfig, confi } } - RuleSet = true + config.RuleSet = true if configBytes, err := json.Marshal(config); err == nil { log.Debugf("config: %s", string(configBytes)) } diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod b/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod index 9762d4870..ae53bf61a 100644 --- a/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/go.mod @@ -5,8 +5,8 @@ go 1.24.1 toolchain go1.24.4 require ( - github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 - github.com/higress-group/wasm-go v1.0.1 + github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 + github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.18.0 ) @@ -15,8 +15,10 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tetratelabs/wazero v1.7.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/resp v0.1.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum b/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum index 35e747e20..b055378c0 100644 --- a/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/go.sum @@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg= -github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA= -github.com/higress-group/wasm-go v1.0.1 h1:T1m++qTEANp8+jwE0sxltwtaTKmrHCkLOp1m9N+YeqY= -github.com/higress-group/wasm-go v1.0.1/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA= +github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o= +github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc= +github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/main.go b/plugins/wasm-go/extensions/hmac-auth-apisix/main.go index b0d2005fd..cee9f66e3 100644 --- a/plugins/wasm-go/extensions/hmac-auth-apisix/main.go +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/main.go @@ -1,3 +1,17 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package main import ( @@ -55,7 +69,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.HmacAuthConfig) ty globalAuthNoSet = cfg.GlobalAuth == nil globalAuthSetTrue = !globalAuthNoSet && *cfg.GlobalAuth globalAuthSetFalse = !globalAuthNoSet && !*cfg.GlobalAuth - ruleSet = config.RuleSet + ruleSet = cfg.RuleSet ) // 不需要认证而直接放行的情况: @@ -155,9 +169,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, cfg config.HmacAuthConfig, body } // 计算请求体的 SHA-256 摘要 - hash := sha256.Sum256(body) - encodedDigest := base64.StdEncoding.EncodeToString(hash[:]) - digestCreated := "SHA-256=" + encodedDigest + digestCreated := calculateBodyDigest(body) // 比较请求头中的 Digest 和服务端计算的摘要 if digestCreated != digestHeaderVal { @@ -169,7 +181,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, cfg config.HmacAuthConfig, body return types.ActionContinue } -// HmacParams 存储从 Authorization 头解析出的 HMAC 参数 +// HmacParams 存储从 Authorization 头中提取 HMAC 参数和消费者信息 type HmacParams struct { KeyId string Algorithm string @@ -180,7 +192,7 @@ type HmacParams struct { // retrieveHmacFieldsAndConsumer 从 Authorization 头中提取 HMAC 参数和消费者信息 func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, error) { - hmacParams := &HmacParams{} + params := &HmacParams{} // 获取 Authorization 头 authString, err := proxywasm.GetHttpRequestHeader(authorizationHeader) @@ -206,26 +218,26 @@ func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, erro switch key { case "keyId": - hmacParams.KeyId = value + params.KeyId = value case "algorithm": - hmacParams.Algorithm = value + params.Algorithm = value case "signature": - hmacParams.Signature = value + params.Signature = value case "headers": // 分割 headers 字段 if value != "" { - hmacParams.Headers = strings.Split(value, " ") + params.Headers = strings.Split(value, " ") } } } } // 验证必要字段 - if hmacParams.KeyId == "" || hmacParams.Signature == "" { + if params.KeyId == "" || params.Signature == "" { return nil, fmt.Errorf("keyId or signature missing") } - if hmacParams.Algorithm == "" { + if params.Algorithm == "" { return nil, fmt.Errorf("algorithm missing") } @@ -233,7 +245,7 @@ func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, erro consumerName := "" found := false for _, consumer := range cfg.Consumers { - if consumer.AccessKey == hmacParams.KeyId { + if consumer.AccessKey == params.KeyId { consumerName = consumer.Name found = true break @@ -244,8 +256,8 @@ func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, erro return nil, fmt.Errorf("Invalid keyId") } - hmacParams.ConsumerName = consumerName - return hmacParams, nil + params.ConsumerName = consumerName + return params, nil } // validateClockSkew 检查时间偏差 @@ -291,10 +303,7 @@ func validateSignature(hmacParams *HmacParams, cfg config.HmacAuthConfig) error } // 生成 HMAC 签名 - signingString, err := generateSigningString(hmacParams) - if err != nil { - return fmt.Errorf("Failed to generate signing string") - } + signingString := generateSigningString(hmacParams) expectedSignature, err := generateHmacSignature(secretKey, hmacParams.Algorithm, signingString) if err != nil { return err @@ -311,7 +320,7 @@ func validateSignature(hmacParams *HmacParams, cfg config.HmacAuthConfig) error } // generateSigningString 生成签名字符串 -func generateSigningString(hmacParams *HmacParams) (string, error) { +func generateSigningString(hmacParams *HmacParams) string { var signingStringItems []string signingStringItems = append(signingStringItems, hmacParams.KeyId) @@ -341,7 +350,7 @@ func generateSigningString(hmacParams *HmacParams) (string, error) { } signingString := strings.Join(signingStringItems, "\n") + "\n" - return signingString, nil + return signingString } // generateHmacSignature 生成 HMAC 签名 @@ -364,6 +373,13 @@ func generateHmacSignature(secretKey, algorithm, message string) (string, error) return base64.StdEncoding.EncodeToString(signature), nil } +// calculateBodyDigest 计算请求体的 SHA-256 摘要 +func calculateBodyDigest(body []byte) string { + hash := sha256.Sum256(body) + encodedDigest := base64.StdEncoding.EncodeToString(hash[:]) + return "SHA-256=" + encodedDigest +} + func sendUnauthorizedResponse(message string) types.Action { errorResponse := fmt.Sprintf(errorResponseTemplate, message) proxywasm.SendHttpResponse(401, nil, []byte(errorResponse), -1) diff --git a/plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go b/plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go new file mode 100644 index 000000000..a0b1b822f --- /dev/null +++ b/plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go @@ -0,0 +1,1279 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "fmt" + "hash" + "strings" + "testing" + "time" + + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" + "github.com/higress-group/wasm-go/pkg/test" + "github.com/stretchr/testify/require" +) + +// 生成有效的 HMAC 签名 - 与插件中的 generateSigningString 方法保持一致 +func generateValidSignature(accessKey, secretKey, algorithm, requestMethod, requestURI string, headers []string, headerValues map[string]string) string { + // 构造签名字符串,严格按照插件中的 generateSigningString 方法实现 + var signingStringItems []string + // 第一行是 keyId + signingStringItems = append(signingStringItems, accessKey) + + if len(headers) > 0 { + for _, h := range headers { + if h == "@request-target" { + // 注意:这里使用原始请求方法和URI + requestTarget := fmt.Sprintf("%s %s", requestMethod, requestURI) + signingStringItems = append(signingStringItems, requestTarget) + } else { + if value, ok := headerValues[h]; ok { + signingStringItems = append(signingStringItems, fmt.Sprintf("%s: %s", h, value)) + } + } + } + } + + // 签名字符串需要以换行符结尾 + signingString := strings.Join(signingStringItems, "\n") + "\n" + + // 生成 HMAC 签名 + var mac hash.Hash + switch algorithm { + case "hmac-sha1": + mac = hmac.New(sha1.New, []byte(secretKey)) + case "hmac-sha256": + mac = hmac.New(sha256.New, []byte(secretKey)) + case "hmac-sha512": + mac = hmac.New(sha512.New, []byte(secretKey)) + default: + mac = hmac.New(sha256.New, []byte(secretKey)) + } + + mac.Write([]byte(signingString)) + signature := mac.Sum(nil) + return base64.StdEncoding.EncodeToString(signature) +} + +// 生成 Authorization 头 +func generateAuthorizationHeader(accessKey, secretKey, algorithm, requestMethod, requestURI string, headers []string, headerValues map[string]string) string { + signature := generateValidSignature(accessKey, secretKey, algorithm, requestMethod, requestURI, headers, headerValues) + header := fmt.Sprintf(`Signature keyId="%s",algorithm="%s",signature="%s"`, accessKey, algorithm, signature) + if len(headers) > 0 { + header += fmt.Sprintf(`,headers="%s"`, strings.Join(headers, " ")) + } + return header +} + +// 辅助函数:生成有效的认证头 +func generateValidAuthHeaderWithDate(dateStr, method, path string) string { + headerValues := map[string]string{ + "date": dateStr, + } + return generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", method, path, []string{"@request-target", "date"}, headerValues) +} + +// 通用测试配置生成函数 +func createConfig(consumers []map[string]interface{}, extra map[string]interface{}) json.RawMessage { + config := map[string]interface{}{ + "consumers": consumers, + // 设置 clock_skew 为 0 来跳过时钟偏差校验 + "clock_skew": 0, + } + + for k, v := range extra { + config[k] = v + } + + data, _ := json.Marshal(config) + return data +} + +func TestParseGlobalConfig(t *testing.T) { + test.RunGoTest(t, func(t *testing.T) { + tests := []struct { + name string + config json.RawMessage + expected bool + }{ + { + "basic global config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{}, + ), + true, + }, + { + "global auth true config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + ), + true, + }, + { + "global auth false config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": false, + }, + ), + true, + }, + { + "clock skew config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "clock_skew": 600, + }, + ), + true, + }, + { + "algorithm config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "allowed_algorithms": []string{"hmac-sha256"}, + }, + ), + true, + }, + { + "signed headers config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "signed_headers": []string{"host", "date"}, + }, + ), + true, + }, + { + "anonymous consumer config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "anonymous_consumer": "anonymous", + }, + ), + true, + }, + { + "invalid config - missing consumers", + createConfig( + []map[string]interface{}{}, + map[string]interface{}{ + "global_auth": false, + }, + ), + false, + }, + { + "invalid config - empty consumers", + createConfig( + []map[string]interface{}{}, + map[string]interface{}{ + "consumers": []map[string]interface{}{}, + }, + ), + false, + }, + { + "invalid config - missing access_key", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{}, + ), + false, + }, + { + "invalid config - missing secret_key", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + }, + }, + map[string]interface{}{}, + ), + false, + }, + { + "invalid config - duplicate access_key", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak1", + "secret_key": "sk2", + }, + }, + map[string]interface{}{}, + ), + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + host, status := test.NewTestHost(tt.config) + defer host.Reset() + + if tt.expected { + require.Equal(t, types.OnPluginStartStatusOK, status) + } else { + require.Equal(t, types.OnPluginStartStatusFailed, status) + } + }) + } + }) +} + +func TestParseOverrideRuleConfig(t *testing.T) { + test.RunGoTest(t, func(t *testing.T) { + tests := []struct { + name string + config json.RawMessage + expected bool + }{ + { + "route auth config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "allow": []string{"consumer1"}, + }, + ), + true, + }, + { + "domain auth config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "allow": []string{"consumer2"}, + }, + ), + true, + }, + { + "invalid config - empty allow list", + createConfig( + []map[string]interface{}{}, + map[string]interface{}{ + "allow": []string{}, + }, + ), + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + host, status := test.NewTestHost(tt.config) + defer host.Reset() + + if tt.expected { + require.Equal(t, types.OnPluginStartStatusOK, status) + } else { + require.Equal(t, types.OnPluginStartStatusFailed, status) + } + }) + } + }) +} + +func TestParseRuleConfig(t *testing.T) { + test.RunGoTest(t, func(t *testing.T) { + tests := []struct { + name string + config json.RawMessage + expected bool + }{ + { + "route level config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_route_": []string{"route-a", "route-b"}, + "allow": []string{"consumer1"}, + }, + { + "_match_route_": []string{"route-c"}, + "allow": []string{"consumer2"}, + }, + }, + }, + ), + true, + }, + { + "domain level config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_domain_": []string{"*.example.com", "test.com"}, + "allow": []string{"consumer2"}, + }, + { + "_match_domain_": []string{"api.example.com"}, + "allow": []string{"consumer1"}, + }, + }, + }, + ), + true, + }, + { + "service level config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_service_": []string{"service-a:8080", "service-b"}, + "allow": []string{"consumer1"}, + }, + { + "_match_service_": []string{"service-b:9090"}, + "allow": []string{"consumer2"}, + }, + }, + }, + ), + true, + }, + { + "route prefix level config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_route_prefix_": []string{"api-", "web-"}, + "allow": []string{"consumer1"}, + }, + { + "_match_route_prefix_": []string{"admin-", "internal-"}, + "allow": []string{"consumer2"}, + }, + }, + }, + ), + true, + }, + { + "route and service level config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_route_": []string{"route-a"}, + "_match_service_": []string{"service-a:8080"}, + "allow": []string{"consumer1"}, + }, + { + "_match_route_": []string{"route-b"}, + "_match_service_": []string{"service-b:9090"}, + "allow": []string{"consumer2"}, + }, + }, + }, + ), + true, + }, + { + "mixed level config", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + { + "name": "consumer3", + "access_key": "ak3", + "secret_key": "sk3", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_route_": []string{"api-route"}, + "allow": []string{"consumer1"}, + }, + { + "_match_domain_": []string{"*.example.com"}, + "allow": []string{"consumer2"}, + }, + { + "_match_service_": []string{"internal-service:8080"}, + "allow": []string{"consumer3"}, + }, + { + "_match_route_prefix_": []string{"web-"}, + "allow": []string{"consumer1", "consumer2"}, + }, + }, + }, + ), + true, + }, + { + "invalid rule config - missing match conditions", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "allow": []string{"consumer1"}, + // 缺少匹配条件 + }, + }, + }, + ), + false, + }, + { + "invalid rule config - empty match conditions", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "_rules_": []map[string]interface{}{ + { + "_match_route_": []string{}, + "allow": []string{"consumer1"}, + }, + }, + }, + ), + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + host, status := test.NewTestHost(tt.config) + defer host.Reset() + + if tt.expected { + require.Equal(t, types.OnPluginStartStatusOK, status) + } else { + require.Equal(t, types.OnPluginStartStatusFailed, status) + } + }) + } + }) +} + +func TestOnHttpRequestHeaders(t *testing.T) { + test.RunTest(t, func(t *testing.T) { + tests := []struct { + name string + config json.RawMessage + headers [][2]string + expectContinue bool + expectError string + }{ + { + "missing authorization header", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{}, + ), + [][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}}, + true, + `{"message":"client request can't be validated: missing Authorization header"}`, + }, + { + "empty authorization header", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{}, + ), + [][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}, {"authorization", ""}}, + true, + `{"message":"client request can't be validated: missing Authorization header"}`, + }, + { + "invalid authorization format - missing signature prefix", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + ), + [][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}, {"authorization", "Bearer token123"}}, + true, + `{"message":"client request can't be validated: Authorization header does not start with 'Signature '"}`, + }, + { + "invalid authorization format - invalid signature format", + createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + ), + [][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}, {"authorization", "Signature invalid-format"}}, + true, + `{"message":"client request can't be validated: keyId or signature missing"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + host, status := test.NewTestHost(tt.config) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + action := host.CallOnHttpRequestHeaders(tt.headers) + require.Equal(t, types.ActionContinue, action) + + localResponse := host.GetLocalResponse() + require.NotNil(t, localResponse) + require.Equal(t, uint32(401), localResponse.StatusCode) + require.Equal(t, tt.expectError, string(localResponse.Data)) + + host.CompleteHttp() + }) + } + + // 测试有效的凭证情况 + t.Run("valid credentials - global auth true, no allow config", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + headerValues := map[string]string{ + "date": fixedTime, + } + + authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues) + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + {"authorization", authHeader}, + {"date", fixedTime}, + }) + + require.Equal(t, types.ActionContinue, action) + require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted") + + host.CompleteHttp() + }) + + // 测试无效的凭证(未配置的 access_key) + t.Run("invalid credential - not configured access_key", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + headerValues := map[string]string{ + "date": fixedTime, + } + + authHeader := generateAuthorizationHeader("unknown_ak", "sk", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues) + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + {"authorization", authHeader}, + {"date", fixedTime}, + }) + + require.Equal(t, types.ActionContinue, action) + + localResponse := host.GetLocalResponse() + require.NotNil(t, localResponse, "Invalid credentials should be rejected") + require.Equal(t, uint32(401), localResponse.StatusCode) + require.Equal(t, `{"message":"client request can't be validated: Invalid keyId"}`, string(localResponse.Data)) + + host.CompleteHttp() + }) + + // 测试无效的签名(错误的签名) + t.Run("invalid signature - wrong signature", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + headerValues := map[string]string{ + "date": fixedTime, + } + + authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues) + // 故意修改签名 + authHeader = strings.Replace(authHeader, "signature=", "signature=wrong", 1) + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + {"authorization", authHeader}, + {"date", fixedTime}, + }) + + require.Equal(t, types.ActionContinue, action) + + localResponse := host.GetLocalResponse() + require.NotNil(t, localResponse, "Invalid signature should be rejected") + require.Equal(t, uint32(401), localResponse.StatusCode) + require.Equal(t, `{"message":"client request can't be validated: keyId or signature missing"}`, string(localResponse.Data)) + + host.CompleteHttp() + }) + + // 测试有效的凭证(全局认证关闭,有 allow 配置) + t.Run("valid credentials - global auth false, with allow config", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "allow": []string{"consumer1"}, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + headerValues := map[string]string{ + "date": fixedTime, + } + + authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues) + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + {"authorization", authHeader}, + {"date", fixedTime}, + }) + + require.Equal(t, types.ActionContinue, action) + require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted") + + host.CompleteHttp() + }) + + // 测试有效的凭证但不在 allow 列表中的情况 + t.Run("valid credentials but not in allow list", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + { + "name": "consumer2", + "access_key": "ak2", + "secret_key": "sk2", + }, + }, + map[string]interface{}{ + "allow": []string{"consumer1"}, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + headerValues := map[string]string{ + "date": fixedTime, + } + + authHeader := generateAuthorizationHeader("ak2", "sk2", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues) + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + {"authorization", authHeader}, + {"date", fixedTime}, + }) + + require.Equal(t, types.ActionContinue, action) + require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted when not in allow list") + + host.CompleteHttp() + }) + + // 测试匿名消费者配置 + t.Run("anonymous consumer config", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "anonymous_consumer": "anonymous", + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + }) + + require.Equal(t, types.ActionContinue, action) + require.Nil(t, host.GetLocalResponse(), "Request without credentials should be accepted with anonymous consumer") + + host.CompleteHttp() + }) + }) +} + +func TestCompleteFlow(t *testing.T) { + test.RunTest(t, func(t *testing.T) { + t.Run("complete hmac auth flow", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "global_auth": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + // 1. 测试缺少认证信息的情况 + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + }) + + require.Equal(t, types.ActionContinue, action) + + localResponse := host.GetLocalResponse() + require.NotNil(t, localResponse, "Request without credentials should be rejected") + require.Equal(t, uint32(401), localResponse.StatusCode) + require.Equal(t, `{"message":"client request can't be validated: missing Authorization header"}`, string(localResponse.Data)) + + host.CompleteHttp() + + // 2. 测试有效认证的情况 + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + headerValues := map[string]string{ + "date": fixedTime, + } + + authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues) + action = host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "GET"}, + {"authorization", authHeader}, + {"date", fixedTime}, + }) + + require.Equal(t, types.ActionContinue, action) + + requestHeaders := host.GetRequestHeaders() + require.True(t, test.HasHeaderWithValue(requestHeaders, "X-Mse-Consumer", "consumer1")) + + require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted") + + host.CompleteHttp() + }) + }) +} + +func TestOnHttpRequestBody(t *testing.T) { + test.RunTest(t, func(t *testing.T) { + // 测试有效的请求体和摘要 + t.Run("valid request body with correct digest", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "validate_request_body": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + // 准备空请求体的摘要 + validBody := []byte(`{"name": "test", "value": 123}`) + correctDigest := calculateBodyDigest(validBody) + + // 使用固定的时间值确保一致性 + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + + // 生成与请求头一致的签名 + authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test") + + // 先处理请求头 + host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "POST"}, + {"authorization", authHeader}, + {"date", fixedTime}, + {"digest", correctDigest}, // 使用正确的摘要 + {"content-type", "application/json"}, + }) + + // 测试有效的请求体 + action := host.CallOnHttpRequestBody(validBody) + require.Equal(t, types.ActionContinue, action, "Valid body with correct digest should be accepted") + require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue") + + localResponse := host.GetLocalResponse() + require.Nil(t, localResponse, "Valid body with correct digest should not be rejected") + + host.CompleteHttp() + }) + + // 测试无效的摘要 + t.Run("invalid digest", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "validate_request_body": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + // 使用固定的时间值确保一致性 + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + + // 生成与请求头一致的签名 + authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test") + + // 先调用头部处理 + host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "POST"}, + {"authorization", authHeader}, + {"date", fixedTime}, + {"digest", "SHA-256=invalid-digest"}, + {"content-type", "application/json"}, + }) + + // 测试请求体但摘要不匹配 + body := []byte(`{"name": "test", "value": 123}`) + action := host.CallOnHttpRequestBody(body) + + require.Equal(t, types.ActionContinue, action, "Request with invalid digest should be rejected") + require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue") + + localResponse := host.GetLocalResponse() + require.NotNil(t, localResponse, "Request with invalid digest should be rejected") + require.Equal(t, uint32(401), localResponse.StatusCode) + require.Contains(t, string(localResponse.Data), "Invalid digest", "Error message should indicate invalid digest") + + host.CompleteHttp() + }) + + // 测试缺少摘要头 + t.Run("missing digest header", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "validate_request_body": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + // 使用固定的时间值确保一致性 + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + + // 生成与请求头一致的签名 + authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test") + + // 先调用头部处理,但不设置digest头 + host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "POST"}, + {"authorization", authHeader}, + {"date", fixedTime}, + // 故意不设置digest头 + {"content-type", "application/json"}, + }) + + // 测试请求体但缺少摘要头 + body := []byte(`{"name": "test", "value": 123}`) + action := host.CallOnHttpRequestBody(body) + + require.Equal(t, types.ActionContinue, action, "Request without digest header should be rejected") + require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue") + + localResponse := host.GetLocalResponse() + require.NotNil(t, localResponse, "Request without digest header should be rejected") + require.Equal(t, uint32(401), localResponse.StatusCode) + require.Contains(t, string(localResponse.Data), "Invalid digest", "Error message should indicate invalid digest") + + host.CompleteHttp() + }) + + // 测试未启用请求体验证 + t.Run("body validation disabled", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "validate_request_body": false, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + // 使用固定的时间值确保一致性 + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + + // 生成与请求头一致的签名 + authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test") + + // 先调用头部处理 + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "POST"}, + {"authorization", authHeader}, + {"date", fixedTime}, + {"content-type", "application/json"}, + }) + // 由于禁用了请求体验证,应该直接继续而不等待请求体 + require.Equal(t, types.ActionContinue, action, "Should continue immediately when validate_request_body is false") + + host.CompleteHttp() + }) + + // 测试空请求体 + t.Run("empty request body", func(t *testing.T) { + host, status := test.NewTestHost(createConfig( + []map[string]interface{}{ + { + "name": "consumer1", + "access_key": "ak1", + "secret_key": "sk1", + }, + }, + map[string]interface{}{ + "validate_request_body": true, + }, + )) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + // 使用固定的时间值确保一致性 + fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + + // 准备空请求体的摘要 + emptyBody := []byte("") + correctDigest := calculateBodyDigest(emptyBody) + + // 生成与请求头一致的签名 + authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test") + + // 先调用头部处理 + host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/api/test"}, + {":method", "POST"}, + {"authorization", authHeader}, + {"date", fixedTime}, + {"digest", correctDigest}, // 空字符串的SHA-256摘要 + {"content-type", "application/json"}, + }) + + // 测试空请求体 + action := host.CallOnHttpRequestBody(emptyBody) + + require.Equal(t, types.ActionContinue, action, "Empty body with correct digest should be accepted") + require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue") + + localResponse := host.GetLocalResponse() + require.Nil(t, localResponse, "Empty body with correct digest should not be rejected") + + host.CompleteHttp() + }) + }) +} diff --git a/plugins/wasm-go/extensions/replay-protection/config/config.go b/plugins/wasm-go/extensions/replay-protection/config/config.go index 8f88f41ef..956ef8a1b 100644 --- a/plugins/wasm-go/extensions/replay-protection/config/config.go +++ b/plugins/wasm-go/extensions/replay-protection/config/config.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/higress-group/wasm-go/pkg/log" "github.com/higress-group/wasm-go/pkg/wrapper" "github.com/tidwall/gjson" ) @@ -26,7 +25,7 @@ type RedisConfig struct { KeyPrefix string } -func ParseConfig(json gjson.Result, config *ReplayProtectionConfig, log log.Log) error { +func ParseConfig(json gjson.Result, config *ReplayProtectionConfig) error { // Parse Redis configuration redisConfig := json.Get("redis") if !redisConfig.Exists() { diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index 42c3f5153..d8b80833c 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -18,12 +18,12 @@ func main() {} func init() { wrapper.SetCtx( "replay-protection", - wrapper.ParseConfigBy(config.ParseConfig), - wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ParseConfig(config.ParseConfig), + wrapper.ProcessRequestHeaders(onHttpRequestHeaders), ) } -func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.ReplayProtectionConfig, log log.Log) types.Action { +func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.ReplayProtectionConfig) types.Action { nonce, _ := proxywasm.GetHttpRequestHeader(cfg.NonceHeader) if cfg.ForceNonce && nonce == "" { // In force mode, reject the request if a required header is missing.