diff --git a/plugins/wasm-go/extensions/cache-control/READMD.md b/plugins/wasm-go/extensions/cache-control/READMD.md new file mode 100644 index 000000000..ea386bac4 --- /dev/null +++ b/plugins/wasm-go/extensions/cache-control/READMD.md @@ -0,0 +1,28 @@ +# 功能说明 +`cache-control`插件实现了基于 URL 文件后缀来为请求的响应头部添加 `Expires` 和 `Cache-Control` 头部,从而方便浏览器对特定后缀的文件进行缓存,例如 `jpg`、`png` 等图片文件。 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +|---------|--------|-----------------------------------------------------------------------------------------------------|-|--------------------------| +| suffix | string | 选填,表示匹配的文件后缀名,例如 `jpg`、`png` 等。
如果需要匹配多种后缀,需要用 `\|` 进行分割,例如 `png\|jpg`。
如果不填写,表示匹配所有后缀 | - | 配置用于匹配的请求文件后缀 | +| expires | string | 必填,表示缓存的最长时间。
当填入的字符串为数字时,单位为秒,例如需要缓存1小时,需填写 3600。
另外,还可以填写 epoch 或 max
,与 nginx 中语义相同。 | - | 配置缓存的最大时间 | + +# 配置示例 +1. 缓存后缀为 `jpg`, `png`, `jpeg` 的文件,缓存时间为一小时 +```yaml +suffix: jpg|png|jpeg +expires: 3600 +``` + +根据该配置,下列请求在访问时,将会在响应头中添加 `Expires` 和 `Cache-Control` 字段,且过期时间为 1 小时后。 + +```bash +curl http://example.com/test.png +curl http://exmaple.com/test.jpg +``` +2. 缓存所有文件,且缓存至最大时间 `“Thu, 31 Dec 2037 23:55:55 GMT”` +```yaml +expires: max +``` + diff --git a/plugins/wasm-go/extensions/cache-control/VERSION b/plugins/wasm-go/extensions/cache-control/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-go/extensions/cache-control/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/cache-control/go.mod b/plugins/wasm-go/extensions/cache-control/go.mod new file mode 100644 index 000000000..9f37a0444 --- /dev/null +++ b/plugins/wasm-go/extensions/cache-control/go.mod @@ -0,0 +1,20 @@ +module github.com/alibaba/higress/plugins/wasm-go/extensions/cache-control + +go 1.18 + +replace github.com/alibaba/higress/plugins/wasm-go => ../.. + +require ( + github.com/alibaba/higress/plugins/wasm-go v0.0.0 + github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a + github.com/tidwall/gjson v1.14.3 +) + +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 + github.com/tidwall/resp v0.1.1 // indirect +) diff --git a/plugins/wasm-go/extensions/cache-control/main.go b/plugins/wasm-go/extensions/cache-control/main.go new file mode 100644 index 000000000..e5b7d16cf --- /dev/null +++ b/plugins/wasm-go/extensions/cache-control/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "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/tidwall/gjson" + "net/http" + "strconv" + "strings" + "time" +) + +func main() { + wrapper.SetCtx( + "cache-control", + wrapper.ParseConfigBy(parseConfig), + wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders), + ) +} + +type CacheControlConfig struct { + suffix []string + expires string +} + +func parseConfig(json gjson.Result, config *CacheControlConfig, log wrapper.Log) error { + suffix := json.Get("suffix").String() + if suffix != "" { + parts := strings.Split(suffix, "|") + config.suffix = parts + } + + config.expires = json.Get("expires").String() + + log.Infof("suffix: %q, expires: %s", config.suffix, config.expires) + return nil +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, config CacheControlConfig, log wrapper.Log) types.Action { + path := ctx.Path() + if strings.Contains(path, "?") { + path = strings.Split(path, "?")[0] + } + ctx.SetContext("path", path) + log.Debugf("path: %s", path) + + return types.ActionContinue +} + +func onHttpResponseHeaders(ctx wrapper.HttpContext, config CacheControlConfig, log wrapper.Log) types.Action { + hit := false + if len(config.suffix) == 0 { + hit = true + } else { + path, ok := ctx.GetContext("path").(string) + if !ok { + log.Error("failed to get request path") + return types.ActionContinue + } + + for _, part := range config.suffix { + if strings.HasSuffix(path, "."+part) { + hit = true + break + } + } + } + if hit { + if config.expires == "max" { + proxywasm.AddHttpResponseHeader("Expires", "Thu, 31 Dec 2037 23:55:55 GMT") + proxywasm.AddHttpResponseHeader("Cache-Control", "maxAge=315360000") + } else if config.expires == "epoch" { + proxywasm.AddHttpResponseHeader("Expires", "Thu, 01 Jan 1970 00:00:01 GMT") + proxywasm.AddHttpResponseHeader("Cache-Control", "no-cache") + } else { + maxAge, _ := strconv.ParseInt(config.expires, 10, 64) + currentTime := time.Now() + expireTime := currentTime.Add(time.Duration(maxAge) * time.Second) + proxywasm.AddHttpResponseHeader("Expires", expireTime.UTC().Format(http.TimeFormat)) + proxywasm.AddHttpResponseHeader("Cache-Control", "maxAge="+strconv.FormatInt(maxAge, 10)) + } + } + return types.ActionContinue +} diff --git a/test/e2e/conformance/tests/go-wasm-cache-control.go b/test/e2e/conformance/tests/go-wasm-cache-control.go new file mode 100644 index 000000000..a36b2c47a --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-cache-control.go @@ -0,0 +1,63 @@ +// 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 ( + "github.com/alibaba/higress/test/e2e/conformance/utils/http" + "github.com/alibaba/higress/test/e2e/conformance/utils/suite" + "testing" +) + +func init() { + Register(WasmPluginCacheControl) +} + +var WasmPluginCacheControl = suite.ConformanceTest{ + ShortName: "WasmPluginCacheControl", + Description: "The Ingress in the higress-conformance-infra namespace test the cache control WASM Plugin", + Manifests: []string{"tests/go-wasm-cache-control.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "case 1: Test hit", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"User-Agent": "BaiduMobaider/1.1.0"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "Cache-Control": "maxAge=3600", + }, + }, + }, + }, + } + t.Run("WasmPlugins cache-control", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/go-wasm-cache-control.yaml b/test/e2e/conformance/tests/go-wasm-cache-control.yaml new file mode 100644 index 000000000..4f98f4862 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-cache-control.yaml @@ -0,0 +1,48 @@ +# 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: + annotations: + name: wasmplugin-cache-control + 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: cache-control + namespace: higress-system +spec: + defaultConfigDisable: true + matchRules: + - config: + expires: 3600 + configDisable: false + ingress: + - higress-conformance-infra/wasmplugin-cache-control + url: file:///opt/plugins/wasm-go/extensions/cache-control/plugin.wasm \ No newline at end of file