Files
higress/plugins/wasm-go/extensions/frontend-gray/main.go

242 lines
9.0 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"
"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"
"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.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody),
)
}
func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error {
// 解析json 为GrayConfig
config.JsonToGrayConfig(json, grayConfig)
log.Infof("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments"))
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
return types.ActionContinue
}
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
path, _ := proxywasm.GetHttpRequestHeader(":path")
fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
isPageRequest := util.IsPageRequest(fetchMode, path)
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey)
grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
// 优先从cookie中获取否则从header中获取
grayKeyValue := util.GetGrayKey(grayKeyValueByCookie, grayKeyValueByHeader, grayConfig.GraySubKey)
// 如果有重写的配置,则进行重写
if hasRewrite {
// 禁止重新路由要在更改Header之前操作否则会失效
ctx.DisableReroute()
}
// 删除Accept-Encoding避免压缩 如果是压缩的内容,后续插件就没法处理了
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
deployment := &config.Deployment{}
preVersion, preUniqueClientId := util.GetXPreHigressVersion(cookies)
// 客户端唯一ID用于在按照比率灰度时候 客户访问黏贴
uniqueClientId := grayKeyValue
if uniqueClientId == "" {
xForwardedFor, _ := proxywasm.GetHttpRequestHeader("X-Forwarded-For")
uniqueClientId = util.GetRealIpFromXff(xForwardedFor)
}
// 如果没有配置比例,则进行灰度规则匹配
if isPageRequest {
if grayConfig.TotalGrayWeight > 0 {
log.Infof("grayConfig.TotalGrayWeight: %v", grayConfig.TotalGrayWeight)
deployment = util.FilterGrayWeight(&grayConfig, preVersion, preUniqueClientId, uniqueClientId)
} else {
deployment = util.FilterGrayRule(&grayConfig, grayKeyValue)
}
log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, path, deployment.BackendVersion, preVersion, preUniqueClientId)
} else {
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue)
deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest)
}
proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version)
ctx.SetContext(config.XPreHigressTag, deployment.Version)
ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion)
ctx.SetContext(config.IsPageRequest, isPageRequest)
ctx.SetContext(config.XUniqueClientId, uniqueClientId)
rewrite := grayConfig.Rewrite
if rewrite.Host != "" {
proxywasm.ReplaceHttpRequestHeader("HOST", rewrite.Host)
}
if hasRewrite {
rewritePath := path
if isPageRequest {
rewritePath = util.IndexRewrite(path, deployment.Version, grayConfig.Rewrite.Index)
} else {
rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File)
}
log.Infof("rewrite path: %s %s %v", path, deployment.Version, rewritePath)
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
}
return types.ActionContinue
}
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
return types.ActionContinue
}
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
if !ok {
isPageRequest = false // 默认值
}
// response 不处理非首页的请求
if !isPageRequest {
ctx.DontReadResponseBody()
return types.ActionContinue
}
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" {
if status == "404" {
if grayConfig.Rewrite.NotFound != "" && isPageRequest {
ctx.SetContext(config.IsNotFound, true)
responseHeaders, _ := proxywasm.GetHttpResponseHeaders()
headersMap := util.ConvertHeaders(responseHeaders)
if _, ok := headersMap[":status"]; !ok {
headersMap[":status"] = []string{"200"} // 如果没有初始化,设定默认值
} else {
headersMap[":status"][0] = "200" // 修改现有值
}
if _, ok := headersMap["content-type"]; !ok {
headersMap["content-type"] = []string{"text/html"} // 如果没有初始化,设定默认值
} else {
headersMap["content-type"][0] = "text/html" // 修改现有值
}
// 删除 content-length 键
delete(headersMap, "content-length")
proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap))
ctx.BufferResponseBody()
return types.ActionContinue
} else {
// 直接返回400
ctx.DontReadResponseBody()
}
}
log.Errorf("error status: %s, error message: %v", status, err)
return types.ActionContinue
}
// 不会进去Streaming 的Body处理
ctx.BufferResponseBody()
proxywasm.ReplaceHttpResponseHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
xUniqueClient := ctx.GetContext(config.XUniqueClientId).(string)
// 设置前端的版本
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, grayConfig.UserStickyMaxAge))
// 设置后端的版本
if util.IsBackendGrayEnabled(grayConfig) {
backendVersion := ctx.GetContext(grayConfig.BackendGrayTag).(string)
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.UserStickyMaxAge))
}
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
}
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
if !ok {
isPageRequest = false // 默认值
}
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
isNotFound, ok := ctx.GetContext(config.IsNotFound).(bool)
if !ok {
isNotFound = false // 默认值
}
// 检查是否存在自定义 HTML 如有则省略 rewrite.indexRouting 的内容
if grayConfig.Html != "" {
log.Debugf("Returning custom HTML from config.")
// 替换响应体为 config.Html 内容
if err := proxywasm.ReplaceHttpResponseBody([]byte(grayConfig.Html)); err != nil {
log.Errorf("Error replacing response body: %v", err)
return types.ActionContinue
}
newHtml := util.InjectContent(grayConfig.Html, grayConfig.Injection)
// 替换当前html加载的动态文件版本
newHtml = strings.ReplaceAll(newHtml, "{version}", frontendVersion)
// 最终替换响应体
if err := proxywasm.ReplaceHttpResponseBody([]byte(newHtml)); err != nil {
log.Errorf("Error replacing injected response body: %v", err)
return types.ActionContinue
}
return types.ActionContinue
}
if isPageRequest && isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
proxywasm.ReplaceHttpResponseBody(responseBody)
proxywasm.ResumeHttpResponse()
}, 1500)
return types.ActionPause
}
if isPageRequest {
// 将原始字节转换为字符串
newBody := string(body)
newBody = util.InjectContent(newBody, grayConfig.Injection)
if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); 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
}