├── .gitignore ├── Dockerfile ├── gatewaydeploymentpatch.yaml ├── wasmplugin.yaml ├── go.mod ├── Makefile ├── envoyfilter.yaml ├── go.sum ├── envoy.yaml ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.wasm 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY plugin.wasm ./ 4 | -------------------------------------------------------------------------------- /gatewaydeploymentpatch.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | template: 3 | spec: 4 | containers: 5 | - name: istio-proxy 6 | volumeMounts: 7 | - name: wasm-plugins 8 | mountPath: /var/local/lib/wasm-plugins 9 | readOnly: true 10 | volumes: 11 | - name: wasm-plugins 12 | configMap: 13 | name: wasm-plugins -------------------------------------------------------------------------------- /wasmplugin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions.istio.io/v1alpha1 2 | kind: WasmPlugin 3 | metadata: 4 | name: json-validation 5 | namespace: istio-system 6 | spec: 7 | selector: 8 | matchLabels: 9 | istio: ingressgateway 10 | url: oci://YOUR_CONTAINER_REGISTRY/json-validation:v1 11 | imagePullPolicy: IfNotPresent 12 | phase: AUTHN 13 | pluginConfig: 14 | requiredKeys: ["id", "token"] 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetrateio/wasm-json-validation-demo 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/stretchr/testify v1.7.0 7 | github.com/tetratelabs/proxy-wasm-go-sdk v0.16.0 8 | github.com/tidwall/gjson v1.14.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | github.com/tidwall/match v1.1.1 // indirect 15 | github.com/tidwall/pretty v1.2.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | tinygo build -o plugin.wasm -scheduler=none -target=wasi main.go 4 | 5 | PROXYV2_IMAGE := containers.istio.tetratelabs.com/proxyv2:1.9.7-tetrate-v0 6 | 7 | .PHONY: run-envoy 8 | run-envoy: 9 | docker run --rm -p 18000:18000 -v $$(pwd)/envoy.yaml:/envoy.yaml -v $$(pwd)/plugin.wasm:/plugin.wasm --entrypoint envoy $(PROXYV2_IMAGE) -l debug -c /envoy.yaml 10 | 11 | .PHONY: docker-build-and-push 12 | docker-build-and-push: build 13 | docker build . -t ${HUB}/json-validation:v1 14 | docker push ${HUB}/json-validation:v1 15 | 16 | .PHONY: run-istio 17 | run-istio: 18 | # TODO 19 | -------------------------------------------------------------------------------- /envoyfilter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: json-validation 5 | namespace: istio-system 6 | spec: 7 | configPatches: 8 | - applyTo: HTTP_FILTER 9 | match: 10 | context: GATEWAY 11 | patch: 12 | operation: INSERT_BEFORE 13 | value: 14 | name: json-validation 15 | typed_config: 16 | '@type': type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 17 | config: 18 | configuration: 19 | "@type": type.googleapis.com/google.protobuf.StringValue 20 | value: | 21 | { "requiredKeys": ["id", "token"] } 22 | vm_config: 23 | code: 24 | local: 25 | filename: /var/local/lib/wasm-plugins/plugin.wasm 26 | runtime: envoy.wasm.runtime.v8 27 | vm_id: json-validation 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 8 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | github.com/tetratelabs/proxy-wasm-go-sdk v0.16.0 h1:6xhDLV4DD2+q3Rs4CDh7cqo69rQ50XgCusv/58D44o4= 10 | github.com/tetratelabs/proxy-wasm-go-sdk v0.16.0/go.mod h1:8CxNZJ+9yDEvNnAog384fC8j1tKNF0tTZevGjOuY9ds= 11 | github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= 12 | github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 13 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 14 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 15 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 16 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 21 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /envoy.yaml: -------------------------------------------------------------------------------- 1 | # This config has Envoy listen on localhost:18000, responding to any requests with static content "hello from server". 2 | # In addition, the example wasm plugin to validate the requests payload runs. 3 | # The plugin intercepts the request and makes Envoy return 403 instead of the static content 4 | # if the request has no JSON payload or the payload JSON doesn't have "id" or "token" keys. 5 | static_resources: 6 | listeners: 7 | - name: main 8 | address: 9 | socket_address: 10 | address: 0.0.0.0 11 | port_value: 18000 12 | filter_chains: 13 | - filters: 14 | - name: envoy.http_connection_manager 15 | typed_config: 16 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 17 | stat_prefix: ingress_http 18 | codec_type: auto 19 | route_config: 20 | name: local_route 21 | virtual_hosts: 22 | - name: local_service 23 | domains: 24 | - "*" 25 | routes: 26 | - match: 27 | prefix: "/" 28 | route: 29 | cluster: web_service 30 | 31 | http_filters: 32 | - name: envoy.filters.http.wasm 33 | typed_config: 34 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 35 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 36 | value: 37 | config: 38 | configuration: 39 | "@type": type.googleapis.com/google.protobuf.StringValue 40 | value: | 41 | { "requiredKeys": ["id", "token"] } 42 | vm_config: 43 | runtime: "envoy.wasm.runtime.v8" 44 | code: 45 | local: 46 | filename: "./plugin.wasm" 47 | - name: envoy.filters.http.router 48 | 49 | - name: staticreply 50 | address: 51 | socket_address: 52 | address: 127.0.0.1 53 | port_value: 8099 54 | filter_chains: 55 | - filters: 56 | - name: envoy.http_connection_manager 57 | typed_config: 58 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 59 | stat_prefix: ingress_http 60 | codec_type: auto 61 | route_config: 62 | name: local_route 63 | virtual_hosts: 64 | - name: local_service 65 | domains: 66 | - "*" 67 | routes: 68 | - match: 69 | prefix: "/" 70 | direct_response: 71 | status: 200 72 | body: 73 | inline_string: "hello from the server\n" 74 | http_filters: 75 | - name: envoy.filters.http.router 76 | typed_config: {} 77 | 78 | clusters: 79 | - name: web_service 80 | connect_timeout: 0.25s 81 | type: STATIC 82 | lb_policy: ROUND_ROBIN 83 | load_assignment: 84 | cluster_name: mock_service 85 | endpoints: 86 | - lb_endpoints: 87 | - endpoint: 88 | address: 89 | socket_address: 90 | address: 127.0.0.1 91 | port_value: 8099 92 | 93 | admin: 94 | access_log_path: "/dev/null" 95 | address: 96 | socket_address: 97 | address: 0.0.0.0 98 | port_value: 8001 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example JSON Payload validation by Proxy-Wasm Go 2 | 3 | `main.go` demonstrates how to perform validation on a request body. 4 | 5 | This wasm plugin checks whether the request has JSON payload and has required keys in it. 6 | If not, the wasm plugin ceases the further process of the request and returns 403 immediately. 7 | 8 | See https://github.com/tetratelabs/proxy-wasm-go-sdk/ for the detailed document of proxy-wasm-go SDK. 9 | 10 | ### Build 11 | 12 | ```sh 13 | make build 14 | ``` 15 | 16 | ### Run it via Envoy 17 | 18 | `envoy.yaml` is the example envoy config file that you can use for running the wasm plugin 19 | with standalone Envoy. 20 | The following make rule will run the wasm plugin in the version of Envoy used in the Istio 1.9 sidecar. 21 | 22 | ```sh 23 | make run-envoy 24 | ``` 25 | 26 | Envoy listens on `localhost:18000`, responding to any requests with static content "hello from server". 27 | However, the wasm plugin also runs to validate the requests' payload. 28 | The plugin intercepts the request and makes Envoy return 403 instead of the static content 29 | if the request has no JSON payload or the payload JSON doesn't have "id" or "token" keys. 30 | 31 | ```console 32 | # Returns the normal response when the request has the required keys, id and token. 33 | $ curl -X POST localhost:18000 -H 'Content-Type: application/json' --data '{"id": "xxx", "token": "xxx"}' 34 | hello from the server 35 | 36 | # Returns 403 when the request has missing required keys. 37 | $ curl -v -X POST localhost:18000 -H 'Content-Type: application/json' --data '"required_keys_missing"' 38 | Note: Unnecessary use of -X or --request, POST is already inferred. 39 | * Trying 127.0.0.1:18000... 40 | * TCP_NODELAY set 41 | * Connected to localhost (127.0.0.1) port 18000 (#0) 42 | > POST / HTTP/1.1 43 | > Host: localhost:18000 44 | > User-Agent: curl/7.68.0 45 | > Accept: */* 46 | > Content-Type: application/json 47 | > Content-Length: 23 48 | > 49 | * upload completely sent off: 23 out of 23 bytes 50 | * Mark bundle as not supporting multiuse 51 | < HTTP/1.1 403 Forbidden 52 | < content-length: 15 53 | < content-type: text/plain 54 | < date: Tue, 01 Mar 2022 19:22:24 GMT 55 | < server: envoy 56 | < 57 | * Connection #0 to host localhost left intact 58 | invalid payload 59 | 60 | ``` 61 | 62 | ### Run it via Istio 63 | 64 | This example details deploying to a kind cluster running the Istio httpbin sample app. 65 | 66 | ```console 67 | # Create a test cluster 68 | kind create cluster 69 | 70 | # Install Istio and the httpbin sample app 71 | istioctl install --set profile=demo -y 72 | kubectl label namespace default istio-injection=enabled 73 | kubectl apply -f samples/httpbin/httpbin.yaml 74 | kubectl apply -f samples/httpbin/httpbin-gateway.yaml 75 | ``` 76 | 77 | For Istio 1.12 and later the easiest way is to use a WasmPlugin resource. For older Istio 78 | versions an EnvoyFilter is needed. 79 | 80 | #### Install using WasmPlugin resource 81 | 82 | Build and push the wasm module to your container registry, then apply the WasmPlugin. 83 | 84 | ```console 85 | export HUB=your_registry # e.g. docker.io/tetrate 86 | make docker-build-and-push 87 | 88 | sed "s|YOUR_CONTAINER_REGISTRY|$HUB|" wasmplugin.yaml | kubectl apply -f - 89 | ``` 90 | 91 | #### Install using EnvoyFilter 92 | 93 | To use an EnvoyFilter, create a config map containing the compiled wasm plugin, mount the config 94 | map into the gateway pod, and then configure Envoy via an EnvoyFilter to load the wasm plugin from 95 | a local file. 96 | 97 | ```console 98 | # Create the config map 99 | kubectl -n istio-system create configmap wasm-plugins --from-file=plugin.wasm 100 | 101 | # Patch the gateway deployment to mount the config map 102 | kubectl -n istio-system patch deployment istio-ingressgateway --patch-file=gatewaydeploymentpatch.yaml 103 | 104 | # Create the EnvoyFilter 105 | kubectl apply -f envoyfilter.yaml 106 | ``` 107 | 108 | #### Test the plugin 109 | 110 | Expose the ingress gateway on port 8080 on your local machine via. 111 | 112 | ```console 113 | kubectl port-forward -n istio-system svc/istio-ingressgateway 8080:80 114 | ``` 115 | 116 | Requests without the required payload will fail: 117 | 118 | ```console 119 | % curl -i http://localhost:8080/post -H 'Content-Type: application/json' --data '{"id": "xxx", "not_token": "xxx"}' 120 | HTTP/1.1 403 Forbidden 121 | content-length: 15 122 | content-type: text/plain 123 | date: Tue, 15 Mar 2022 23:05:52 GMT 124 | server: istio-envoy 125 | 126 | invalid payload 127 | ``` 128 | 129 | But those with the payload will proceed: 130 | 131 | ```console 132 | % curl -i http://localhost:8080/post -H 'Content-Type: application/json' --data '{"id": "xxx", "token": "xxx"}' 133 | HTTP/1.1 200 OK 134 | server: istio-envoy 135 | date: Tue, 15 Mar 2022 23:06:29 GMT 136 | content-type: application/json 137 | content-length: 884 138 | access-control-allow-origin: * 139 | access-control-allow-credentials: true 140 | x-envoy-upstream-service-time: 3 141 | 142 | { 143 | "args": {}, 144 | "data": "{\"id\": \"xxx\", \"token\": \"xxx\"}", 145 | "files": {}, 146 | "form": {}, 147 | "headers": { 148 | "Accept": "*/*", 149 | "Content-Length": "29", 150 | "Content-Type": "application/json", 151 | "Host": "localhost:8080", 152 | "User-Agent": "curl/7.64.1", 153 | "X-B3-Parentspanid": "99a94908edd26592", 154 | "X-B3-Sampled": "1", 155 | "X-B3-Spanid": "e12fc7fd9aa74838", 156 | "X-B3-Traceid": "2b7375cda8bc98a299a94908edd26592", 157 | "X-Envoy-Attempt-Count": "1", 158 | "X-Envoy-Internal": "true", 159 | "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/default/sa/httpbin;Hash=5703a66dcdbc8cafc8c29e1ebee1174f4bc81234d8dc1ccc20fb9e3c26b320e1;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account" 160 | }, 161 | "json": { 162 | "id": "xxx", 163 | "token": "xxx" 164 | }, 165 | "origin": "10.244.0.9", 166 | "url": "http://localhost:8080/post" 167 | } 168 | ``` 169 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" 7 | "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" 8 | "github.com/tidwall/gjson" 9 | ) 10 | 11 | func main() { 12 | // SetVMContext is the entrypoint for setting up this entire Wasm VM. 13 | // Please make sure that this entrypoint be called during "main()" function, otherwise 14 | // this VM would fail. 15 | proxywasm.SetVMContext(&vmContext{}) 16 | } 17 | 18 | // vmContext implements types.VMContext interface of proxy-wasm-go SDK. 19 | type vmContext struct { 20 | // Embed the default VM context here, 21 | // so that we don't need to reimplement all the methods. 22 | types.DefaultVMContext 23 | } 24 | 25 | // Override types.DefaultVMContext. 26 | func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { 27 | return &pluginContext{} 28 | } 29 | 30 | // pluginContext implements types.PluginContext interface of proxy-wasm-go SDK. 31 | type pluginContext struct { 32 | // Embed the default plugin context here, 33 | // so that we don't need to reimplement all the methods. 34 | types.DefaultPluginContext 35 | configuration *pluginConfiguration 36 | } 37 | 38 | // pluginConfiguration is a type to represent an example configuration for this wasm plugin. 39 | type pluginConfiguration struct { 40 | // Example configuration field. 41 | // The plugin will validate if those fields exist in the json payload. 42 | requiredKeys []string 43 | } 44 | 45 | // Override types.DefaultPluginContext. 46 | func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { 47 | data, err := proxywasm.GetPluginConfiguration() 48 | if err != nil { 49 | proxywasm.LogCriticalf("error reading plugin configuration: %v", err) 50 | return types.OnPluginStartStatusFailed 51 | } 52 | config, err := parsePluginConfiguration(data) 53 | if err != nil { 54 | proxywasm.LogCriticalf("error parsing plugin configuration: %v", err) 55 | return types.OnPluginStartStatusFailed 56 | } 57 | ctx.configuration = config 58 | return types.OnPluginStartStatusOK 59 | } 60 | 61 | // parsePluginConfiguration parses the json plugin confiuration data and returns pluginConfiguration. 62 | // Note that this parses the json data by gjson, since TinyGo doesn't support encoding/json. 63 | // You can also try https://github.com/mailru/easyjson, which supports decoding to a struct. 64 | func parsePluginConfiguration(data []byte) (*pluginConfiguration, error) { 65 | config := &pluginConfiguration{} 66 | if !gjson.ValidBytes(data) { 67 | return nil, fmt.Errorf("the plugin configuration is not a valid json: %v", data) 68 | } 69 | 70 | jsonData := gjson.ParseBytes(data) 71 | requiredKeys := jsonData.Get("requiredKeys").Array() 72 | for _, requiredKey := range requiredKeys { 73 | config.requiredKeys = append(config.requiredKeys, requiredKey.Str) 74 | } 75 | 76 | return config, nil 77 | } 78 | 79 | // Override types.DefaultPluginContext. 80 | func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { 81 | return &payloadValidationContext{requiredKeys: ctx.configuration.requiredKeys} 82 | } 83 | 84 | // payloadValidationContext implements types.HttpContext interface of proxy-wasm-go SDK. 85 | type payloadValidationContext struct { 86 | // Embed the default root http context here, 87 | // so that we don't need to reimplement all the methods. 88 | types.DefaultHttpContext 89 | totalRequestBodySize int 90 | requiredKeys []string 91 | } 92 | 93 | // Override types.DefaultHttpContext. 94 | func (ctx *payloadValidationContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { 95 | contentType, err := proxywasm.GetHttpRequestHeader("content-type") 96 | if err != nil || contentType != "application/json" { 97 | // If the header doesn't have the expected content value, send the 403 response, 98 | if err := proxywasm.SendHttpResponse(403, nil, []byte("content-type must be provided"), -1); err != nil { 99 | panic(err) 100 | } 101 | // and terminates the further processing of this traffic by ActionPause. 102 | return types.ActionPause 103 | } 104 | 105 | // ActionContinue lets the host continue the processing the body. 106 | return types.ActionContinue 107 | } 108 | 109 | // Override types.DefaultHttpContext. 110 | func (ctx *payloadValidationContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action { 111 | ctx.totalRequestBodySize += bodySize 112 | if !endOfStream { 113 | // OnHttpRequestBody may be called each time a part of the body is received. 114 | // Wait until we see the entire body to replace. 115 | return types.ActionPause 116 | } 117 | 118 | body, err := proxywasm.GetHttpRequestBody(0, ctx.totalRequestBodySize) 119 | if err != nil { 120 | proxywasm.LogErrorf("failed to get request body: %v", err) 121 | return types.ActionContinue 122 | } 123 | 124 | if !ctx.validatePayload(body) { 125 | // If the validation fails, send the 403 response, 126 | if err := proxywasm.SendHttpResponse(403, nil, []byte("invalid payload"), -1); err != nil { 127 | proxywasm.LogErrorf("failed to send the 403 response: %v", err) 128 | } 129 | // and terminates this traffic. 130 | return types.ActionPause 131 | } 132 | 133 | return types.ActionContinue 134 | } 135 | 136 | // validatePayload validates the given json payload. 137 | // Note that this function parses the json data by gjson, since TinyGo doesn't support encoding/json. 138 | func (ctx *payloadValidationContext) validatePayload(body []byte) bool { 139 | if !gjson.ValidBytes(body) { 140 | proxywasm.LogErrorf("body is not a valid json: %v", body) 141 | return false 142 | } 143 | jsonData := gjson.ParseBytes(body) 144 | 145 | // Do any validation on the json. Check if required keys exist here as an example. 146 | // The required keys are configurable via the plugin configuration. 147 | for _, requiredKey := range ctx.requiredKeys { 148 | if !jsonData.Get(requiredKey).Exists() { 149 | proxywasm.LogErrorf("required key (%v) is missing: %v", requiredKey, jsonData) 150 | return false 151 | } 152 | } 153 | 154 | return true 155 | } 156 | --------------------------------------------------------------------------------