Files
SamWaf/wafenginecore/wafwebcache/webcache.go
T
2025-05-26 16:18:42 +08:00

445 lines
14 KiB
Go

package wafwebcache
import (
"SamWaf/common/zlog"
"SamWaf/enums"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"SamWaf/utils"
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
// LoadWebDataFormCache 从缓存里面拿数据
func LoadWebDataFormCache(w http.ResponseWriter, r *http.Request, hostSafe *wafenginmodel.HostSafe, cacheConfig model.CacheConfig, weblog *innerbean.WebLog) *http.Response {
bodyHashKey := ""
if weblog.SrcByteBody != nil && len(weblog.SrcByteBody) > 0 {
bodyHash := sha256.Sum256(weblog.SrcByteBody)
bodyHashKey = hex.EncodeToString(bodyHash[:])
//重新赋值给weblog
weblog.BodyHash = bodyHashKey
}
requestMethod := strings.ToLower(r.Method)
requestURI := r.RequestURI
zlog.Debug(fmt.Sprintf("缓存键组成部分 (加载) - 方法: %s, URI: %s", requestMethod, requestURI))
key := utils.Md5String(requestMethod + requestURI)
if requestMethod == "post" || requestMethod == "put" {
zlog.Debug(fmt.Sprintf("缓存键组成部分 (加载) - BodyHash: %s (用于POST/PUT请求)", bodyHashKey))
key = utils.Md5String(requestMethod + requestURI + bodyHashKey)
}
zlog.Debug(fmt.Sprintf("尝试从缓存加载数据 URL: %s, 缓存键: %s, 主机代码: %s, 缓存位置: %s",
requestURI, key, hostSafe.Host.Code, cacheConfig.CacheLocation))
//规则匹配查询
_, err2 := checkCacheRule(hostSafe, cacheConfig, r)
if err2 != nil {
return nil
}
var data []byte = nil
if cacheConfig.CacheLocation == "memory" || cacheConfig.CacheLocation == "all" {
zlog.Debug(fmt.Sprintf("尝试从内存缓存加载 主机代码: %s, 缓存键: %s", hostSafe.Host.Code, key))
memoryData := loadFormMemory(hostSafe.Host.Code, key)
if memoryData == nil {
zlog.Debug(fmt.Sprintf("内存缓存未命中 主机代码: %s, 缓存键: %s", hostSafe.Host.Code, key))
} else {
zlog.Debug(fmt.Sprintf("内存缓存命中 主机代码: %s, 缓存键: %s, 数据大小: %d",
hostSafe.Host.Code, key, len(memoryData)))
data = memoryData
}
}
if data == nil && (cacheConfig.CacheLocation == "file" || cacheConfig.CacheLocation == "all") {
zlog.Debug(fmt.Sprintf("尝试从文件缓存加载 主机代码: %s, 缓存键: %s, 缓存目录: %s",
hostSafe.Host.Code, key, cacheConfig.CacheDir))
fileData := loadFormFile(hostSafe.Host.Code, key, cacheConfig.CacheDir)
if fileData == nil {
zlog.Debug(fmt.Sprintf("文件缓存未命中 主机代码: %s, 缓存键: %s", hostSafe.Host.Code, key))
} else {
zlog.Debug(fmt.Sprintf("文件缓存命中 主机代码: %s, 缓存键: %s, 数据大小: %d",
hostSafe.Host.Code, key, len(fileData)))
data = fileData
}
}
if data == nil {
zlog.Debug(fmt.Sprintf("缓存完全未命中 URL: %s, 缓存键: %s", r.RequestURI, key))
return nil
}
buf := bufio.NewReader(bytes.NewReader(data))
resp, err := http.ReadResponse(buf, nil)
if err != nil {
zlog.Error(fmt.Sprintf("解析缓存响应失败 URL: %s, 缓存键: %s, 错误: %v", r.RequestURI, key, err))
return nil
}
zlog.Debug(fmt.Sprintf("成功从缓存加载响应 URL: %s, 缓存键: %s, 状态码: %d, 响应头数量: %d",
r.RequestURI, key, resp.StatusCode, len(resp.Header)))
return resp
}
// StoreWebDataCache 缓存web数据到cache里面
func StoreWebDataCache(resp *http.Response, hostSafe *wafenginmodel.HostSafe, cacheConfig model.CacheConfig, weblog *innerbean.WebLog) {
// 检查HTTP响应状态码,只缓存2xx的响应
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
zlog.Debug(fmt.Sprintf("响应状态码 %d 不符合缓存条件 (2xx),不进行缓存 URL: %s",
resp.StatusCode, resp.Request.RequestURI))
return
}
requestMethod := strings.ToLower(resp.Request.Method)
requestURI := resp.Request.RequestURI
zlog.Debug(fmt.Sprintf("缓存键组成部分 - 方法: %s, URI: %s", requestMethod, requestURI))
key := utils.Md5String(requestMethod + requestURI)
if requestMethod == "post" || requestMethod == "put" {
zlog.Debug(fmt.Sprintf("缓存键组成部分 - BodyHash: %s (用于POST/PUT请求)", weblog.BodyHash))
key = utils.Md5String(requestMethod + requestURI + weblog.BodyHash)
}
zlog.Debug(fmt.Sprintf("尝试存储响应到缓存 URL: %s, 缓存键: %s, 主机代码: %s, 缓存位置: %s",
requestURI, key, hostSafe.Host.Code, cacheConfig.CacheLocation))
//规则匹配查询 获取缓存时间
timeout, err2 := checkCacheRule(hostSafe, cacheConfig, resp.Request)
if err2 != nil {
return
}
data, err := httputil.DumpResponse(resp, true)
if err != nil {
zlog.Error(fmt.Sprintf("序列化响应失败 URL: %s, 错误: %v", requestURI, err))
return
}
zlog.Debug(fmt.Sprintf("响应序列化成功 URL: %s, 数据大小: %d", requestURI, len(data)))
if cacheConfig.CacheLocation == "memory" || cacheConfig.CacheLocation == "all" {
zlog.Debug(fmt.Sprintf("存储到内存缓存 主机代码: %s, 缓存键: %s, 超时时间(秒): %d",
hostSafe.Host.Code, key, timeout))
storeMemory(hostSafe.Host.Code, key, data, timeout, cacheConfig.MaxMemorySizeMB)
}
if cacheConfig.CacheLocation == "file" || cacheConfig.CacheLocation == "all" {
zlog.Debug(fmt.Sprintf("存储到文件缓存 主机代码: %s, 缓存键: %s, 缓存目录: %s, 超时时间(秒): %d",
hostSafe.Host.Code, key, cacheConfig.CacheDir, timeout))
storeFile(hostSafe.Host.Code, key, data, cacheConfig.CacheDir, timeout, cacheConfig.MaxFileSizeMB)
}
zlog.Debug(fmt.Sprintf("响应缓存完成 URL: %s, 缓存键: %s", resp.Request.RequestURI, key))
}
func loadFormMemory(hostCode string, key string) []byte {
cacheKey := enums.CACHE_WEBFILE + hostCode + key
zlog.Debug(fmt.Sprintf("检查内存缓存键 缓存键: %s", cacheKey))
isKeyExist := global.GCACHE_WAFCACHE.IsKeyExist(cacheKey)
if isKeyExist {
zlog.Debug(fmt.Sprintf("内存缓存键存在 缓存键: %s", cacheKey))
data, err := global.GCACHE_WAFCACHE.GetBytes(cacheKey)
if err == nil {
zlog.Debug(fmt.Sprintf("成功获取内存缓存数据 缓存键: %s, 数据大小: %d", cacheKey, len(data)))
return data
}
zlog.Error(fmt.Sprintf("获取内存缓存数据失败 缓存键: %s, 错误: %v", cacheKey, err))
} else {
zlog.Debug(fmt.Sprintf("内存缓存键不存在 缓存键: %s", cacheKey))
}
return nil
}
func loadFormFile(hostCode string, key string, path string) []byte {
safePath := filepath.Join(path, hostCode)
// 获取目录中所有可能的缓存文件
pattern := filepath.Join(safePath, key+".*.cache")
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
zlog.Debug(fmt.Sprintf("文件缓存不存在 匹配模式: %s, 错误: %v", pattern, err))
return nil
}
// 当前时间戳
now := time.Now().Unix()
// 查找有效的缓存文件(未过期的)
var validFile string
var latestExpire int64
for _, match := range matches {
// 从文件名中提取过期时间戳
base := filepath.Base(match)
parts := strings.Split(base, ".")
if len(parts) < 3 {
continue
}
expireStr := parts[len(parts)-2]
expire, err := strconv.ParseInt(expireStr, 10, 64)
if err != nil {
continue
}
// 检查是否过期
if expire > now {
// 如果有多个有效文件,选择过期时间最晚的
if expire > latestExpire {
validFile = match
latestExpire = expire
}
} else {
// 文件已过期,可以选择删除
zlog.Debug(fmt.Sprintf("缓存文件已过期,准备删除: %s", match))
os.Remove(match)
}
}
if validFile == "" {
zlog.Debug(fmt.Sprintf("没有找到有效的缓存文件 键: %s", key))
return nil
}
zlog.Debug(fmt.Sprintf("找到有效的缓存文件: %s, 过期时间戳: %d", validFile, latestExpire))
data, err := ioutil.ReadFile(validFile)
if err != nil {
zlog.Error(fmt.Sprintf("读取文件缓存失败 文件路径: %s, 错误: %v", validFile, err))
return nil
}
zlog.Debug(fmt.Sprintf("成功读取文件缓存 文件路径: %s, 数据大小: %d", validFile, len(data)))
return data
}
func storeMemory(hostCode string, key string, data []byte, timeout int, maxSize float64) {
cacheKey := enums.CACHE_WEBFILE + hostCode + key
zlog.Debug(fmt.Sprintf("存储到内存缓存 缓存键: %s, 数据大小: %d", cacheKey, len(data)))
// 检查内存大小限制
dataSizeMB := float64(len(data)) / (1024 * 1024)
if maxSize > 0 && dataSizeMB > maxSize {
zlog.Warn(fmt.Sprintf("内存缓存大小超过限制 缓存键: %s, 数据大小: %.2fMB, 最大限制: %.2fMB",
cacheKey, dataSizeMB, maxSize))
return
}
if timeout > 0 {
zlog.Debug(fmt.Sprintf("设置内存缓存带超时 缓存键: %s, 超时时间(秒): %d", cacheKey, timeout))
global.GCACHE_WAFCACHE.SetWithTTlRenewTime(cacheKey, data, time.Duration(timeout)*time.Second)
} else {
zlog.Debug(fmt.Sprintf("设置内存缓存长期有效 缓存键: %s", cacheKey))
global.GCACHE_WAFCACHE.SetWithTTlRenewTime(cacheKey, data, time.Duration(87600)*time.Hour)
}
}
func storeFile(hostCode string, key string, data []byte, path string, timeout int, maxSize float64) {
safePath := filepath.Join(path, hostCode)
zlog.Debug(fmt.Sprintf("准备存储文件缓存 目录路径: %s, 缓存键: %s", safePath, key))
// 确保缓存目录存在
if _, err := os.Stat(safePath); os.IsNotExist(err) {
zlog.Debug(fmt.Sprintf("缓存目录不存在,创建目录 目录路径: %s", safePath))
err := os.MkdirAll(safePath, 0755)
if err != nil {
zlog.Error(fmt.Sprintf("创建缓存目录失败 目录路径: %s, 错误: %v", safePath, err))
return
}
}
// 计算过期时间戳
var expireTimestamp int64
if timeout > 0 {
expireTimestamp = time.Now().Unix() + int64(timeout)
} else {
// 如果没有设置超时,使用一个很大的值(10年)
expireTimestamp = time.Now().Unix() + 315360000
}
// 文件名中包含过期时间戳
filename := filepath.Join(safePath, fmt.Sprintf("%s.%d.cache", key, expireTimestamp))
zlog.Debug(fmt.Sprintf("写入文件缓存 文件路径: %s, 数据大小: %d, 过期时间戳: %d", filename, len(data), expireTimestamp))
// 检查文件大小限制
dataSizeMB := float64(len(data)) / (1024 * 1024)
if maxSize > 0 && dataSizeMB > maxSize {
zlog.Warn(fmt.Sprintf("文件缓存大小超过限制 文件路径: %s, 数据大小: %.2fMB, 最大限制: %.2fMB",
filename, dataSizeMB, maxSize))
return
}
err := ioutil.WriteFile(filename, data, 0644)
if err != nil {
zlog.Error(fmt.Sprintf("写入文件缓存失败 文件路径: %s, 错误: %v", filename, err))
return
}
zlog.Debug(fmt.Sprintf("文件缓存写入成功 文件路径: %s", filename))
}
func checkCacheRule(hostSafe *wafenginmodel.HostSafe, cacheConfig model.CacheConfig, r *http.Request) (int, error) {
//规则匹配查询
if hostSafe.CacheRule == nil || len(hostSafe.CacheRule) == 0 {
return 0, fmt.Errorf("没有配置缓存规则")
}
requestURI := r.URL.RequestURI()
requestMethod := r.Method
// 按优先级排序规则(优先级数字越大越优先)
sort.Slice(hostSafe.CacheRule, func(i, j int) bool {
return hostSafe.CacheRule[i].Priority > hostSafe.CacheRule[j].Priority
})
pureRequestUrl := r.URL.Path
// 循环检查每个规则
for _, rule := range hostSafe.CacheRule {
finalUrl := requestURI
if rule.ParamType == 1 {
finalUrl = pureRequestUrl
}
// 检查请求方法是否匹配
if !checkRequestMethod(requestMethod, rule.RequestMethod) {
continue
}
// 根据规则类型进行匹配
matched := false
switch rule.RuleType {
case 1: // 后缀匹配
matched = checkSuffixMatch(finalUrl, rule.RuleContent)
case 2: // 指定目录
matched = checkDirectoryMatch(finalUrl, rule.RuleContent)
case 3: // 指定文件
matched = checkExactFileMatch(finalUrl, rule.RuleContent)
}
if matched {
zlog.Debug(fmt.Sprintf("缓存规则匹配成功: %s, 缓存时间: %d秒", rule.RuleName, rule.CacheTime))
return rule.CacheTime, nil
}
}
// 没有匹配的规则
return 0, fmt.Errorf("没有匹配的缓存规则")
}
// 检查请求方法是否匹配
func checkRequestMethod(requestMethod, ruleMethods string) bool {
if ruleMethods == "" {
return true // 空字符串表示匹配所有方法
}
methods := strings.Split(ruleMethods, ";")
for _, method := range methods {
if strings.TrimSpace(method) == requestMethod {
return true
}
}
return false
}
// 检查后缀匹配
func checkSuffixMatch(path, suffixes string) bool {
if suffixes == "" {
return false
}
suffixList := strings.Split(suffixes, ";")
for _, suffix := range suffixList {
suffix = strings.TrimSpace(suffix)
if suffix != "" && strings.HasSuffix(strings.ToLower(path), strings.ToLower(suffix)) {
return true
}
}
return false
}
// 检查目录匹配
func checkDirectoryMatch(path, directories string) bool {
if directories == "" {
return false
}
dirList := strings.Split(directories, ";")
for _, dir := range dirList {
dir = strings.TrimSpace(dir)
if dir != "" && strings.HasPrefix(strings.ToLower(path), strings.ToLower(dir)) {
return true
}
}
return false
}
// 检查精确文件匹配
func checkExactFileMatch(path, files string) bool {
if files == "" {
return false
}
fileList := strings.Split(files, ";")
for _, file := range fileList {
file = strings.TrimSpace(file)
if file != "" && strings.ToLower(path) == strings.ToLower(file) {
return true
}
}
return false
}
// CleanExpiredCache 清理过期的缓存文件
func CleanExpiredCache(cacheDir string) {
zlog.Debug(fmt.Sprintf("开始清理过期缓存文件 目录: %s", cacheDir))
// 获取当前时间戳
now := time.Now().Unix()
// 遍历缓存目录
err := filepath.Walk(cacheDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 只处理文件
if !info.IsDir() {
// 检查是否是缓存文件
if strings.HasSuffix(path, ".cache") {
// 从文件名中提取过期时间戳
base := filepath.Base(path)
parts := strings.Split(base, ".")
if len(parts) >= 3 {
expireStr := parts[len(parts)-2]
expire, err := strconv.ParseInt(expireStr, 10, 64)
if err == nil && expire <= now {
// 文件已过期,删除
zlog.Debug(fmt.Sprintf("删除过期缓存文件: %s", path))
os.Remove(path)
}
}
}
}
return nil
})
if err != nil {
zlog.Error(fmt.Sprintf("清理过期缓存文件失败: %v", err))
} else {
zlog.Debug(fmt.Sprintf("清理过期缓存文件完成"))
}
}