mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 04:37:31 +08:00
feat: 🎸 frontend-gray plugin support cdn type deploy (#1178)
Co-authored-by: Kent Dong <ch3cho@qq.com>
This commit is contained in:
@@ -2,39 +2,56 @@ package util
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// GetValueByCookie 根据 cookieStr 和 cookieName 获取 cookie 值
|
||||
func GetValueByCookie(cookieStr string, cookieName string) string {
|
||||
if cookieStr == "" {
|
||||
func IsGrayEnabled(grayConfig config.GrayConfig) bool {
|
||||
// 检查是否存在重写主机
|
||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查灰度部署是否为 nil 或空
|
||||
grayDeployments := grayConfig.GrayDeployments
|
||||
if grayDeployments != nil && len(grayDeployments) > 0 {
|
||||
for _, grayDeployment := range grayDeployments {
|
||||
if grayDeployment.Enabled {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtractCookieValueByKey 根据 cookie 和 key 获取 cookie 值
|
||||
func ExtractCookieValueByKey(cookie string, key string) string {
|
||||
if cookie == "" {
|
||||
return ""
|
||||
}
|
||||
cookies := strings.Split(cookieStr, ";")
|
||||
curCookieName := cookieName + "="
|
||||
var foundCookieValue string
|
||||
var found bool
|
||||
// 遍历找到 cookie 对并处理
|
||||
for _, cookie := range cookies {
|
||||
cookie = strings.TrimSpace(cookie) // 清理空白符
|
||||
if strings.HasPrefix(cookie, curCookieName) {
|
||||
foundCookieValue = cookie[len(curCookieName):]
|
||||
found = true
|
||||
value := ""
|
||||
pairs := strings.Split(cookie, ";")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
kv := strings.Split(pair, "=")
|
||||
if kv[0] == key {
|
||||
value = kv[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
return foundCookieValue
|
||||
return value
|
||||
}
|
||||
|
||||
// contains 检查切片 slice 中是否含有元素 value。
|
||||
func Contains(slice []interface{}, value string) bool {
|
||||
func ContainsValue(slice []string, value string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
@@ -43,6 +60,30 @@ func Contains(slice []interface{}, value string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// headers: [][2]string -> map[string][]string
|
||||
func ConvertHeaders(hs [][2]string) map[string][]string {
|
||||
ret := make(map[string][]string)
|
||||
for _, h := range hs {
|
||||
k, v := strings.ToLower(h[0]), h[1]
|
||||
ret[k] = append(ret[k], v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// headers: map[string][]string -> [][2]string
|
||||
func ReconvertHeaders(hs map[string][]string) [][2]string {
|
||||
var ret [][2]string
|
||||
for k, vs := range hs {
|
||||
for _, v := range vs {
|
||||
ret = append(ret, [2]string{k, v})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ret, func(i, j int) bool {
|
||||
return ret[i][0] < ret[j][0]
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
for _, rule := range rules {
|
||||
if rule.Name == name {
|
||||
@@ -52,7 +93,66 @@ func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 检查是否是页面
|
||||
var indexSuffixes = []string{
|
||||
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
|
||||
}
|
||||
|
||||
// IsIndexRequest determines if the request is an index request
|
||||
func IsIndexRequest(fetchMode string, p string) bool {
|
||||
if fetchMode == "cors" {
|
||||
return false
|
||||
}
|
||||
ext := path.Ext(p)
|
||||
return ext == "" || ContainsValue(indexSuffixes, ext)
|
||||
}
|
||||
|
||||
// 首页Rewrite
|
||||
func IndexRewrite(path, version string, matchRules map[string]string) string {
|
||||
for prefix, rewrite := range matchRules {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
newPath := strings.Replace(rewrite, "{version}", version, -1)
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func PrefixFileRewrite(path, version string, matchRules map[string]string) string {
|
||||
var matchedPrefix, replacement string
|
||||
for prefix, template := range matchRules {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
if len(prefix) > len(matchedPrefix) { // 找到更长的前缀
|
||||
matchedPrefix = prefix
|
||||
replacement = strings.Replace(template, "{version}", version, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 将path 中的前缀部分用 replacement 替换掉
|
||||
newPath := strings.Replace(path, matchedPrefix, replacement+"/", 1)
|
||||
return filepath.Clean(newPath)
|
||||
}
|
||||
|
||||
func GetVersion(version string, cookies string, isIndex bool) string {
|
||||
if isIndex {
|
||||
return version
|
||||
}
|
||||
// 来自Cookie中的版本
|
||||
cookieVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag)
|
||||
// cookie 中为空,返回当前版本
|
||||
if cookieVersion == "" {
|
||||
return version
|
||||
}
|
||||
|
||||
// cookie 中和当前版本不相同,返回cookie中值
|
||||
if cookieVersion != version {
|
||||
return cookieVersion
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// 从cookie中解析出灰度信息
|
||||
func getBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 首先对 URL 编码的字符串进行解码
|
||||
jsonStr, err := url.QueryUnescape(grayInfoStr)
|
||||
if err != nil {
|
||||
@@ -68,3 +168,43 @@ func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 返回字符串形式的值
|
||||
return value.String()
|
||||
}
|
||||
|
||||
func GetGrayKey(grayKeyValue string, graySubKey string) string {
|
||||
// 如果有子key, 尝试从子key中获取值
|
||||
if graySubKey != "" {
|
||||
subKeyValue := getBySubKey(grayKeyValue, graySubKey)
|
||||
if subKeyValue != "" {
|
||||
grayKeyValue = subKeyValue
|
||||
}
|
||||
}
|
||||
return grayKeyValue
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, logInfof func(format string, args ...interface{})) *config.GrayDeployment {
|
||||
for _, grayDeployment := range grayConfig.GrayDeployments {
|
||||
if !grayDeployment.Enabled {
|
||||
// 跳过Enabled=false
|
||||
continue
|
||||
}
|
||||
grayRule := GetRule(grayConfig.Rules, grayDeployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if ContainsValue(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
logInfof("frontendVersion: %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 := ExtractCookieValueByKey(cookieStr, grayRule.GrayTagKey)
|
||||
if ContainsValue(grayRule.GrayTagValue, grayTagValue) {
|
||||
logInfof("frontendVersion: %s, grayTag: %s=%s", grayDeployment.Version, grayRule.GrayTagKey, grayTagValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
}
|
||||
logInfof("frontendVersion: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetValueByCookie(t *testing.T) {
|
||||
func TestExtractCookieValueByKey(t *testing.T) {
|
||||
var tests = []struct {
|
||||
cookie, cookieKey, output string
|
||||
}{
|
||||
@@ -19,23 +19,85 @@ func TestGetValueByCookie(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
testName := test.cookie
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetValueByCookie(test.cookie, test.cookieKey)
|
||||
output := ExtractCookieValueByKey(test.cookie, test.cookieKey)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJsonCookie(t *testing.T) {
|
||||
// 测试首页Rewrite重写
|
||||
func TestIndexRewrite(t *testing.T) {
|
||||
matchRules := map[string]string{
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
userInfoStr, grayJsonKey, output string
|
||||
path, output string
|
||||
}{
|
||||
{"{%22password%22:%22$2a$10$YAvYjA6783YeCi44/M395udIZ4Ll2iyKkQCzePaYx5NNG/aIWgICG%22%2C%22username%22:%22%E8%B0%A2%E6%99%AE%E8%80%80%22%2C%22authorities%22:[]%2C%22accountNonExpired%22:true%2C%22accountNonLocked%22:true%2C%22credentialsNonExpired%22:true%2C%22enabledd%22:true%2C%22id%22:838925798835720200%2C%22mobile%22:%22%22%2C%22userCode%22:%22noah%22%2C%22userName%22:%22%E8%B0%A2%E6%99%AE%E8%80%80%22%2C%22orgId%22:10%2C%22ocId%22:87%2C%22userType%22:%22OWN%22%2C%22firstLogin%22:false%2C%22ownOrgId%22:null%2C%22clientCode%22:%22%22%2C%22clientType%22:null%2C%22country%22:%22UAE%22%2C%22isGuide%22:null%2C%22acctId%22:null%2C%22userToken%22:null%2C%22deviceId%22:%223a47fec00a59d140%22%2C%22ocCode%22:%2299990002%22%2C%22secondType%22:%22dtl%22%2C%22vendorCode%22:%2210000001%22%2C%22status%22:%22ACTIVE%22%2C%22isDelete%22:false%2C%22email%22:%22%22%2C%22deleteStatus%22:null%2C%22deleteRequestDate%22:null%2C%22wechatId%22:null%2C%22userMfaInfoDTO%22:{%22checkMfa%22:false%2C%22checkSuccess%22:false%2C%22mobile%22:null%2C%22email%22:null%2C%22wechatId%22:null%2C%22totpSecret%22:null}}",
|
||||
"userCode", "noah"},
|
||||
{"/app1/", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app123", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/index.html", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/index.jsp", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/xxx", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/xxxx", "/mfe/app1/v1.0.0/index.html"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.userInfoStr
|
||||
testName := test.path
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetBySubKey(test.userInfoStr, test.grayJsonKey)
|
||||
output := IndexRewrite(testName, "v1.0.0", matchRules)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixFileRewrite(t *testing.T) {
|
||||
matchRules := map[string]string{
|
||||
// 前缀匹配
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app2/": "/mfe/app1/{version}",
|
||||
"/app1/": "/mfe/app1/{version}",
|
||||
"/app1/prefix2": "/mfe/app1/{version}",
|
||||
"/mfe/app1": "/mfe/app1/{version}",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
path, output string
|
||||
}{
|
||||
{"/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/prefix2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/prefix2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/mfe/app1/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.path
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := PrefixFileRewrite(testName, "v1.0.0", matchRules)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIndexRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fetchMode string
|
||||
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},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testPath := test.p
|
||||
t.Run(testPath, func(t *testing.T) {
|
||||
output := IsIndexRequest(test.fetchMode, testPath)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user