plugins/wasm-go/extensions/frontend-gray/main.go (195 lines of code) (raw):

package main import ( "fmt" "strings" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util" "github.com/alibaba/higress/plugins/wasm-go/pkg/log" "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" ) func main() { wrapper.SetCtx( "frontend-gray", wrapper.ParseConfig(parseConfig), wrapper.ProcessRequestHeaders(onHttpRequestHeaders), wrapper.ProcessResponseHeaders(onHttpResponseHeader), wrapper.ProcessResponseBody(onHttpResponseBody), ) } func parseConfig(json gjson.Result, grayConfig *config.GrayConfig) error { // 解析json 为GrayConfig if err := config.JsonToGrayConfig(json, grayConfig); err != nil { log.Errorf("failed to parse config: %v", err) return err } return nil } func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig) types.Action { requestPath := util.GetRequestPath() enabledGray := util.IsGrayEnabled(requestPath, &grayConfig) ctx.SetContext(config.EnabledGray, enabledGray) route, _ := util.GetRouteName() if !enabledGray { log.Infof("route: %s, gray not enabled, requestPath: %v", route, requestPath) ctx.DontReadRequestBody() return types.ActionContinue } cookie, _ := proxywasm.GetHttpRequestHeader("cookie") isHtmlRequest := util.CheckIsHtmlRequest(requestPath) ctx.SetContext(config.IsHtmlRequest, isHtmlRequest) isIndexRequest := util.IsIndexRequest(requestPath, grayConfig.IndexPaths) ctx.SetContext(config.IsIndexRequest, isIndexRequest) hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0 grayKeyValueByCookie := util.GetCookieValue(cookie, grayConfig.GrayKey) grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey) // 优先从cookie中获取,否则从header中获取 grayKeyValue := util.GetGrayKey(grayKeyValueByCookie, grayKeyValueByHeader, grayConfig.GraySubKey) // 如果有重写的配置,则进行重写 if hasRewrite { // 禁止重新路由,要在更改Header之前操作,否则会失效 ctx.DisableReroute() } frontendVersion := util.GetCookieValue(cookie, config.XHigressTag) if grayConfig.GrayWeight > 0 { ctx.SetContext(grayConfig.UniqueGrayTag, util.GetGrayWeightUniqueId(cookie, grayConfig.UniqueGrayTag)) } // 删除Accept-Encoding,避免压缩, 如果是压缩的内容,后续插件就没法处理了 _ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding") _ = proxywasm.RemoveHttpRequestHeader("Content-Length") deployment := &config.Deployment{} globalConfig := grayConfig.Injection.GlobalConfig if globalConfig.Enabled { conditionRule := util.GetConditionRules(grayConfig.Rules, grayKeyValue, cookie) trimmedValue := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(globalConfig.Value), "{"), "}") ctx.SetContext(globalConfig.Key, fmt.Sprintf("<script>var %s = {\n%s:%s,\n %s \n}\n</script>", globalConfig.Key, globalConfig.FeatureKey, conditionRule, trimmedValue)) } if isHtmlRequest { // index首页请求每次都会进度灰度规则判断 deployment = util.FilterGrayRule(&grayConfig, grayKeyValue, cookie) log.Infof("route: %s, index html request: %v, backend: %v, xPreHigressVersion: %s", route, requestPath, deployment.BackendVersion, frontendVersion) ctx.SetContext(config.PreHigressVersion, deployment.Version) ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion) } else { if util.IsSupportMultiVersion(grayConfig) { deployment = util.FilterMultiVersionGrayRule(&grayConfig, grayKeyValue, cookie, requestPath) log.Infof("route: %s, multi version %v", route, deployment) } else { grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, cookie) if isIndexRequest { deployment = grayDeployment } else { deployment = util.GetVersion(grayConfig, grayDeployment, frontendVersion) } } } proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version) rewrite := grayConfig.Rewrite if rewrite.Host != "" { err := proxywasm.ReplaceHttpRequestHeader(":authority", rewrite.Host) if err != nil { log.Errorf("route: %s, host rewrite failed: %v", route, err) } } if hasRewrite { rewritePath := requestPath if isHtmlRequest { rewritePath = util.IndexRewrite(requestPath, deployment.Version, grayConfig.Rewrite.Index) } else { rewritePath = util.PrefixFileRewrite(requestPath, deployment.Version, grayConfig.Rewrite.File) } if requestPath != rewritePath { log.Infof("route: %s, rewrite path:%s, rewritePath:%s, Version:%v", route, requestPath, rewritePath, deployment.Version) proxywasm.ReplaceHttpRequestHeader(":path", rewritePath) } } return types.ActionContinue } func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig) types.Action { enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool) if !enabledGray { ctx.DontReadResponseBody() return types.ActionContinue } isIndexRequest, indexOk := ctx.GetContext(config.IsIndexRequest).(bool) if indexOk && isIndexRequest { // 首页请求强制不缓存 proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") ctx.DontReadResponseBody() return types.ActionContinue } isHtmlRequest, htmlOk := ctx.GetContext(config.IsHtmlRequest).(bool) // response 不处理非首页的请求 if !htmlOk || !isHtmlRequest { ctx.DontReadResponseBody() return types.ActionContinue } else { // 不会进去Streaming 的Body处理 ctx.BufferResponseBody() } // 处理HTML的首页 status, err := proxywasm.GetHttpResponseHeader(":status") if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { // 删除Content-Disposition,避免自动下载文件 proxywasm.RemoveHttpResponseHeader("Content-Disposition") } // 删除content-length,可能要修改Response返回值 proxywasm.RemoveHttpResponseHeader("Content-Length") // 处理code为 200的情况 if err != nil || status != "200" { // 如果找不到HTML,但配置了HTML页面 if status == "404" && grayConfig.Html != "" { responseHeaders, _ := proxywasm.GetHttpResponseHeaders() headersMap := util.ConvertHeaders(responseHeaders) delete(headersMap, "content-length") headersMap[":status"][0] = "200" headersMap["content-type"][0] = "text/html" ctx.BufferResponseBody() proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap)) } else { route, _ := util.GetRouteName() log.Errorf("route: %s, request error code: %s, message: %v", route, status, err) ctx.DontReadResponseBody() return types.ActionContinue } } proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") // 前端版本 frontendVersion, isFrontendVersionOk := ctx.GetContext(config.PreHigressVersion).(string) if isFrontendVersionOk { proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", config.XHigressTag, frontendVersion, grayConfig.StoreMaxAge)) } // 设置GrayWeight 唯一值 if grayConfig.GrayWeight > 0 { uniqueId, isUniqueIdOk := ctx.GetContext(grayConfig.UniqueGrayTag).(string) if isUniqueIdOk { proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", grayConfig.UniqueGrayTag, uniqueId, grayConfig.StoreMaxAge)) } } // 设置后端的版本 if util.IsBackendGrayEnabled(grayConfig) { backendVersion, isBackVersionOk := ctx.GetContext(grayConfig.BackendGrayTag).(string) if isBackVersionOk { if backendVersion == "" { // 删除后端灰度版本 proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/;", grayConfig.BackendGrayTag, backendVersion)) } else { proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.StoreMaxAge)) } } } return types.ActionContinue } func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte) types.Action { enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool) if !enabledGray { return types.ActionContinue } isHtmlRequest, isHtmlRequestOk := ctx.GetContext(config.IsHtmlRequest).(bool) frontendVersion, isFeVersionOk := ctx.GetContext(config.PreHigressVersion).(string) // 只处理首页相关请求 if !isFeVersionOk || !isHtmlRequestOk || !isHtmlRequest { return types.ActionContinue } globalConfig := grayConfig.Injection.GlobalConfig globalConfigValue, isGobalConfigOk := ctx.GetContext(globalConfig.Key).(string) if !isGobalConfigOk { globalConfigValue = "" } newHtml := string(body) if grayConfig.Html != "" { newHtml = grayConfig.Html } newHtml = util.InjectContent(newHtml, grayConfig.Injection, globalConfigValue) // 替换当前html加载的动态文件版本 newHtml = strings.ReplaceAll(newHtml, "{version}", frontendVersion) newHtml = util.FixLocalStorageKey(newHtml, grayConfig.LocalStorageGrayKey) // 最终替换响应体 if err := proxywasm.ReplaceHttpResponseBody([]byte(newHtml)); err != nil { route, _ := util.GetRouteName() log.Errorf("route: %s, Failed to replace response body: %v", route, err) return types.ActionContinue } return types.ActionContinue }