diff --git a/plugins/wasm-go/extensions/request-validation/README.md b/plugins/wasm-go/extensions/request-validation/README.md new file mode 100644 index 000000000..25761ca6f --- /dev/null +++ b/plugins/wasm-go/extensions/request-validation/README.md @@ -0,0 +1,146 @@ +# 功能说明 +`request-validation`插件用于提前验证向上游服务转发的请求。该插件使用`JSON Schema`机制进行数据验证,可以验证请求的body及header数据。 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- |-----| -------- | +|header_schema|object|选填| - |配置用于验证请求header的JSON Schema| +|body_schema|object|选填| - |配置用于验证请求body的JSON Schema| +|rejected_code|number|选填| 403 |配置请求被拒绝时返回的HTTP状态码| +|rejected_msg|string|选填| - |配置请求被拒绝时返回的HTTP应答Body| +|enable_swagger|bool|选填| false |配置是否开启swagger文档验证| +|enable_oas3|bool|选填| false |配置是否开启OAS3文档验证| + +**校验规则对header和body是一样的,下面以body为例说明** + +# 配置示例 + +## 枚举(Enum)验证 +```yaml +body_schema: + type: object + required: + - enum_payload + properties: + enum_payload: + type: string + enum: + - "enum_string_1" + - "enum_string_2" + default: "enum_string_1" +``` + +## 布尔(Boolean)验证 +```yaml +body_schema: + type: object + required: + - boolean_payload + properties: + boolean_payload: + type: boolean + default: true +``` + +## 数字范围(Number or Integer)验证 +```yaml +body_schema: + type: object + required: + - integer_payload + properties: + integer_payload: + type: integer + minimum: 1 + maximum: 10 +``` + +## 字符串长度(String)验证 +```yaml +body_schema: + type: object + required: + - string_payload + properties: + string_payload: + type: string + minLength: 1 + maxLength: 10 +``` + +## 正则表达式(Regex)验证 +```yaml +body_schema: + type: object + required: + - regex_payload + properties: + regex_payload: + type: string + minLength: 1 + maxLength: 10 + pattern: "^[a-zA-Z0-9_]+$" +``` + +## 数组(Array)验证 +```yaml +body_schema: + type: object + required: + - array_payload + properties: + array_payload: + type: array + minItems: 1 + items: + type: integer + minimum: 1 + maximum: 10 + uniqueItems: true + default: [1, 2, 3] +``` + +## 多字段组合(Combined)验证 +```yaml +body_schema: + type: object + required: + - boolean_payload + - array_payload + - regex_payload + properties: + boolean_payload: + type: boolean + array_payload: + type: array + minItems: 1 + items: + type: integer + minimum: 1 + maximum: 10 + uniqueItems: true + default: [1, 2, 3] + regex_payload: + type: string + minLength: 1 + maxLength: 10 + pattern: "^[a-zA-Z0-9_]+$" +``` + +## 自定义拒绝信息 +```yaml +body_schema: + type: object + required: + - boolean_payload + properties: + boolean_payload: + type: boolean +rejected_code: 403 +rejected_msg: "请求被拒绝" +``` + +# 本地调试 + +参考[使用 GO 语言开发 WASM 插件](https://higress.io/zh-cn/docs/user/wasm-go#%E4%B8%89%E6%9C%AC%E5%9C%B0%E8%B0%83%E8%AF%95) \ No newline at end of file diff --git a/plugins/wasm-go/extensions/request-validation/VERSION b/plugins/wasm-go/extensions/request-validation/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-go/extensions/request-validation/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/request-validation/go.mod b/plugins/wasm-go/extensions/request-validation/go.mod new file mode 100644 index 000000000..c72d3250f --- /dev/null +++ b/plugins/wasm-go/extensions/request-validation/go.mod @@ -0,0 +1,18 @@ +module github.com/alibaba/higress/plugins/wasm-go/extensions/request-validation + +go 1.19 + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.3.1 + github.com/santhosh-tekuri/jsonschema v1.2.4 + github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 + github.com/tidwall/gjson v1.17.0 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect + github.com/magefile/mage v1.14.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/plugins/wasm-go/extensions/request-validation/go.sum b/plugins/wasm-go/extensions/request-validation/go.sum new file mode 100644 index 000000000..f7eeb4daa --- /dev/null +++ b/plugins/wasm-go/extensions/request-validation/go.sum @@ -0,0 +1,22 @@ +github.com/alibaba/higress/plugins/wasm-go v1.3.1 h1:d+t4W2NyqmqUz6DPZENflODfkLgdVlTfyso+nq0fSkg= +github.com/alibaba/higress/plugins/wasm-go v1.3.1/go.mod h1:WZ/68vwe8qWhusa6C4/gMwUqas0jvHWSOa1bp8iK8F4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= +github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M= +github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugins/wasm-go/extensions/request-validation/main.go b/plugins/wasm-go/extensions/request-validation/main.go new file mode 100644 index 000000000..975861753 --- /dev/null +++ b/plugins/wasm-go/extensions/request-validation/main.go @@ -0,0 +1,199 @@ +// Copyright (c) 2022 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 ( + "encoding/json" + "fmt" + "strings" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/santhosh-tekuri/jsonschema" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" +) + +const ( + defaultHeaderSchema = "header" + defaultBodySchema = "body" + defaultRejectedCode = 403 +) + +func main() { + wrapper.SetCtx( + "request-validation", + wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ProcessRequestBodyBy(onHttpRequestBody), + wrapper.ParseConfigBy(parseConfig), + ) +} + +// Config is the config for request validation. +type Config struct { + // compiler is the compiler for json schema. + compiler *jsonschema.Compiler + // rejectedCode is the code for rejected request. + rejectedCode uint32 + // rejectedMsg is the message for rejected request. + rejectedMsg string + // draft is the draft version of json schema. + draft *jsonschema.Draft + // enableBodySchema is the flag for enable body schema. + enableBodySchema bool + // enableHeaderSchema is the flag for enable header schema. + enableHeaderSchema bool +} + +func parseConfig(result gjson.Result, config *Config, log wrapper.Log) error { + headerSchema := result.Get("header_schema").String() + bodySchema := result.Get("body_schema").String() + enableSwagger := result.Get("enable_swagger").Bool() + enableOas3 := result.Get("enable_oas3").Bool() + code := result.Get("rejected_code").Int() + msg := result.Get("rejected_msg").String() + + // set config default value + config.enableBodySchema = false + config.enableHeaderSchema = false + + // check enable_swagger and enable_oas3 + if enableSwagger && enableOas3 { + return fmt.Errorf("enable_swagger and enable_oas3 can not be true at the same time") + } + + // set draft version + if enableSwagger { + config.draft = jsonschema.Draft4 + } + if enableOas3 { + config.draft = jsonschema.Draft7 + } + if !enableSwagger && !enableOas3 { + config.draft = jsonschema.Draft7 + } + + // create compiler + compiler := jsonschema.NewCompiler() + compiler.Draft = config.draft + config.compiler = compiler + + // add header schema to compiler + if headerSchema != "" { + err := config.compiler.AddResource(defaultHeaderSchema, strings.NewReader(headerSchema)) + if err != nil { + return err + } + config.enableHeaderSchema = true + } + + // add body schema to compiler + if bodySchema != "" { + err := config.compiler.AddResource(defaultBodySchema, strings.NewReader(bodySchema)) + if err != nil { + return err + } + config.enableBodySchema = true + } + + // check rejected_code is valid + if code != 0 && code > 100 && code < 600 { + config.rejectedCode = uint32(code) + } else { + config.rejectedCode = defaultRejectedCode + } + config.rejectedMsg = msg + + return nil +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, config Config, log wrapper.Log) types.Action { + if !config.enableHeaderSchema { + return types.ActionContinue + } + + // get headers + headers, err := proxywasm.GetHttpRequestHeaders() + if err != nil { + log.Errorf("get request headers failed: %v", err) + return types.ActionContinue + } + + // covert to schema + schema := make(map[string]interface{}) + for _, header := range headers { + schema[header[0]] = header[1] + } + + // convert to json string + schemaBytes, err := json.Marshal(schema) + if err != nil { + log.Errorf("marshal schema failed: %v", err) + return types.ActionContinue + } + + // validate + document := strings.NewReader(string(schemaBytes)) + compile, err := config.compiler.Compile(defaultHeaderSchema) + if err != nil { + log.Errorf("compile schema failed: %v", err) + return types.ActionContinue + } + err = compile.Validate(document) + if err != nil { + log.Errorf("validate request headers failed: %v", err) + proxywasm.SendHttpResponse(config.rejectedCode, nil, []byte(config.rejectedMsg), -1) + return types.ActionPause + } + + return types.ActionContinue +} + +func onHttpRequestBody(ctx wrapper.HttpContext, config Config, body []byte, log wrapper.Log) types.Action { + if !config.enableBodySchema { + return types.ActionContinue + } + + // covert to schema + schema := make(map[string]interface{}) + err := json.Unmarshal(body, &schema) + if err != nil { + log.Errorf("unmarshal body failed: %v", err) + return types.ActionContinue + } + + // convert to json string + schemaBytes, err := json.Marshal(schema) + if err != nil { + log.Errorf("marshal schema failed: %v", err) + return types.ActionContinue + } + + // validate + document := strings.NewReader(string(schemaBytes)) + compile, err := config.compiler.Compile(defaultBodySchema) + if err != nil { + log.Errorf("compile schema failed: %v", err) + return types.ActionContinue + } + err = compile.Validate(document) + if err != nil { + log.Errorf("validate request body failed: %v", err) + proxywasm.SendHttpResponse(config.rejectedCode, nil, []byte(config.rejectedMsg), -1) + return types.ActionPause + } + + return types.ActionContinue +} diff --git a/test/e2e/conformance/tests/go-wasm-request-validation.go b/test/e2e/conformance/tests/go-wasm-request-validation.go new file mode 100644 index 000000000..4095bce87 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-request-validation.go @@ -0,0 +1,412 @@ +// Copyright (c) 2022 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 tests + +import ( + "testing" + + "github.com/alibaba/higress/test/e2e/conformance/utils/http" + "github.com/alibaba/higress/test/e2e/conformance/utils/suite" +) + +func init() { + Register(WasmPluginsRequestValidation) +} + +var WasmPluginsRequestValidation = suite.ConformanceTest{ + ShortName: "WasmPluginsRequestValidation", + Description: "The Ingress in the higress-conformance-infra namespace test the request-validation wasmplugins.", + Manifests: []string{"tests/go-wasm-request-validation.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testCases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "request validation pass", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "header lack of require parameter", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body lack of require parameter", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body enum payload not in enum list", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_3", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body bool payload not bool type", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": "string", + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body integer payload not in range", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 70000, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body string payload length not in range", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "a", + "regex_payload": "abc123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body regex payload not match regex pattern", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc@123", + "array_payload": [200, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body array payload not in array range", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [150, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body array payload not unique array items", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [302, 302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "body array payload length not in range", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{ + "enum_payload": "enum_string_1", + }, + Body: []byte(` + { + "enum_payload": "enum_string_1", + "bool_payload": true, + "integer_payload": 100, + "string_payload": "abc", + "regex_payload": "abc123", + "array_payload": [302] + } + `), + ContentType: http.ContentTypeApplicationJson, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + Body: []byte(`customize reject message`), + }, + }, + }, + } + + t.Run("WasmPlugins request-validation", func(t *testing.T) { + for _, testcase := range testCases { + t.Logf("Running test case: %s", testcase.Meta.TestCaseName) + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/go-wasm-request-validation.yaml b/test/e2e/conformance/tests/go-wasm-request-validation.yaml new file mode 100644 index 000000000..26ea47839 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-request-validation.yaml @@ -0,0 +1,97 @@ +# Copyright (c) 2022 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. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: httproute-app-root + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-validation + namespace: higress-system +spec: + defaultConfig: + header_schema: + type: object + required: + - enum_payload + properties: + enum_payload: + type: string + enum: + - "enum_string_1" + - "enum_string_2" + default: "enum_string_1" + body_schema: + type: object + required: + - enum_payload + - bool_payload + - integer_payload + - string_payload + - regex_payload + - array_payload + properties: + enum_payload: + type: string + enum: + - "enum_string_1" + - "enum_string_2" + default: "enum_string_1" + bool_payload: + type: boolean + default: true + integer_payload: + type: integer + minimum: 1 + maximum: 65535 + string_payload: + type: string + minLength: 2 + maxLength: 32 + regex_payload: + type: string + minLength: 1 + maxLength: 32 + pattern: "^[a-zA-Z0-9]+$" + array_payload: + type: array + minItems: 2 + items: + type: integer + minimum: 200 + maximum: 599 + uniqueItems: true + default: [200, 302] + rejected_code: 403 + rejected_msg: "customize reject message" + enable_swagger: true + enable_oas3: false + url: file:///opt/plugins/wasm-go/extensions/request-validation/plugin.wasm