Files
higress/plugins/wasm-go/extensions/request-validation/main.go
2025-03-26 20:27:53 +08:00

200 lines
5.5 KiB
Go

// 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/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/santhosh-tekuri/jsonschema"
"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.SendHttpResponseWithDetail(config.rejectedCode, "request-validation.invalid_headers", 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.SendHttpResponseWithDetail(config.rejectedCode, "request-validation.invalid_body", nil, []byte(config.rejectedMsg), -1)
return types.ActionPause
}
return types.ActionContinue
}