Files
higress/plugins/wasm-go/extensions/frontend-gray/main.go
2025-07-11 10:43:00 +08:00

245 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/higress-group/wasm-go/pkg/log"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/wrapper"
"github.com/tidwall/gjson"
)
func main() {}
func init() {
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.UniqueGrayTagConfigured || 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
}
if !grayConfig.UseManifestAsEntry {
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=/; HttpOnly; Secure", config.XHigressTag, frontendVersion, grayConfig.StoreMaxAge))
}
// 设置GrayWeight 唯一值
if grayConfig.UniqueGrayTagConfigured || grayConfig.GrayWeight > 0 {
uniqueId, isUniqueIdOk := ctx.GetContext(grayConfig.UniqueGrayTag).(string)
if isUniqueIdOk {
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/; HttpOnly; Secure", 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=/; HttpOnly; Secure", grayConfig.BackendGrayTag, backendVersion))
} else {
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/; HttpOnly; Secure", 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
}