package wafenginecore
import (
"SamWaf/common/domaintool"
"SamWaf/common/uuid"
"SamWaf/common/zlog"
"SamWaf/customtype"
"SamWaf/enums"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/detection"
"SamWaf/model/wafenginmodel"
"SamWaf/utils"
"SamWaf/wafenginecore/loadbalance"
"SamWaf/wafenginecore/wafhttpcore"
"SamWaf/wafenginecore/wafhttpserver"
"SamWaf/wafenginecore/wafwebcache"
"SamWaf/wafproxy"
"SamWaf/webplugin"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/pires/go-proxyproto"
goahocorasick "github.com/samwafgo/ahocorasick"
"go.uber.org/zap"
"golang.org/x/time/rate"
)
type WafEngine struct {
//主机情况(key:主机名+":"+端口,value : hostsafe信息里面有规则,ip信息等)
HostTarget map[string]*wafenginmodel.HostSafe
//主机和code的关系(key:主机code,value:主机名+":"+端口)
HostCode map[string]string
//主机域名和配置防护关系 (key:主机域名,value:主机的hostCode)
HostTargetNoPort map[string]string
//更多域名和配置防护关系 (key:主机域名,value:主机的hostCode) 一个主机绑定很多域名的情况
HostTargetMoreDomain map[string]string
//服务在线情况(key:端口,value :服务情况)
ServerOnline *wafenginmodel.SafeServerMap
AllCertificate AllCertificate //所有证书
EngineCurrentStatus int // 当前waf引擎状态
//敏感词管理
Sensitive []model.Sensitive //敏感词
SensitiveManager *goahocorasick.Machine
// 敏感词检测方向映射,用于快速判断是否需要存在
SensitiveDirectionMap map[string]bool // key: "in"/"out"/"all", value: 是否存在
TransportPool map[string]*http.Transport // 添加Transport缓存池
TransportMux sync.RWMutex // 保护Transport池的读写锁
}
func (waf *WafEngine) Error() string {
fs := "HTTP: %d, HostCode: %d, Message: %s"
zlog.Error(fmt.Sprintf(fs))
return fmt.Sprintf(fs)
}
func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
innerLogName := "WafEngine ServeHTTP"
global.IncrementQPS() // 使用统一的QPS增量函数
port := ""
host := r.Host
if !strings.Contains(host, ":") {
// 检查请求是否使用了HTTPS
if r.TLS != nil {
// 请求使用了HTTPS
host = host + ":443"
} else {
// 请求使用了HTTP
host = host + ":80"
}
}
// 从 host 字符串中提取端口
if strings.Contains(host, ":") {
parts := strings.Split(host, ":")
if len(parts) == 2 {
port = parts[1]
}
}
defer func() {
e := recover()
if e != nil {
// 检查是否是连接断开相关的错误
if e == http.ErrAbortHandler {
fmt.Printf("Client connection aborted: %s\n", r.URL.Path)
return
}
// 其他类型的panic才打印详细堆栈
fmt.Printf("Unexpected error in request handler: %v\n", e)
debug.PrintStack()
}
}()
targetCode := ""
//检测是否是不检测端口的情况
if targetHost, ok := waf.HostTargetNoPort[utils.GetPureDomain(host)]; ok {
host = targetHost
}
findHost := false
target, ok := waf.HostTarget[host]
if !ok {
// 看看是不是泛域名情况
target, ok = waf.HostTarget[domaintool.MaskSubdomain(host)]
if ok {
findHost = true
targetCode = target.Host.Code
}
} else {
targetCode = target.Host.Code
findHost = true
}
if !findHost {
// 绑定更多域名的信息
targetCode, ok = waf.HostTargetMoreDomain[host]
if ok {
findHost = true
} else {
// 看看是不是泛域名情况
targetCode, ok = waf.HostTargetMoreDomain[domaintool.MaskSubdomain(host)]
if ok {
findHost = true
}
}
}
//检测是否是宽松端口且是不指定域名的情况
if targetHost, ok := waf.HostTargetNoPort["*"]; ok {
host = targetHost
//检测是否存在不指定域名的情况
target, ok = waf.HostTarget[host]
if ok {
findHost = true
targetCode = target.Host.Code
zlog.Debug(fmt.Sprintf("%s %s", innerLogName, "触发*逻辑和来源port"))
}
} else {
//检测是否存在不指定域名的情况
target, ok = waf.HostTarget["*:"+port]
if ok {
findHost = true
targetCode = target.Host.Code
zlog.Debug(fmt.Sprintf("%s %s", innerLogName, "触发*逻辑"))
}
}
// 检查域名是否已经注册
if findHost == true {
hostTarget := waf.HostTarget[waf.HostCode[targetCode]]
hostCode := hostTarget.Host.Code
incrementMonitor(hostCode)
defer decrementMonitor(hostCode)
//检测网站是否已关闭
if hostTarget.Host.START_STATUS == 1 {
resBytes := []byte("
网站已关闭当前访问网站已关闭
")
w.WriteHeader(http.StatusServiceUnavailable)
if _, writeErr := w.Write(resBytes); writeErr != nil {
zlog.Debug("write fail:", zap.Any("err", writeErr))
return
}
return
}
// 取出客户IP
ipErr, clientIP, clientPort := waf.getClientIP(r, strings.Split(global.GCONFIG_RECORD_PROXY_HEADER, ",")...)
if ipErr != nil {
zlog.Error("get client error", ipErr.Error())
return
}
_, ok := waf.ServerOnline.Get(hostTarget.Host.Remote_port)
//检测如果访问IP和远程IP是同一个IP,且远程端口在本地Server已存在则显示配置错误
if clientIP == hostTarget.Host.Remote_ip && ok == true {
resBytes := []byte("500: 配置有误" + host + " 当前IP和访问远端IP一样,且端口也一样,会造成循环问题")
_, err := w.Write(resBytes)
if err != nil {
zlog.Debug("write fail:", zap.Any("", err))
return
}
return
}
// 静态站点服务检查
staticSiteConfig := InitDefaultStaticSiteConfig()
err := json.Unmarshal([]byte(hostTarget.Host.StaticSiteJSON), &staticSiteConfig)
if err != nil {
zlog.Debug("解析static site json失败")
}
//检测cache
cacheConfig := model.CacheConfig{
IsEnableCache: 0,
CacheLocation: "",
CacheDir: "",
MaxFileSizeMB: 0,
MaxMemorySizeMB: 0,
}
err = json.Unmarshal([]byte(hostTarget.Host.CacheJSON), &cacheConfig)
if err != nil {
zlog.Debug("解析cache json失败")
}
// 获取请求报文的内容长度
contentLength := r.ContentLength
var bodyByte []byte
// 拷贝一份request的Body ,控制不记录大文件的情况
if r.Body != nil && r.Body != http.NoBody && (contentLength < (global.GCONFIG_RECORD_MAX_BODY_LENGTH) || cacheConfig.IsEnableCache == 1) {
// 检查请求是否包含Content-Encoding
if r.Header.Get("Content-Encoding") != "" {
// 处理压缩的请求体
decompressedBytes, decompressErr := waf.decompressRequestContent(r)
if decompressErr != nil {
zlog.Warn("请求Content-Encoding处理失败: %v", decompressErr)
// 如果解压失败,使用原始内容
bodyByte, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyByte))
} else {
bodyByte = decompressedBytes
// 请求体已经在decompressRequestContent中更新
}
} else {
// 没有压缩,正常处理
bodyByte, _ = io.ReadAll(r.Body)
// 把刚刚读出来的再写进去,不然后面解析表单数据就解析不到了
r.Body = io.NopCloser(bytes.NewBuffer(bodyByte))
}
}
cookies, _ := json.Marshal(r.Cookies())
hlen := 0
for key, values := range r.Header {
for _, value := range values {
hlen += len(key)
hlen += len(": ")
hlen += len(value)
hlen += len("\r\n")
}
}
var Header strings.Builder
Header.Grow(hlen)
for key, values := range r.Header {
for _, value := range values {
Header.WriteString(key)
Header.WriteString(": ")
Header.WriteString(value)
Header.WriteString("\r\n")
}
}
header := Header.String()
region := utils.GetCountry(clientIP)
currentDay, _ := strconv.Atoi(time.Now().Format("20060102"))
//URL 解码
deRawQueryUrl := wafhttpcore.WafHttpCoreUrlEncode(r.URL.RawQuery, 10)
datetimeNow := time.Now()
weblogbean := innerbean.WebLog{
HOST: r.Host,
URL: r.RequestURI,
RawQuery: deRawQueryUrl,
REFERER: r.Referer(),
USER_AGENT: r.UserAgent(),
METHOD: r.Method,
HEADER: header,
COUNTRY: region[0],
PROVINCE: region[2],
CITY: region[3],
SRC_IP: clientIP,
SRC_PORT: clientPort,
CREATE_TIME: datetimeNow.Format("2006-01-02 15:04:05"),
UNIX_ADD_TIME: datetimeNow.UnixNano() / 1e6,
CONTENT_LENGTH: contentLength,
COOKIES: string(cookies),
BODY: string(bodyByte),
REQ_UUID: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
HOST_CODE: hostCode,
TenantId: global.GWAF_TENANT_ID,
RULE: "",
ACTION: "通过",
Day: currentDay,
POST_FORM: r.PostForm.Encode(),
TASK_FLAG: -1,
RISK_LEVEL: 0, //危险等级
GUEST_IDENTIFICATION: "正常访客", //访客身份识别
TimeSpent: 0,
NetSrcIp: utils.GetSourceClientIP(r.RemoteAddr),
SrcByteBody: bodyByte,
WebLogVersion: global.GWEBLOG_VERSION,
Scheme: r.Proto,
SrcURL: []byte(r.RequestURI),
}
// 检查是否为WebSocket升级请求
if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
if r.TLS != nil {
weblogbean.HOST = "wss://" + weblogbean.HOST
} else {
weblogbean.HOST = "ws://" + weblogbean.HOST
}
} else if r.TLS != nil {
weblogbean.HOST = "https://" + weblogbean.HOST
} else {
weblogbean.HOST = "http://" + weblogbean.HOST
}
formValues := url.Values{}
if strings.Contains(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") {
// 解码 x-www-form-urlencoded 数据
formValuest, err := url.ParseQuery(weblogbean.BODY)
if err == nil {
formValues = formValuest
} else {
fmt.Println("解码失败:", err)
fmt.Println("解码失败:", weblogbean.BODY)
}
}
// 检测是否已经被CC封禁
ccCacheKey := enums.CACHE_CCVISITBAN_PRE + weblogbean.NetSrcIp
if global.GCACHE_WAFCACHE.IsKeyExist(ccCacheKey) {
// 使用新的IP封禁消息格式
regionStr := strings.Join(region, ",")
serverName := global.GWAF_CUSTOM_SERVER_NAME
if serverName == "" {
serverName = "未命名服务器"
}
global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.IPBanMessageInfo{
BaseMessageInfo: innerbean.BaseMessageInfo{
OperaType: "CC封禁提醒",
Server: serverName,
},
Ip: weblogbean.NetSrcIp + " (" + regionStr + ")",
Reason: "CC攻击,访问频次过高",
Duration: 0, // CC封禁时长由配置决定
Time: time.Now().Format("2006-01-02 15:04:05"),
})
EchoErrorInfo(w, r, &weblogbean, "", "当前IP由于访问频次太高暂时无法访问", hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], false)
return
}
if host == hostTarget.Host.Host+":80" && !strings.HasPrefix(weblogbean.URL, global.GSSL_HTTP_CHANGLE_PATH) && hostTarget.Host.AutoJumpHTTPS == 1 && hostTarget.Host.Ssl == 1 {
domainJump := ""
if strings.Contains(host, ":") {
partsJump := strings.Split(host, ":")
if len(partsJump) == 2 {
domainJump = partsJump[0]
}
}
// 重定向到 HTTPS 版本的 URL
targetHttpsUrl := fmt.Sprintf("%s%s%s", "https://", domainJump, r.URL.Path)
// 只有在非标准端口时才显示端口号(443是标准端口)
if hostTarget.Host.Port != 443 {
targetHttpsUrl = fmt.Sprintf("%s%s:%d%s", "https://", domainJump, hostTarget.Host.Port, r.URL.Path)
}
if r.URL.RawQuery != "" {
targetHttpsUrl += "?" + r.URL.RawQuery
}
zlog.Info(innerLogName, "jump https", targetHttpsUrl)
http.Redirect(w, r, targetHttpsUrl, int(global.GCONFIG_RECORD_REDIRECT_HTTPS_CODE))
return
}
r.Header.Add("waf_req_uuid", weblogbean.REQ_UUID)
if hostTarget.Host.GUARD_STATUS == 1 {
//一系列检测逻辑
handleBlock := func(checkFunc func(*http.Request, *innerbean.WebLog, url.Values, *wafenginmodel.HostSafe, *wafenginmodel.HostSafe) detection.Result) bool {
detectionResult := checkFunc(r, &weblogbean, formValues, hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]])
if detectionResult.IsBlock {
if hostTarget.Host.LogOnlyMode == 1 {
// 仅记录模式:记录攻击日志但不阻断请求
weblogbean.LogOnlyMode = 1
weblogbean.RULE = detectionResult.Title
return false
} else {
decrementMonitor(hostCode)
EchoErrorInfo(w, r, &weblogbean, detectionResult.Title, detectionResult.Content, hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true)
return true
}
}
return false
}
globalHostSafe := waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]]
// 检测白名单开始
detectionWhiteResult := detection.Result{JumpGuardResult: false}
checkFunctions := []func(*http.Request, *innerbean.WebLog, url.Values, *wafenginmodel.HostSafe, *wafenginmodel.HostSafe) detection.Result{
waf.CheckAllowIP,
waf.CheckAllowURL,
waf.CheckAllowCallBackIP,
}
for _, checkFunc := range checkFunctions {
detectionWhiteResult = checkFunc(r, &weblogbean, formValues, hostTarget, globalHostSafe)
if detectionWhiteResult.JumpGuardResult == true {
break
}
}
//检测白名单结束
if detectionWhiteResult.JumpGuardResult == false {
if handleBlock(waf.CheckDenyIP) {
return
}
if handleBlock(waf.CheckDenyURL) {
return
}
hostDefense := model.ParseHostsDefense(hostTarget.Host.DEFENSE_JSON)
//检测爬虫bot
if hostDefense.DEFENSE_BOT == 1 {
if handleBlock(waf.CheckBot) {
return
}
}
//检测sqli
if hostDefense.DEFENSE_SQLI == 1 {
if handleBlock(waf.CheckSql) {
return
}
}
//检测xss
if hostDefense.DEFENSE_XSS == 1 {
if handleBlock(waf.CheckXss) {
return
}
}
//检测扫描工具
if hostDefense.DEFENSE_SCAN == 1 {
if handleBlock(waf.CheckSan) {
return
}
}
//检测RCE
if hostDefense.DEFENSE_RCE == 1 {
if handleBlock(waf.CheckRce) {
return
}
}
//目录穿越检测
if hostDefense.DEFENSE_DIR_TRAVERSAL == 1 {
if handleBlock(waf.CheckDirTraversal) {
return
}
}
//检测CC
if handleBlock(waf.CheckCC) {
return
}
//规则判断
if handleBlock(waf.CheckRule) {
return
}
//检测敏感词
if hostDefense.DEFENSE_SENSITIVE == 1 {
if handleBlock(waf.CheckSensitive) {
return
}
}
//检测OWASP
if handleBlock(waf.CheckOwasp) {
return
}
// 添加防盗链检查
if handleBlock(waf.CheckAntiLeech) {
return
}
// 验证码检测
captchaConfig := model.ParseCaptchaConfig(hostTarget.Host.CaptchaJSON)
if captchaConfig.IsEnableCaptcha == 1 {
if !waf.checkCaptchaToken(r, weblogbean, captchaConfig) {
// 检查当前URL是否在排除列表中
currentURL := strings.ToLower(r.URL.Path)
isExcluded := false
if len(captchaConfig.ExcludeURLs) > 0 {
// 将换行分隔的URL列表拆分为数组
excludeURLs := strings.Split(captchaConfig.ExcludeURLs, "\n")
for _, excludeURL := range excludeURLs {
// 去除可能的空白字符并转为小写
excludeURL = strings.TrimSpace(strings.ToLower(excludeURL))
if excludeURL != "" && strings.HasPrefix(currentURL, excludeURL) {
isExcluded = true
break
}
}
}
if !isExcluded {
waf.handleCaptchaRequest(w, r, &weblogbean, captchaConfig)
return
}
}
}
}
}
if cacheConfig.IsEnableCache == 1 && !strings.HasPrefix(weblogbean.URL, global.GSSL_HTTP_CHANGLE_PATH) {
cacheResp := wafwebcache.LoadWebDataFormCache(w, r, hostTarget, cacheConfig, &weblogbean)
if cacheResp != nil {
// 将缓存的响应写回给客户端
for k, v := range cacheResp.Header {
for _, val := range v {
w.Header().Add(k, val)
}
}
w.Header().Add("X-Cache", "HIT")
w.WriteHeader(cacheResp.StatusCode)
// 读取并写入响应体
if cacheResp.Body != nil {
body, _ := ioutil.ReadAll(cacheResp.Body)
cacheResp.Body.Close()
w.Write(body)
// 重新创建一个body供后续使用
cacheResp.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
// 设置响应并返回
r.Response = cacheResp
return
}
}
// 如果开启了静态站点服务且请求路径匹配前缀
if staticSiteConfig.IsEnableStaticSite == 1 {
if strings.HasPrefix(weblogbean.URL, global.GSSL_HTTP_CHANGLE_PATH) {
//Challenge /.well-known/acme-challenge/2NKiiETgQdPmmjlM88mH5uo6jM98PrgWwsDslaN8
urls := strings.Split(weblogbean.URL, "/")
if len(urls) == 4 {
challengeFile := urls[3]
//检测challengeFile是否合法
if !utils.IsValidChallengeFile(challengeFile) {
return
}
//当前路径 data/vhost/domain code 变量下
// 需要读取的文件路径
filePath := utils.GetCurrentDir() + "/data/vhost/" + weblogbean.HOST_CODE + "/.well-known/acme-challenge/" + challengeFile
// 调用读取文件的函数
content, err := utils.ReadFile(filePath)
if err != nil {
zlog.Error("Error reading file: %v", err.Error())
return
}
if content != "" {
// 创建新的Response对象
r.Response = &http.Response{
StatusCode: http.StatusOK,
Status: http.StatusText(http.StatusOK),
Body: io.NopCloser(bytes.NewBuffer([]byte(content))),
ContentLength: int64(len(content)),
Header: make(http.Header),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
}
r.Response.Header.Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
// 直接写入响应到客户端
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
w.WriteHeader(http.StatusOK)
w.Write([]byte(content))
weblogbean.ACTION = "放行"
weblogbean.STATUS = r.Response.Status
weblogbean.STATUS_CODE = r.Response.StatusCode
weblogbean.TASK_FLAG = 1
global.GQEQUE_LOG_DB.Enqueue(&weblogbean)
return
}
}
} else {
waf.serveStaticFile(w, r, staticSiteConfig, &weblogbean, hostTarget)
return
}
}
//如果正常的流量不记录请求原始包
if global.GCONFIG_RECORD_ALL_SRC_BYTE_INFO == 0 {
weblogbean.SrcByteBody = nil
}
//基本验证是否开关是否开启
if hostTarget.Host.IsEnableHttpAuthBase == 1 {
bHttpAuthBaseResult, sHttpAuthBaseResult := waf.DoHttpAuthBase(hostTarget, w, r)
if bHttpAuthBaseResult == true {
// 记录日志
weblogbean.RES_BODY = sHttpAuthBaseResult
weblogbean.ACTION = "禁止"
global.GQEQUE_LOG_DB.Enqueue(&weblogbean)
return
}
}
// 日志保存时候也是脱敏保存防止,数据库密码被破解,遭到敏感信息遭到泄露
if weblogbean.BODY != "" {
weblogbean.BODY = utils.DeSenTextByCustomMark(enums.DLP_MARK_RULE_LoginSensitiveInfoMaskRule, weblogbean.BODY)
}
remoteUrl, err := url.Parse(hostTarget.TargetHost)
if err != nil {
zlog.Debug("target parse fail:", zap.Any("", err))
return
}
// 记录前置校验耗时
weblogbean.PreCheckCost = time.Now().UnixNano()/1e6 - weblogbean.UNIX_ADD_TIME
// 在请求上下文中存储自定义数据
ctx := context.WithValue(r.Context(), "waf_context", innerbean.WafHttpContextData{
Weblog: &weblogbean,
HostCode: hostCode,
})
// 代理请求
waf.ProxyHTTP(w, r, host, remoteUrl, clientIP, ctx, &weblogbean, hostTarget)
return
} else {
w.WriteHeader(403)
resBytes := []byte("403: Host forbidden " + host)
_, err := w.Write(resBytes)
if err != nil {
zlog.Debug("write fail:", zap.Any("", err))
return
}
// 获取请求报文的内容长度
contentLength := r.ContentLength
//server_online[8081].Svr.Close()
var bodyByte []byte
// 拷贝一份request的Body ,控制不记录大文件的情况 ,先写死的
if r.Body != nil && r.Body != http.NoBody && contentLength < (global.GCONFIG_RECORD_MAX_BODY_LENGTH) {
bodyByte, _ = io.ReadAll(r.Body)
// 把刚刚读出来的再写进去,不然后面解析表单数据就解析不到了
r.Body = io.NopCloser(bytes.NewBuffer(bodyByte))
}
cookies, _ := json.Marshal(r.Cookies())
header, _ := json.Marshal(r.Header)
// 取出客户IP
ipErr, clientIP, clientPort := waf.getClientIP(r, strings.Split(global.GCONFIG_RECORD_PROXY_HEADER, ",")...)
if ipErr != nil {
zlog.Error("get client error", ipErr.Error())
return
}
region := utils.GetCountry(clientIP)
datetimeNow := time.Now()
currentDay, _ := strconv.Atoi(datetimeNow.Format("20060102"))
weblogbean := innerbean.WebLog{
HOST: r.Host,
URL: r.RequestURI,
REFERER: r.Referer(),
USER_AGENT: r.UserAgent(),
METHOD: r.Method,
HEADER: string(header),
COUNTRY: region[0],
PROVINCE: region[2],
CITY: region[3],
SRC_IP: clientIP,
SRC_PORT: clientPort,
CREATE_TIME: datetimeNow.Format("2006-01-02 15:04:05"),
UNIX_ADD_TIME: datetimeNow.UnixNano() / 1e6,
CONTENT_LENGTH: contentLength,
COOKIES: string(cookies),
BODY: string(bodyByte),
REQ_UUID: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
HOST_CODE: "",
TenantId: global.GWAF_TENANT_ID,
RULE: "",
ACTION: "通过",
Day: currentDay,
STATUS: "禁止访问",
STATUS_CODE: 403,
TASK_FLAG: 1,
RISK_LEVEL: 1, //危险等级
GUEST_IDENTIFICATION: "未解析域名", //访客身份识别
TimeSpent: 0,
NetSrcIp: utils.GetSourceClientIP(r.RemoteAddr),
WebLogVersion: global.GWEBLOG_VERSION,
Scheme: r.Proto,
SrcURL: []byte(r.RequestURI),
}
//记录响应body
weblogbean.RES_BODY = string(resBytes)
weblogbean.ACTION = "禁止"
global.GQEQUE_LOG_DB.Enqueue(&weblogbean)
}
}
func (waf *WafEngine) getClientIP(r *http.Request, headers ...string) (error, string, string) {
for _, header := range headers {
ip := r.Header.Get(header)
if ip != "" {
// 处理可能的多个 IP 地址,以逗号分隔,取第一个有效的
ips := strings.Split(ip, ",")
trimmedIP := strings.TrimSpace(ips[0])
if utils.IsValidIPv4(trimmedIP) {
return nil, trimmedIP, "0"
}
if utils.IsValidIPv6(ip) {
return nil, ip, "0"
}
return errors.New("invalid IPv4 address from header: " + header + " value:" + ip), "", ""
}
}
// 如果没有找到,使用 r.RemoteAddr
ip, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return err, "", ""
}
// 验证 IPv4 地址
if utils.IsValidIPv4(ip) {
return nil, ip, port
}
// 如果是 IPv6 地址,进一步检查是否是有效的 IPv6 地址
if strings.Contains(ip, ":") {
// 如果是 IPv6 地址,进一步检查是否是有效的 IPv6 地址
if strings.Contains(ip, ":") {
if utils.IsValidIPv6(ip) {
return nil, ip, port
} else {
return errors.New("invalid IPv6 address from RemoteAddr: " + ip), "", ""
}
}
}
return errors.New("invalid IP address (not IPv4 or IPv6): " + ip), "", ""
}
func (waf *WafEngine) errorResponse() func(http.ResponseWriter, *http.Request, error) {
return func(w http.ResponseWriter, req *http.Request, err error) {
if wafHttpContext, ok := req.Context().Value("waf_context").(innerbean.WafHttpContextData); ok {
weblogReq := wafHttpContext.Weblog
requestInfo := fmt.Sprintf("Method: %s \r\nURL: %s \r\nHeaders: %v", req.Method, req.URL.String(), req.Header)
zlog.Error("服务不可用 response:", zap.Any("err", err.Error()), zap.String("request_info", requestInfo))
resBytes := []byte("服务不可用服务不可用
")
//记录响应Header信息
resHeader := ""
if req.Response != nil {
for key, values := range req.Response.Header {
for _, value := range values {
resHeader += key + ": " + value + "\r\n"
}
}
}
weblogReq.ResHeader = resHeader
weblogReq.ACTION = "放行"
weblogReq.STATUS = "Service Unavailable"
weblogReq.STATUS_CODE = 503
weblogReq.RES_BODY = fmt.Sprintf("请求相关信息:%v \r\n 错误信息:%v \r\n", requestInfo, err.Error())
weblogReq.TASK_FLAG = 1
global.GQEQUE_LOG_DB.Enqueue(weblogReq)
w.WriteHeader(http.StatusServiceUnavailable)
if _, writeErr := w.Write(resBytes); writeErr != nil {
zlog.Debug("write fail:", zap.Any("err", writeErr))
return
}
}
return
}
}
func (waf *WafEngine) modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
resp.Header.Set("X-Xss-Protection", "1; mode=block")
if global.GCONFIG_RECORD_HIDE_SERVER_HEADER == 1 {
resp.Header.Del("Server")
resp.Header.Del("X-Powered-By")
}
r := resp.Request
// 检查是否为WebSocket协议切换
if resp.StatusCode == http.StatusSwitchingProtocols {
if wafHttpContext, ok := r.Context().Value("waf_context").(innerbean.WafHttpContextData); ok {
weblogfrist := wafHttpContext.Weblog
host := waf.HostCode[wafHttpContext.HostCode]
weblogfrist.ACTION = "放行"
weblogfrist.STATUS = resp.Status
weblogfrist.STATUS_CODE = resp.StatusCode
resHeader := ""
for key, values := range resp.Header {
for _, value := range values {
resHeader += key + ": " + value + "\r\n"
}
}
weblogfrist.ResHeader = resHeader
datetimeNow := time.Now()
weblogfrist.TimeSpent = datetimeNow.UnixNano()/1e6 - weblogfrist.UNIX_ADD_TIME
weblogfrist.TASK_FLAG = 1
// 记录日志
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
// 检查每一行
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
// 对于WebSocket连接,不修改响应体,直接返回
return nil
}
if wafHttpContext, ok := r.Context().Value("waf_context").(innerbean.WafHttpContextData); ok {
backendCheckStart := time.Now().UnixNano() / 1e6
weblogfrist := wafHttpContext.Weblog
host := waf.HostCode[wafHttpContext.HostCode]
weblogfrist.ACTION = "放行"
weblogfrist.STATUS = resp.Status
weblogfrist.STATUS_CODE = resp.StatusCode
//返回内容的类型
respContentType := strings.ToLower(resp.Header.Get("Content-Type"))
respContentType = strings.Replace(respContentType, "; charset=utf-8", "", -1)
respContentType = strings.Replace(respContentType, "; charset=gbk", "", -1)
//记录静态日志
isStaticAssist := utils.IsStaticAssist(resp, respContentType)
// 检查是否为流式内容
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
isStreamContent := strings.Contains(contentType, "text/event-stream") ||
strings.Contains(contentType, "application/stream+json")
//记录响应body
if !isStaticAssist && resp.Body != nil && resp.Body != http.NoBody {
// 如果是流式内容,使用流式处理器
if isStreamContent {
// 创建流式处理器包装原始响应体
streamProcessor := waf.createStreamProcessor(resp.Body, wafHttpContext, host)
resp.Body = io.NopCloser(streamProcessor)
// 对于流式内容,跳过后续的常规处理逻辑
// 但仍然记录基本的访问日志
weblogfrist.ACTION = "放行"
weblogfrist.STATUS = resp.Status
weblogfrist.STATUS_CODE = resp.StatusCode
weblogfrist.TASK_FLAG = 1
// 记录响应Header信息
resHeader := ""
for key, values := range resp.Header {
for _, value := range values {
resHeader += key + ": " + value + "\r\n"
}
}
weblogfrist.ResHeader = resHeader
datetimeNow := time.Now()
weblogfrist.TimeSpent = datetimeNow.UnixNano()/1e6 - weblogfrist.UNIX_ADD_TIME
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart //响应数据处理时间
// 记录流式访问日志
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
return nil
}
ldpFlag := false
// 将请求URL转为小写,用于不区分大小写的比较
lowerRequestURI := strings.ToLower(resp.Request.RequestURI)
//隐私保护(局部)
for i := 0; i < len(waf.HostTarget[host].LdpUrlLists); i++ {
// 将规则URL也转为小写
lowerRuleURL := strings.ToLower(waf.HostTarget[host].LdpUrlLists[i].Url)
if (waf.HostTarget[host].LdpUrlLists[i].CompareType == "等于" && lowerRuleURL == lowerRequestURI) ||
(waf.HostTarget[host].LdpUrlLists[i].CompareType == "前缀匹配" && strings.HasPrefix(lowerRequestURI, lowerRuleURL)) ||
(waf.HostTarget[host].LdpUrlLists[i].CompareType == "后缀匹配" && strings.HasSuffix(lowerRequestURI, lowerRuleURL)) ||
(waf.HostTarget[host].LdpUrlLists[i].CompareType == "包含匹配" && strings.Contains(lowerRequestURI, lowerRuleURL)) {
ldpFlag = true
break
}
}
//隐私保护(全局)
for i := 0; i < len(waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].LdpUrlLists); i++ {
// 将全局规则URL也转为小写
lowerGlobalRuleURL := strings.ToLower(waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].LdpUrlLists[i].Url)
if (waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].LdpUrlLists[i].CompareType == "等于" && lowerGlobalRuleURL == lowerRequestURI) ||
(waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].LdpUrlLists[i].CompareType == "前缀匹配" && strings.HasPrefix(lowerRequestURI, lowerGlobalRuleURL)) ||
(waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].LdpUrlLists[i].CompareType == "后缀匹配" && strings.HasSuffix(lowerRequestURI, lowerGlobalRuleURL)) ||
(waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].LdpUrlLists[i].CompareType == "包含匹配" && strings.Contains(lowerRequestURI, lowerGlobalRuleURL)) {
ldpFlag = true
break
}
}
if ldpFlag == true {
orgContentBytes, charsetName, responseEncodingError := waf.getOrgContent(resp, isStaticAssist, waf.HostTarget[host].Host.DefaultEncoding)
if responseEncodingError == nil {
newPayload := []byte("" + utils.DeSenText(string(orgContentBytes)))
finalCompressBytes, _ := waf.compressContent(resp, isStaticAssist, newPayload, charsetName)
resp.Body = io.NopCloser(bytes.NewBuffer(finalCompressBytes))
// head 修改追加内容
resp.ContentLength = int64(len(finalCompressBytes))
resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(finalCompressBytes)), 10))
} else {
resp.Body = io.NopCloser(bytes.NewBuffer(orgContentBytes))
zlog.Warn(fmt.Sprintf("识别响应内容编码失败,隐私防护不可用 %v,请求URL: %s ,可以在主机其他设置里面设置默认编码", responseEncodingError, r.URL.String()))
}
}
//编码转换,自动检测网页编码 resp *http.Response
orgContentBytes, charsetName, responseEncodingError := waf.getOrgContent(resp, isStaticAssist, waf.HostTarget[host].Host.DefaultEncoding)
if responseEncodingError == nil {
if global.GCONFIG_RECORD_RESP == 1 {
if resp.ContentLength < global.GCONFIG_RECORD_MAX_RES_BODY_LENGTH {
weblogfrist.RES_BODY = string(orgContentBytes)
weblogfrist.SrcByteResBody = orgContentBytes
}
}
//处理敏感词
if waf.CheckResponseSensitive() {
matchBodyResult := waf.SensitiveManager.MultiPatternSearch([]rune(string(orgContentBytes)), false)
if len(matchBodyResult) > 0 {
sensitive := matchBodyResult[0].CustomData.(model.Sensitive)
if sensitive.CheckDirection != "in" {
weblogfrist.RISK_LEVEL = 1
if sensitive.Action == "deny" {
if waf.HostTarget[host].Host.LogOnlyMode == 1 {
// 仅记录模式:记录攻击日志但不阻断请求
weblogfrist.LogOnlyMode = 1
weblogfrist.GUEST_IDENTIFICATION = "触发敏感词"
weblogfrist.RULE = "敏感词检测:" + string(matchBodyResult[0].Word)
} else {
EchoResponseErrorInfo(resp, weblogfrist, "敏感词检测:"+string(matchBodyResult[0].Word), "敏感词内容", waf.HostTarget[host], waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true)
return nil
}
} else {
words := processSensitiveWords(matchBodyResult, "in")
weblogfrist.GUEST_IDENTIFICATION = "触发敏感词"
weblogfrist.RULE = "敏感词检测:" + strings.Join(words, ",")
for _, word := range words {
orgContentBytes = bytes.ReplaceAll(orgContentBytes, []byte(word), []byte(global.GWAF_HTTP_SENSITIVE_REPLACE_STRING))
}
}
}
}
}
//将数据在回写上去
finalCompressBytes, _ := waf.compressContent(resp, isStaticAssist, orgContentBytes, charsetName)
resp.Body = io.NopCloser(bytes.NewBuffer(finalCompressBytes))
resp.ContentLength = int64(len(finalCompressBytes))
resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(finalCompressBytes)), 10))
} else {
resp.Body = io.NopCloser(bytes.NewBuffer(orgContentBytes))
zlog.Warn(fmt.Sprintf("识别响应内容编码失败,响应日志,敏感词替换 不可用 %v,请求URL: %s ,可以在主机其他设置里面设置默认编码", responseEncodingError, r.URL.String()))
}
}
//检测cache
cacheConfig := model.CacheConfig{
IsEnableCache: 0,
CacheLocation: "",
CacheDir: "",
MaxFileSizeMB: 0,
MaxMemorySizeMB: 0,
}
err := json.Unmarshal([]byte(waf.HostTarget[host].Host.CacheJSON), &cacheConfig)
if err != nil {
zlog.Debug("解析cache json失败")
}
if cacheConfig.IsEnableCache == 1 && !strings.HasPrefix(weblogfrist.URL, global.GSSL_HTTP_CHANGLE_PATH) {
wafwebcache.StoreWebDataCache(resp, waf.HostTarget[host], cacheConfig, weblogfrist)
}
if !isStaticAssist {
datetimeNow := time.Now()
weblogfrist.TimeSpent = datetimeNow.UnixNano()/1e6 - weblogfrist.UNIX_ADD_TIME
// 根据配置决定是否检查HTTP响应代码并重定向到本地
if strings.HasPrefix(weblogfrist.URL, global.GSSL_HTTP_CHANGLE_PATH) {
zlog.Info("acme-challenge", weblogfrist.HOST, weblogfrist.URL)
if global.GCONFIG_RECORD_SSLHTTP_CHECK == 0 || resp.StatusCode == 404 || resp.StatusCode == 301 || resp.StatusCode == 302 {
//如果远端HTTP01不存在挑战验证文件,那么我们映射到走本地再试一下
//或者配置为不检查HTTP响应代码,直接走本地
//Challenge /.well-known/acme-challenge/2NKiiETgQdPmmjlM88mH5uo6jM98PrgWwsDslaN8
urls := strings.Split(weblogfrist.URL, "/")
if len(urls) == 4 {
challengeFile := urls[3]
//检测challengeFile是否合法
if !utils.IsValidChallengeFile(challengeFile) {
zlog.Error("challengeFile is invalid", challengeFile)
return nil
}
//当前路径 data/vhost/domain code 变量下
// 需要读取的文件路径
filePath := utils.GetCurrentDir() + "/data/vhost/" + weblogfrist.HOST_CODE + "/.well-known/acme-challenge/" + challengeFile
// 调用读取文件的函数
content, err := utils.ReadFile(filePath)
if err != nil {
zlog.Error("Error reading file: %v", err.Error())
}
if content != "" {
resp.StatusCode = http.StatusOK
resp.Status = http.StatusText(http.StatusOK)
resp.Body = io.NopCloser(bytes.NewBuffer([]byte(content)))
resp.ContentLength = int64(len(content))
resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
resp.Header.Del("Content-Encoding")
weblogfrist.ACTION = "放行"
weblogfrist.STATUS = resp.Status
weblogfrist.STATUS_CODE = resp.StatusCode
weblogfrist.TASK_FLAG = 1
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart //响应数据处理时间
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GCONFIG_RECORD_SSLHTTP_CHECK == 1 {
// 当配置为检查HTTP响应码且响应不是404/301/302时,记录警告信息
zlog.Warn(fmt.Sprintf("ACME Challenge检测:域名 %s 的 URL %s 返回了非预期的状态码 %d,影响证书验证,可在系统配置里面将sslhttp_check设置成0",
weblogfrist.HOST, weblogfrist.URL, resp.StatusCode))
}
} else {
//记录响应Header信息
resHeader := ""
for key, values := range resp.Header {
for _, value := range values {
resHeader += key + ": " + value + "\r\n"
}
}
weblogfrist.ResHeader = resHeader
weblogfrist.ACTION = "放行"
weblogfrist.STATUS = resp.Status
weblogfrist.STATUS_CODE = resp.StatusCode
weblogfrist.TASK_FLAG = 1
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart //响应数据处理时间
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
// 检查每一行
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
}
} else {
fmt.Println("weblog not found")
}
return nil
}
}
func (waf *WafEngine) StartWaf() {
waf.EngineCurrentStatus = 1
var hosts []model.Hosts
//是否有初始化全局保护
global.GWAF_LOCAL_DB.Where("global_host = ?", 1).Find(&hosts)
if hosts != nil && len(hosts) == 0 {
//初始化全局保护
var wafGlobalHost = &model.Hosts{
BaseOrm: baseorm.BaseOrm{
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
Code: uuid.GenUUID(),
Host: "全局网站",
Port: 0,
Ssl: 0,
GUARD_STATUS: 0,
REMOTE_SYSTEM: "",
REMOTE_APP: "",
Remote_host: "",
Remote_port: 0,
Certfile: "",
Keyfile: "",
REMARKS: "",
GLOBAL_HOST: 1,
START_STATUS: 0,
UnrestrictedPort: 0,
BindSslId: "",
AutoJumpHTTPS: 0,
}
global.GWAF_LOCAL_DB.Create(wafGlobalHost)
}
//初始化步骤[加载ip数据库]
// 从嵌入的文件中读取内容
//global.GCACHE_IP_CBUFF = main.Ip2regionBytes
//第一步 检测合法性并加入到全局
waf.LoadAllHost()
wafSysLog := model.WafSysLog{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
OpType: "信息",
OpContent: "WAF启动",
}
global.GQEQUE_LOG_DB.Enqueue(&wafSysLog)
waf.StartAllProxyServer()
}
// CloseWaf 关闭waf
func (waf *WafEngine) CloseWaf() {
defer func() {
e := recover()
if e != nil { // 捕获该协程的panic 111111
zlog.Debug("关闭 recover ", e)
}
}()
wafSysLog := &model.WafSysLog{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
OpType: "信息",
OpContent: "WAF关闭",
}
global.GQEQUE_LOG_DB.Enqueue(wafSysLog)
waf.EngineCurrentStatus = 0
waf.StopAllProxyServer()
//重置信息
waf.HostTarget = map[string]*wafenginmodel.HostSafe{}
waf.HostCode = map[string]string{}
waf.HostTargetNoPort = map[string]string{}
waf.ServerOnline.Clear()
waf.AllCertificate = AllCertificate{
Mux: sync.Mutex{},
Map: map[string]*tls.Certificate{},
}
// 清除Transport缓存池
waf.TransportMux.Lock()
waf.TransportPool = map[string]*http.Transport{}
defer waf.TransportMux.Unlock()
}
// 清除代理
func (waf *WafEngine) ClearProxy(hostCode string) {
var list []model.LoadBalance
global.GWAF_LOCAL_DB.Where("host_code = ? ", hostCode).Find(&list)
if waf.HostTarget[waf.HostCode[hostCode]] != nil {
waf.HostTarget[waf.HostCode[hostCode]].Mux.Lock()
defer waf.HostTarget[waf.HostCode[hostCode]].Mux.Unlock()
waf.HostTarget[waf.HostCode[hostCode]].LoadBalanceRuntime.RevProxies = []*wafproxy.ReverseProxy{}
waf.HostTarget[waf.HostCode[hostCode]].LoadBalanceRuntime.WeightRoundRobinBalance = loadbalance.NewWeightRoundRobinBalance(hostCode)
waf.HostTarget[waf.HostCode[hostCode]].LoadBalanceRuntime.IpHashBalance = loadbalance.NewConsistentHashBalance(nil, hostCode)
waf.HostTarget[waf.HostCode[hostCode]].LoadBalanceLists = list
}
}
// 开启所有代理
func (waf *WafEngine) StartAllProxyServer() {
waf.ServerOnline.Range(func(port int, v innerbean.ServerRunTime) bool {
waf.StartProxyServer(v)
return true
})
waf.EnumAllPortProxyServer()
waf.ReLoadSensitive()
}
// 罗列端口
func (waf *WafEngine) EnumAllPortProxyServer() {
onlinePorts := ""
waf.ServerOnline.Range(func(port int, v innerbean.ServerRunTime) bool {
onlinePorts = strconv.Itoa(v.Port) + "," + onlinePorts
return true
})
global.GWAF_RUNTIME_CURRENT_WEBPORT = onlinePorts
}
func (waf *WafEngine) StartProxyServer(innruntime innerbean.ServerRunTime) {
if innruntime.Status == 0 {
//启动完成的就不进这里了
return
}
if innruntime.ServerType == "" {
//如果是空的就不进行了
return
}
go func(innruntime innerbean.ServerRunTime) {
if (innruntime.ServerType) == "https" {
defer func() {
e := recover()
if e != nil { // 捕获该协程的panic 111111
zlog.Warn("https recover ", e)
}
}()
var svr *http.Server
// 检查是否启用HTTPS重定向服务器
if global.GCONFIG_ENABLE_HTTPS_REDIRECT == 1 {
// 使用新的重定向服务器
redirectServer := &wafhttpserver.RedirectingHTTPSServer{
Server: &http.Server{
Addr: ":" + strconv.Itoa(innruntime.Port),
Handler: waf,
TLSConfig: &tls.Config{
GetCertificate: waf.GetCertificateFunc,
MinVersion: utils.ParseTLSVersion(global.GCONFIG_RECORD_SSLMinVerson),
MaxVersion: utils.ParseTLSVersion(global.GCONFIG_RECORD_SSLMaxVerson),
},
},
}
svr = redirectServer.Server
serclone, _ := waf.ServerOnline.Get(innruntime.Port)
serclone.Svr = svr
serclone.Status = 0
waf.ServerOnline.Set(innruntime.Port, serclone)
zlog.Info("启动HTTPS重定向服务器" + strconv.Itoa(innruntime.Port))
err := redirectServer.ListenAndServeTLS("", "")
if err == http.ErrServerClosed {
zlog.Error("[HTTPServer] https redirect server has been close, cause:[%v]", err)
}
} else {
svr = &http.Server{
Addr: ":" + strconv.Itoa(innruntime.Port),
Handler: waf,
TLSConfig: &tls.Config{
GetCertificate: waf.GetCertificateFunc,
MinVersion: utils.ParseTLSVersion(global.GCONFIG_RECORD_SSLMinVerson),
MaxVersion: utils.ParseTLSVersion(global.GCONFIG_RECORD_SSLMaxVerson),
},
}
serclone, _ := waf.ServerOnline.Get(innruntime.Port)
serclone.Svr = svr
serclone.Status = 0
waf.ServerOnline.Set(innruntime.Port, serclone)
zlog.Info("启动HTTPS 服务器" + strconv.Itoa(innruntime.Port))
ln, err := net.Listen("tcp", svr.Addr)
if err != nil {
zlog.Error("https listen fail", err.Error())
return
}
if global.GCONFIG_ENABLE_PROXY_PROTOCOL == 1 {
ln = &proxyproto.Listener{Listener: ln}
}
err = svr.ServeTLS(ln, "", "")
if err == http.ErrServerClosed {
zlog.Error("[HTTPServer] https server has been close, cause:[%v]", err)
} else {
//TODO 记录如果https 端口被占用的情况 记录日志 且应该推送websocket
wafSysLog := model.WafSysLog{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
OpType: "系统运行错误",
OpContent: "HTTPS端口被占用: " + strconv.Itoa(innruntime.Port) + ",请检查",
}
global.GQEQUE_LOG_DB.Enqueue(&wafSysLog)
zlog.Error("[HTTPServer] https server start fail, cause:[%v]", err)
}
zlog.Info("server https shutdown")
}
} else {
defer func() {
e := recover()
if e != nil { // 捕获该协程的panic 111111
zlog.Warn("http recover ", e)
}
}()
svr := &http.Server{
Addr: ":" + strconv.Itoa(innruntime.Port),
Handler: waf,
}
serclone, _ := waf.ServerOnline.Get(innruntime.Port)
serclone.Svr = svr
serclone.Status = 0
waf.ServerOnline.Set(innruntime.Port, serclone)
zlog.Info("启动HTTP 服务器" + strconv.Itoa(innruntime.Port))
ln, err := net.Listen("tcp", svr.Addr)
if err != nil {
zlog.Error("http listen fail", err.Error())
return
}
if global.GCONFIG_ENABLE_PROXY_PROTOCOL == 1 {
ln = &proxyproto.Listener{Listener: ln}
}
err = svr.Serve(ln)
if err == http.ErrServerClosed {
zlog.Warn("[HTTPServer] http server has been close, cause:[%v]", err)
} else {
//TODO 记录如果http 端口被占用的情况 记录日志 且应该推送websocket
wafSysLog := model.WafSysLog{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
OpType: "系统运行错误",
OpContent: "HTTP端口被占用: " + strconv.Itoa(innruntime.Port) + ",请检查",
}
global.GQEQUE_LOG_DB.Enqueue(&wafSysLog)
zlog.Error("[HTTPServer] http server start fail, cause:[%v]", err)
}
zlog.Info("server http shutdown")
}
}(innruntime)
}
// 关闭所有代理服务
func (waf *WafEngine) StopAllProxyServer() {
waf.ServerOnline.Range(func(port int, v innerbean.ServerRunTime) bool {
waf.StopProxyServer(v)
return true
})
}
// 关闭指定代理服务
func (waf *WafEngine) StopProxyServer(v innerbean.ServerRunTime) {
if v.Svr != nil {
v.Svr.Close()
}
}
func (waf *WafEngine) ClearCcWindows() {
// 清理所有主机的IP限流器记录
for _, hostSafe := range waf.HostTarget {
if hostSafe.PluginIpRateLimiter != nil {
hostSafe.PluginIpRateLimiter.CleanupOldRecords()
}
}
}
// ClearCcWindowsForIP 清理特定IP的CC限流记录
func (waf *WafEngine) ClearCcWindowsForIP(ip string) {
// 遍历所有主机,清理指定IP的限流记录
hostCount := 0
for hostKey, hostSafe := range waf.HostTarget {
if hostSafe.PluginIpRateLimiter != nil {
// 获取清理前的请求计数
var countBefore int
countBefore = hostSafe.PluginIpRateLimiter.GetRequestCount(ip)
// 清理该IP的限流记录
hostSafe.PluginIpRateLimiter.ClearWindowForIP(ip)
hostCount++
// 获取清理后的请求计数
var countAfter int
countAfter = hostSafe.PluginIpRateLimiter.GetRequestCount(ip)
// 打印清理前后的计数信息
zlog.Debug(fmt.Sprintf("主机 %s 的 IP %s 限流记录: 清理前=%d, 清理后=%d",
hostKey, ip, countBefore, countAfter))
}
}
zlog.Debug(fmt.Sprintf("已清理 IP %s 在 %d 个主机上的 CC 限流记录", ip, hostCount),
zap.String("ip", ip),
zap.Int("hostCount", hostCount))
}
func (waf *WafEngine) ApplyAntiCCConfig(hostCode string, antiCC model.AntiCC) {
targetKey, ok := waf.HostCode[hostCode]
if !ok {
zlog.Debug("Anticc reload skip: hostCode not found", zap.String("hostCode", hostCode))
return
}
hostSafe, ok := waf.HostTarget[targetKey]
if !ok {
zlog.Debug("Anticc reload skip: hostTarget not found", zap.String("targetKey", targetKey))
return
}
hostSafe.Mux.Lock()
defer hostSafe.Mux.Unlock()
if antiCC.Id == "" {
// 关闭CC防护
hostSafe.PluginIpRateLimiter = nil
hostSafe.AntiCCBean = antiCC
zlog.Debug("Anticc disabled", zap.String("hostCode", hostCode))
return
}
// 与初始化逻辑保持一致:支持滑动窗口/平均速率
if antiCC.LimitMode == "window" {
hostSafe.PluginIpRateLimiter = webplugin.NewWindowIPRateLimiter(antiCC.Rate, antiCC.Limit)
} else {
hostSafe.PluginIpRateLimiter = webplugin.NewIPRateLimiter(rate.Limit(antiCC.Rate), antiCC.Limit)
}
if antiCC.IsEnableRule {
hostSafe.PluginIpRateLimiter.Rule = &utils.RuleHelper{}
hostSafe.PluginIpRateLimiter.Rule.InitRuleEngine()
hostSafe.PluginIpRateLimiter.Rule.LoadRuleString(antiCC.RuleContent)
}
hostSafe.AntiCCBean = antiCC
zlog.Debug("远程配置", zap.Any("Anticc", antiCC))
// ... existing code ...
}