From bb6c43c767d129f83d9799413bb503ed86ec0713 Mon Sep 17 00:00:00 2001 From: mamba <371510756@qq.com> Date: Wed, 23 Oct 2024 09:34:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=80=90frontend-gray=E3=80=91?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20skipedRoutes=E4=BB=A5=E5=8F=8AskipedByHead?= =?UTF-8?q?ers=20=E9=85=8D=E7=BD=AE=20(#1409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kent Dong --- .../extensions/frontend-gray/README.md | 5 +- .../extensions/frontend-gray/config/config.go | 5 ++ .../extensions/frontend-gray/envoy.yaml | 3 + .../wasm-go/extensions/frontend-gray/main.go | 32 +++++--- .../extensions/frontend-gray/util/utils.go | 81 ++++++++++++++----- .../frontend-gray/util/utils_test.go | 20 +++-- 6 files changed, 101 insertions(+), 45 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index dba4b73c0..870100eb2 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -20,6 +20,9 @@ description: 前端灰度插件配置参考 | `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 | | `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` | | `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 | +| `skippedPathPrefixes` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求。例如,在 rewrite 场景下,XHR 接口请求 `/api/xxx` 如果经过插件转发逻辑,可能会导致非预期的结果。 | +| `skippedByHeaders` | map of string to string | 非必填 | - | 用于通过请求头过滤,指定哪些请求不被当前插件 +处理。`skippedPathPrefixes` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。若本配置为空,默认会判断`sec-fetch-mode=cors`以及`upgrade=websocket`两个header头,进行过滤 | | `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 | | `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 | | `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 | @@ -266,4 +269,4 @@ injection: - - ``` -通过 `injection`往HTML首页注入代码,可以在`head`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。 \ No newline at end of file +通过 `injection`往HTML首页注入代码,可以在`head`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。 diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index ecfbb3a83..25596fefe 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -12,6 +12,7 @@ const ( XPreHigressTag = "x-pre-higress-tag" IsPageRequest = "is-page-request" IsNotFound = "is-not-found" + EnabledGray = "enabled-gray" ) type LogInfo func(format string, args ...interface{}) @@ -61,6 +62,8 @@ type GrayConfig struct { GrayDeployments []*Deployment BackendGrayTag string Injection *Injection + SkippedPathPrefixes []string + SkippedByHeaders map[string]string } func convertToStringList(results []gjson.Result) []string { @@ -91,6 +94,8 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { grayConfig.BackendGrayTag = json.Get("backendGrayTag").String() grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String() grayConfig.Html = json.Get("html").String() + grayConfig.SkippedPathPrefixes = convertToStringList(json.Get("skippedPathPrefixes").Array()) + grayConfig.SkippedByHeaders = convertToStringMap(json.Get("skippedByHeaders")) if grayConfig.UserStickyMaxAge == "" { // 默认值2天 diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index 239e221bd..bc584d5cc 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -82,6 +82,9 @@ static_resources: "/app1": "/mfe/app1/{version}" } }, + "skippedPathPrefixes": [ + "/api/" + ], "baseDeployment": { "version": "dev" }, diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index b1b5d28af..c5e738eea 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "net/http" + "path" "strings" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" @@ -32,15 +33,18 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L } func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { - if !util.IsGrayEnabled(grayConfig) { + requestPath, _ := proxywasm.GetHttpRequestHeader(":path") + requestPath = path.Clean(requestPath) + enabledGray := util.IsGrayEnabled(grayConfig, requestPath) + ctx.SetContext(config.EnabledGray, enabledGray) + + if !enabledGray { + ctx.DontReadRequestBody() return types.ActionContinue } cookies, _ := proxywasm.GetHttpRequestHeader("cookie") - path, _ := proxywasm.GetHttpRequestHeader(":path") - fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - - isPageRequest := util.IsPageRequest(fetchMode, path) + isPageRequest := util.IsPageRequest(requestPath) hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0 grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey) grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey) @@ -73,7 +77,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } else { deployment = util.FilterGrayRule(&grayConfig, grayKeyValue) } - log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, path, deployment.BackendVersion, preVersion, preUniqueClientId) + log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, requestPath, deployment.BackendVersion, preVersion, preUniqueClientId) } else { grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue) deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest) @@ -91,14 +95,14 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } if hasRewrite { - rewritePath := path + rewritePath := requestPath if isPageRequest { - rewritePath = util.IndexRewrite(path, deployment.Version, grayConfig.Rewrite.Index) + rewritePath = util.IndexRewrite(requestPath, deployment.Version, grayConfig.Rewrite.Index) } else { - rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File) + rewritePath = util.PrefixFileRewrite(requestPath, deployment.Version, grayConfig.Rewrite.File) } - if path != rewritePath { - log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", path, rewritePath, deployment.Version) + if requestPath != rewritePath { + log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", requestPath, rewritePath, deployment.Version) proxywasm.ReplaceHttpRequestHeader(":path", rewritePath) } } @@ -106,7 +110,8 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { - if !util.IsGrayEnabled(grayConfig) { + enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool) + if !enabledGray { ctx.DontReadResponseBody() return types.ActionContinue } @@ -179,7 +184,8 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action { - if !util.IsGrayEnabled(grayConfig) { + enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool) + if !enabledGray { return types.ActionContinue } isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool) diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index e67d3e089..da93c6821 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -47,7 +47,39 @@ func GetRealIpFromXff(xff string) string { return "" } -func IsGrayEnabled(grayConfig config.GrayConfig) bool { +func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool { + secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") + upgrade, _ := proxywasm.GetHttpRequestHeader("upgrade") + if len(grayConfig.SkippedByHeaders) == 0 { + // 默认不走插件逻辑的header + return secFetchMode == "cors" || upgrade == "websocket" + } + for headerKey, headerValue := range grayConfig.SkippedByHeaders { + requestHeader, _ := proxywasm.GetHttpRequestHeader(headerKey) + if requestHeader == headerValue { + return true + } + } + return false +} + +func IsGrayEnabled(grayConfig config.GrayConfig, requestPath string) bool { + // 当前路径中前缀为 SkipedRoute,则不走插件逻辑 + for _, prefix := range grayConfig.SkippedPathPrefixes { + if strings.HasPrefix(requestPath, prefix) { + return false + } + } + + // 如果是首页,进入插件逻辑 + if IsPageRequest(requestPath) { + return true + } + // 检查header标识,判断是否需要跳过 + if IsRequestSkippedByHeaders(grayConfig) { + return false + } + // 检查是否存在重写主机 if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { return true @@ -132,29 +164,35 @@ var indexSuffixes = []string{ ".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig", } -func IsPageRequest(fetchMode string, myPath string) bool { - if fetchMode == "cors" { - return false +func IsPageRequest(requestPath string) bool { + if requestPath == "/" || requestPath == "" { + return true } - ext := path.Ext(myPath) + ext := path.Ext(requestPath) return ext == "" || ContainsValue(indexSuffixes, ext) } -// 首页Rewrite -func IndexRewrite(path, version string, matchRules map[string]string) string { - // Create a slice of keys in matchRules and sort them by length in descending order +// SortKeysByLengthAndLexicographically 按长度降序和字典序排序键 +func SortKeysByLengthAndLexicographically(matchRules map[string]string) []string { keys := make([]string, 0, len(matchRules)) for prefix := range matchRules { keys = append(keys, prefix) } sort.Slice(keys, func(i, j int) bool { if len(keys[i]) != len(keys[j]) { - return len(keys[i]) > len(keys[j]) // Sort by length + return len(keys[i]) > len(keys[j]) // 按长度排序 } - return keys[i] < keys[j] // Sort lexicographically + return keys[i] < keys[j] // 按字典序排序 }) + return keys +} - // Iterate over sorted keys to find the longest match +// 首页Rewrite +func IndexRewrite(path, version string, matchRules map[string]string) string { + // 使用新的排序函数 + keys := SortKeysByLengthAndLexicographically(matchRules) + + // 遍历排序后的键以找到最长匹配 for _, prefix := range keys { if strings.HasPrefix(path, prefix) { rewrite := matchRules[prefix] @@ -166,18 +204,21 @@ func IndexRewrite(path, version string, matchRules map[string]string) string { } func PrefixFileRewrite(path, version string, matchRules map[string]string) string { - var matchedPrefix, replacement string - for prefix, template := range matchRules { + // 对规则的键进行排序 + sortedKeys := SortKeysByLengthAndLexicographically(matchRules) + + // 遍历排序后的键 + for _, prefix := range sortedKeys { if strings.HasPrefix(path, prefix) { - if len(prefix) > len(matchedPrefix) { // 找到更长的前缀 - matchedPrefix = prefix - replacement = strings.Replace(template, "{version}", version, 1) - } + // 找到第一个匹配的前缀就停止,因为它是最长的匹配 + replacement := strings.Replace(matchRules[prefix], "{version}", version, 1) + newPath := strings.Replace(path, prefix, replacement+"/", 1) + return filepath.Clean(newPath) } } - // 将path 中的前缀部分用 replacement 替换掉 - newPath := strings.Replace(path, matchedPrefix, replacement+"/", 1) - return filepath.Clean(newPath) + + // 如果没有匹配,返回原始路径 + return path } func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment { diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index b6681c98a..5fb69bb36 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -108,22 +108,20 @@ func TestPrefixFileRewrite(t *testing.T) { func TestIsPageRequest(t *testing.T) { var tests = []struct { - fetchMode string - p string - output bool + p string + output bool }{ - {"cors", "/js/a.js", false}, - {"no-cors", "/js/a.js", false}, - {"no-cors", "/images/a.png", false}, - {"no-cors", "/index", true}, - {"cors", "/inde", false}, - {"no-cors", "/index.html", true}, - {"no-cors", "/demo.php", true}, + {"/js/a.js", false}, + {"/js/a.js", false}, + {"/images/a.png", false}, + {"/index", true}, + {"/index.html", true}, + {"/demo.php", true}, } for _, test := range tests { testPath := test.p t.Run(testPath, func(t *testing.T) { - output := IsPageRequest(test.fetchMode, testPath) + output := IsPageRequest(testPath) assert.Equal(t, test.output, output) }) }