feat: 🎸 frontend-gray plugin support cdn type deploy (#1178)

Co-authored-by: Kent Dong <ch3cho@qq.com>
This commit is contained in:
mamba
2024-08-14 15:41:32 +08:00
committed by GitHub
parent d31c978ed3
commit ba98f3a7ad
8 changed files with 524 additions and 128 deletions

View File

@@ -1,6 +1,10 @@
package main
import (
"fmt"
"net/http"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util"
@@ -15,6 +19,9 @@ func main() {
"frontend-gray",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody),
)
}
@@ -24,55 +31,146 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L
return nil
}
// FilterGrayRule 过滤灰度规则
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, log wrapper.Log) *config.GrayDeployments {
for _, grayDeployment := range grayConfig.GrayDeployments {
if !grayDeployment.Enabled {
// 跳过Enabled=false
continue
}
grayRule := util.GetRule(grayConfig.Rules, grayDeployment.Name)
// 首先先校验用户名单ID
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
if util.Contains(grayRule.GrayKeyValue, grayKeyValue) {
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayDeployment.Version, grayKeyValue)
return grayDeployment
}
}
// 第二校验Cookie中的 GrayTagKey
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
grayTagValue := util.GetValueByCookie(cookieStr, grayRule.GrayTagKey)
if util.Contains(grayRule.GrayTagValue, grayTagValue) {
log.Infof("x-mse-tag: %s, grayTag: %s=%s", grayDeployment.Version, grayRule.GrayTagKey, grayTagValue)
return grayDeployment
}
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
return types.ActionContinue
}
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
return nil
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
path, _ := proxywasm.GetHttpRequestHeader(":path")
fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
isIndex := util.IsIndexRequest(fetchMode, path)
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
grayKeyValue := util.GetGrayKey(util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey), grayConfig.GraySubKey)
// 如果有重写的配置,则进行重写
if hasRewrite {
// 禁止重新路由要在更改Header之前操作否则会失效
ctx.DisableReroute()
}
// 删除Accept-Encoding避免压缩 如果是压缩的内容,后续插件就没法处理了
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, log.Infof)
frontendVersion := util.GetVersion(grayConfig.BaseDeployment.Version, cookies, isIndex)
backendVersion := ""
// 命中灰度规则
if grayDeployment != nil {
frontendVersion = util.GetVersion(grayDeployment.Version, cookies, isIndex)
backendVersion = grayDeployment.BackendVersion
}
proxywasm.AddHttpRequestHeader(config.XHigressTag, frontendVersion)
ctx.SetContext(config.XPreHigressTag, frontendVersion)
ctx.SetContext(config.XMseTag, backendVersion)
ctx.SetContext(config.IsIndex, isIndex)
rewrite := grayConfig.Rewrite
if rewrite.Host != "" {
proxywasm.ReplaceHttpRequestHeader("HOST", rewrite.Host)
}
if hasRewrite {
rewritePath := path
if isIndex {
rewritePath = util.IndexRewrite(path, frontendVersion, grayConfig.Rewrite.Index)
} else {
rewritePath = util.PrefixFileRewrite(path, frontendVersion, grayConfig.Rewrite.File)
}
log.Infof("rewrite path: %s %s %v", path, frontendVersion, rewritePath)
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
}
return types.ActionContinue
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
// 优先从cookie中获取如果拿不到再从header中获取
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
grayHeaderKey, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
grayKeyValue := util.GetValueByCookie(cookieStr, grayConfig.GrayKey)
proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
// 优先从Cookie中获取否则从header中获取
if grayKeyValue == "" {
grayKeyValue = grayHeaderKey
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
return types.ActionContinue
}
// 如果有子key, 尝试从子key中获取值
if grayConfig.GraySubKey != "" {
subKeyValue := util.GetBySubKey(grayKeyValue, grayConfig.GraySubKey)
if subKeyValue != "" {
grayKeyValue = subKeyValue
status, err := proxywasm.GetHttpResponseHeader(":status")
contentType, _ := proxywasm.GetHttpResponseHeader("Content-Type")
if err != nil || status != "200" {
isIndex := ctx.GetContext(config.IsIndex)
if status == "404" {
if grayConfig.Rewrite.NotFound != "" && isIndex != nil && isIndex.(bool) {
ctx.SetContext(config.NotFound, true)
responseHeaders, _ := proxywasm.GetHttpResponseHeaders()
headersMap := util.ConvertHeaders(responseHeaders)
headersMap[":status"][0] = "200"
headersMap["content-type"][0] = "text/html"
delete(headersMap, "content-length")
proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap))
ctx.BufferResponseBody()
return types.ActionContinue
} else {
ctx.DontReadResponseBody()
}
}
log.Errorf("error status: %s, error message: %v", status, err)
return types.ActionContinue
}
grayDeployment := FilterGrayRule(&grayConfig, grayKeyValue, log)
if grayDeployment != nil {
proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployment.Version)
// 删除content-length可能要修改Response返回值
proxywasm.RemoveHttpResponseHeader("Content-Length")
// 删除Content-Disposition避免自动下载文件
proxywasm.RemoveHttpResponseHeader("Content-Disposition")
if strings.HasPrefix(contentType, "text/html") {
ctx.SetContext(config.IsHTML, true)
// 不会进去Streaming 的Body处理
ctx.BufferResponseBody()
// 添加Cache-Control 头部,禁止缓存
proxywasm.ReplaceHttpRequestHeader("Cache-Control", "no-cache, no-store")
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
backendVersion := ctx.GetContext(config.XMseTag).(string)
// 设置当前的前端版本
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XPreHigressTag, frontendVersion))
// 设置后端的前端版本
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XMseTag, backendVersion))
}
return types.ActionContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
return types.ActionContinue
}
backendVersion := ctx.GetContext(config.XMseTag)
isHtml := ctx.GetContext(config.IsHTML)
isIndex := ctx.GetContext(config.IsIndex)
notFoundUri := ctx.GetContext(config.NotFound)
if isIndex != nil && isIndex.(bool) && notFoundUri != nil && notFoundUri.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
client.Get(grayConfig.Rewrite.NotFound, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
proxywasm.ReplaceHttpResponseBody(responseBody)
proxywasm.ResumeHttpResponse()
}, 1500)
return types.ActionPause
}
// 以text/html 开头,将 cookie转到cookie
if isHtml != nil && isHtml.(bool) && backendVersion != nil && backendVersion.(string) != "" {
newText := strings.ReplaceAll(string(body), "</head>", `<script>
!function(e,t){function n(e){var n="; "+t.cookie,r=n.split("; "+e+"=");return 2===r.length?r.pop().split(";").shift():null}var r=n("x-mse-tag");if(!r)return null;var s=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(e,t,n,a,i){return this._XHR=!0,this.addEventListener("readystatechange",function(){1===this.readyState&&r&&this.setRequestHeader("x-mse-tag",r)}),s.apply(this,arguments)};var a=e.fetch;e.fetch=function(e,t){return"undefined"==typeof t&&(t={}),"undefined"==typeof t.headers&&(t.headers={}),r&&(t.headers["x-mse-tag"]=r),a.apply(this,[e,t])}}(window,document);
</script>
</head>`)
if err := proxywasm.ReplaceHttpResponseBody([]byte(newText)); err != nil {
return types.ActionContinue
}
}
return types.ActionContinue
}
func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.GrayConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
return chunk
}