Files
SamWaf/wafenginecore/proxy.go
T
2026-05-14 14:08:42 +08:00

529 lines
17 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 wafenginecore
import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"SamWaf/wafproxy"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
func (waf *WafEngine) ProxyHTTP(w http.ResponseWriter, r *http.Request, host string, remoteUrl *url.URL, clientIp string, ctx context.Context, weblog *innerbean.WebLog, hostTarget *wafenginmodel.HostSafe) {
// 在 director 修改请求头之前,将客户端原始 Accept-Encoding 存入 context,
// 供响应压缩逻辑使用(自定义头可能把转发请求的 Accept-Encoding 清空)
ctx = context.WithValue(ctx, "original_accept_encoding", r.Header.Get("Accept-Encoding"))
//检测是否启动负载
if hostTarget.Host.IsEnableLoadBalance > 0 {
lb := &hostTarget.LoadBalanceRuntime
(*lb).Mux.Lock()
defer (*lb).Mux.Unlock()
if len(hostTarget.LoadBalanceLists) > 0 && len(hostTarget.LoadBalanceRuntime.RevProxies) == 0 {
for addrIndex, loadBalance := range hostTarget.LoadBalanceLists {
//初始化后端负载
zlog.Debug("HTTP REQUEST", weblog.REQ_UUID, weblog.URL, "未初始化")
transport := waf.getOrCreateTransport(r, host, 1, loadBalance, hostTarget) // 使用缓存的Transport
customHeaders := waf.getCustomHeaders(r, host, 1, loadBalance, hostTarget)
customConfig := map[string]string{}
customConfig["IsTransBackDomain"] = strconv.Itoa(hostTarget.Host.IsTransBackDomain)
proxy := wafproxy.NewSingleHostReverseProxyCustomHeader(remoteUrl, customHeaders, customConfig)
proxy.Transport = transport
proxy.ModifyResponse = waf.modifyResponse()
proxy.ErrorHandler = waf.errorResponse()
hostTarget.LoadBalanceRuntime.RevProxies = append(hostTarget.LoadBalanceRuntime.RevProxies, proxy)
// 初始化策略相关信息
switch hostTarget.Host.LoadBalanceStage {
case 1: // 加权轮询(WRR)
hostTarget.LoadBalanceRuntime.WeightRoundRobinBalance.Add(addrIndex, loadBalance.Weight)
break
case 2: // IPHash
hostTarget.LoadBalanceRuntime.IpHashBalance.Add(strconv.Itoa(addrIndex), 1)
break
default:
http.Error(w, "Invalid Load Balance Stage", http.StatusBadRequest)
}
}
}
proxyIndex := waf.getProxyIndex(host, clientIp, hostTarget)
if proxyIndex == -1 {
http.Error(w, "No Available BackServer", http.StatusBadRequest)
return
}
// 记录使用的负载均衡IP和端口信息
if proxyIndex >= 0 && proxyIndex < len(hostTarget.LoadBalanceLists) {
selectedLoadBalance := hostTarget.LoadBalanceLists[proxyIndex]
balanceInfo := fmt.Sprintf("%s:%d", selectedLoadBalance.Remote_ip, selectedLoadBalance.Remote_port)
// 记录到WebLog的BalanceInfo字段
weblog.BalanceInfo = balanceInfo
}
proxy := hostTarget.LoadBalanceRuntime.RevProxies[proxyIndex]
if proxy != nil {
// 添加转发耗时记录
if wafCtx, ok := ctx.Value("waf_context").(innerbean.WafHttpContextData); ok && wafCtx.Weblog != nil {
forwardStart := time.Now().UnixNano() / 1e6
defer func() {
wafCtx.Weblog.ForwardCost = time.Now().UnixNano()/1e6 - forwardStart
wafCtx.Weblog.IsBalance = 1
}()
}
proxy.ServeHTTP(w, r.WithContext(ctx))
} else {
http.Error(w, "No Available Server", http.StatusBadRequest)
}
} else {
transport := waf.getOrCreateTransport(r, host, 0, model.LoadBalance{}, hostTarget) // 使用缓存的Transport
customHeaders := waf.getCustomHeaders(r, host, 0, model.LoadBalance{}, hostTarget)
customConfig := map[string]string{}
customConfig["IsTransBackDomain"] = strconv.Itoa(hostTarget.Host.IsTransBackDomain)
proxy := wafproxy.NewSingleHostReverseProxyCustomHeader(remoteUrl, customHeaders, customConfig)
proxy.Transport = transport
proxy.ModifyResponse = waf.modifyResponse()
proxy.ErrorHandler = waf.errorResponse()
// 添加转发耗时记录
if wafCtx, ok := ctx.Value("waf_context").(innerbean.WafHttpContextData); ok && wafCtx.Weblog != nil {
forwardStart := time.Now().UnixNano() / 1e6
defer func() {
wafCtx.Weblog.ForwardCost = time.Now().UnixNano()/1e6 - forwardStart
wafCtx.Weblog.IsBalance = 0
}()
}
proxy.ServeHTTP(w, r.WithContext(ctx))
}
}
// ProxyHTTPWithPathRule 根据路径规则将请求代理到指定后端
func (waf *WafEngine) ProxyHTTPWithPathRule(w http.ResponseWriter, r *http.Request, host string, rule *model.HostPathRule, clientIP string, ctx context.Context, weblog *innerbean.WebLog, hostTarget *wafenginmodel.HostSafe) {
ctx = context.WithValue(ctx, "original_accept_encoding", r.Header.Get("Accept-Encoding"))
// 构建后端 URL
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
targetStr := scheme + "://" + rule.RemoteHost
if rule.RemotePort > 0 {
targetStr += ":" + strconv.Itoa(rule.RemotePort)
}
remoteUrl, err := url.Parse(targetStr)
if err != nil {
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
// 若需要剥离前缀,克隆请求并修改 URL
if rule.StripPrefix == 1 && strings.HasPrefix(r.URL.Path, rule.Path) {
newR := r.Clone(ctx)
stripped := strings.TrimPrefix(r.URL.Path, rule.Path)
if !strings.HasPrefix(stripped, "/") {
stripped = "/" + stripped
}
newR.URL.Path = stripped
if r.URL.RawPath != "" {
rawStripped := strings.TrimPrefix(r.URL.RawPath, rule.Path)
if !strings.HasPrefix(rawStripped, "/") {
rawStripped = "/" + rawStripped
}
newR.URL.RawPath = rawStripped
}
r = newR
}
transport := waf.createPathRuleTransport(r, rule, hostTarget)
customHeaders := map[string]string{}
if r.TLS != nil {
customHeaders["X-FORWARDED-PROTO"] = "https"
}
customConfig := map[string]string{}
customConfig["IsTransBackDomain"] = strconv.Itoa(hostTarget.Host.IsTransBackDomain)
proxy := wafproxy.NewSingleHostReverseProxyCustomHeader(remoteUrl, customHeaders, customConfig)
proxy.Transport = transport
proxy.ModifyResponse = waf.modifyResponse()
proxy.ErrorHandler = waf.errorResponse()
if wafCtx, ok := ctx.Value("waf_context").(innerbean.WafHttpContextData); ok && wafCtx.Weblog != nil {
forwardStart := time.Now().UnixNano() / 1e6
defer func() {
wafCtx.Weblog.ForwardCost = time.Now().UnixNano()/1e6 - forwardStart
wafCtx.Weblog.IsBalance = 0
}()
}
proxy.ServeHTTP(w, r.WithContext(ctx))
}
// createPathRuleTransport 为路径规则代理创建 Transport
func (waf *WafEngine) createPathRuleTransport(r *http.Request, rule *model.HostPathRule, hostTarget *wafenginmodel.HostSafe) *http.Transport {
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := net.Dialer{
Timeout: time.Duration(global.GCONFIG_RECORD_CONNECT_TIME_OUT) * time.Second,
KeepAlive: time.Duration(global.GCONFIG_RECORD_KEEPALIVE_TIME_OUT) * time.Second,
}
if rule.RemoteIP != "" {
conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(rule.RemoteIP, strconv.Itoa(rule.RemotePort)))
if err == nil {
return conn, nil
}
}
return dialer.DialContext(ctx, network, addr)
}
var transport *http.Transport
if r.TLS != nil {
transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: hostTarget.Host.InsecureSkipVerify == 1,
GetCertificate: waf.GetCertificateFunc,
},
DialContext: dialContext,
}
} else {
transport = &http.Transport{
DialContext: dialContext,
}
}
transport.ResponseHeaderTimeout = time.Duration(hostTarget.Host.ResponseTimeOut) * time.Second
return transport
}
func (waf *WafEngine) createTransport(r *http.Request, host string, isEnableLoadBalance int, loadBalance model.LoadBalance, hostTarget *wafenginmodel.HostSafe) (*http.Transport, map[string]string) {
customHeaders := map[string]string{}
var transport *http.Transport
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := net.Dialer{
Timeout: time.Duration(global.GCONFIG_RECORD_CONNECT_TIME_OUT) * time.Second,
KeepAlive: time.Duration(global.GCONFIG_RECORD_KEEPALIVE_TIME_OUT) * time.Second,
}
if isEnableLoadBalance > 0 {
conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(loadBalance.Remote_ip, strconv.Itoa(loadBalance.Remote_port)))
if err == nil {
return conn, nil
}
} else {
if hostTarget.Host.Remote_ip != "" {
conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(hostTarget.Host.Remote_ip, strconv.Itoa(hostTarget.Host.Remote_port)))
if err == nil {
return conn, nil
}
}
}
return dialer.DialContext(ctx, network, addr)
}
if r.TLS != nil {
// 增加https标识
customHeaders["X-FORWARDED-PROTO"] = "https"
transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: hostTarget.Host.InsecureSkipVerify == 1,
GetCertificate: waf.GetCertificateFunc,
},
DialContext: dialContext,
}
} else {
transport = &http.Transport{
DialContext: dialContext,
}
}
// 解析并应用Transport配置
transportConfig := model.ParseTransportConfig(hostTarget.Host.TransportJSON)
// 应用Transport配置(只有非零值才设置)
if transportConfig.MaxIdleConns > 0 {
transport.MaxIdleConns = transportConfig.MaxIdleConns
}
if transportConfig.MaxIdleConnsPerHost > 0 {
transport.MaxIdleConnsPerHost = transportConfig.MaxIdleConnsPerHost
}
if transportConfig.MaxConnsPerHost > 0 {
transport.MaxConnsPerHost = transportConfig.MaxConnsPerHost
}
if transportConfig.IdleConnTimeout > 0 {
transport.IdleConnTimeout = time.Duration(transportConfig.IdleConnTimeout) * time.Second
}
if transportConfig.TLSHandshakeTimeout > 0 {
transport.TLSHandshakeTimeout = time.Duration(transportConfig.TLSHandshakeTimeout) * time.Second
}
if transportConfig.ExpectContinueTimeout > 0 {
transport.ExpectContinueTimeout = time.Duration(transportConfig.ExpectContinueTimeout) * time.Second
}
transport.ResponseHeaderTimeout = time.Duration(hostTarget.Host.ResponseTimeOut) * time.Second
// 若自定义头里将 Accept-Encoding 设为空值,则禁用 Transport 自动追加 gzip,
customHeadersConfig := model.ParseCustomHeadersConfig(hostTarget.Host.CustomHeadersJSON)
for _, h := range customHeadersConfig.Headers {
if strings.EqualFold(h.HeaderName, "Accept-Encoding") && h.HeaderValue == "" {
transport.DisableCompression = true
break
}
}
//把下面的参数一次性使用格式化实现
zlog.Debug(fmt.Sprintf("Transport配置信息:\nMaxIdleConns: %d\nMaxIdleConnsPerHost: %d\nMaxConnsPerHost: %d\nIdleConnTimeout: %v\nTLSHandshakeTimeout: %v\nExpectContinueTimeout: %v\nResponseHeaderTimeout: %v\nDisableKeepAlives: %v\nDisableCompression: %v",
transport.MaxIdleConns,
transport.MaxIdleConnsPerHost,
transport.MaxConnsPerHost,
transport.IdleConnTimeout,
transport.TLSHandshakeTimeout,
transport.ExpectContinueTimeout,
transport.ResponseHeaderTimeout,
transport.DisableKeepAlives,
transport.DisableCompression))
return transport, customHeaders
}
// 获取或创建Transport
func (waf *WafEngine) getOrCreateTransport(r *http.Request, host string, isEnableLoadBalance int, loadBalance model.LoadBalance, hostTarget *wafenginmodel.HostSafe) *http.Transport {
// 生成Transport的唯一键
transportKey := waf.generateTransportKey(host, isEnableLoadBalance, loadBalance, hostTarget)
waf.TransportMux.RLock()
if transport, exists := waf.TransportPool[transportKey]; exists {
waf.TransportMux.RUnlock()
return transport
}
waf.TransportMux.RUnlock()
// 创建新的Transport
waf.TransportMux.Lock()
defer waf.TransportMux.Unlock()
// 双重检查,防止并发创建
if transport, exists := waf.TransportPool[transportKey]; exists {
return transport
}
transport, _ := waf.createTransport(r, host, isEnableLoadBalance, loadBalance, hostTarget)
// 优化Transport配置
/*transport.MaxIdleConns = 1000
transport.MaxIdleConnsPerHost = 1000*/
if waf.TransportPool == nil {
waf.TransportPool = make(map[string]*http.Transport)
}
waf.TransportPool[transportKey] = transport
return transport
}
// 生成Transport的唯一键
func (waf *WafEngine) generateTransportKey(host string, isEnableLoadBalance int, loadBalance model.LoadBalance, hostTarget *wafenginmodel.HostSafe) string {
key := fmt.Sprintf("%s_%d_%s_%d_%v",
host,
isEnableLoadBalance,
hostTarget.Host.Remote_ip,
hostTarget.Host.Remote_port,
hostTarget.Host.InsecureSkipVerify)
if isEnableLoadBalance == 1 {
key = fmt.Sprintf("%s_%d_%s_%d_%v",
host,
isEnableLoadBalance,
loadBalance.Remote_ip,
loadBalance.Remote_port,
hostTarget.Host.InsecureSkipVerify)
}
return key
}
// 分离自定义头部逻辑
func (waf *WafEngine) getCustomHeaders(r *http.Request, host string, isEnableLoadBalance int, loadBalance model.LoadBalance, hostTarget *wafenginmodel.HostSafe) map[string]string {
customHeaders := map[string]string{}
if r.TLS != nil {
customHeaders["X-FORWARDED-PROTO"] = "https"
}
// 解析自定义头信息配置
customHeadersConfig := model.ParseCustomHeadersConfig(hostTarget.Host.CustomHeadersJSON)
// 如果启用了自定义头信息
if customHeadersConfig.IsEnableCustomHeaders == 1 {
// 获取客户端IP
clientIP := r.RemoteAddr
if idx := strings.LastIndex(clientIP, ":"); idx != -1 {
clientIP = clientIP[:idx]
}
// 遍历自定义头信息列表
for _, header := range customHeadersConfig.Headers {
if header.HeaderName == "" {
continue
}
// 解析并替换变量
headerValue := waf.parseHeaderValue(header.HeaderValue, r, clientIP)
// 添加到自定义头信息中
customHeaders[header.HeaderName] = headerValue
}
}
return customHeaders
}
// matchResponseHeaderRule 判断请求路径是否匹配某条响应头规则
// MatchType: global(全部匹配) / prefix(路径前缀) / suffix(文件后缀,分号分隔) / exact(精确) / regex(正则)
func matchResponseHeaderRule(rule model.CustomResponseHeaderRule, urlPath string) bool {
switch rule.MatchType {
case "global", "":
return true
case "prefix":
return strings.HasPrefix(urlPath, rule.MatchValue)
case "exact":
return urlPath == rule.MatchValue
case "suffix":
// 支持分号分隔多个后缀,如 ".mp4;.mp3;.webm"
ext := ""
if idx := strings.LastIndex(urlPath, "."); idx != -1 {
ext = strings.ToLower(urlPath[idx:])
}
if ext == "" {
return false
}
for _, s := range strings.Split(rule.MatchValue, ";") {
s = strings.TrimSpace(strings.ToLower(s))
if s != "" && ext == s {
return true
}
}
return false
case "regex":
matched, err := regexp.MatchString(rule.MatchValue, urlPath)
if err != nil {
return false
}
return matched
default:
return false
}
}
// applyCustomResponseHeaders 根据规则列表和请求路径,计算最终需要设置的响应头 map
// 合并逻辑:先汇总所有 global 规则的 headers,再找第一条匹配的 location 规则按 MergeMode 合并。
func (waf *WafEngine) applyCustomResponseHeaders(
rules []model.CustomResponseHeaderRule,
r *http.Request,
clientIP string,
) map[string]string {
result := map[string]string{}
// 1. 收集全局规则 headers(可能有多条 global 规则)
for _, rule := range rules {
if rule.MatchType == "global" || rule.MatchType == "" {
for _, h := range rule.Headers {
if h.HeaderName == "" {
continue
}
result[h.HeaderName] = waf.parseHeaderValue(h.HeaderValue, r, clientIP)
}
}
}
// 2. 按顺序找第一条匹配的非 global 规则
for _, rule := range rules {
if rule.MatchType == "global" || rule.MatchType == "" {
continue
}
if !matchResponseHeaderRule(rule, r.URL.Path) {
continue
}
// 找到匹配规则
locationHeaders := map[string]string{}
for _, h := range rule.Headers {
if h.HeaderName == "" {
continue
}
locationHeaders[h.HeaderName] = waf.parseHeaderValue(h.HeaderValue, r, clientIP)
}
if rule.MergeMode == "override" {
// override:丢弃全局,仅用本规则
return locationHeaders
}
// merge(默认):全局 + 本规则,同名以本规则为准
for k, v := range locationHeaders {
result[k] = v
}
return result
}
return result
}
// parseHeaderValue 解析头信息值中的变量
// 支持的变量:
// ${header:HeaderName} - 获取原始请求头
// ${client_ip} - 获取客户端IP
// ${remote_addr} - 获取远程地址
// ${host} - 获取主机名
// ${scheme} - 获取协议(http/https)
// ${request_uri} - 获取请求URI
// ${request_method} - 获取请求方法
func (waf *WafEngine) parseHeaderValue(value string, r *http.Request, clientIP string) string {
// 正则表达式匹配 ${变量名} 或 ${header:头名称}
varPattern := regexp.MustCompile(`\$\{([^}]+)\}`)
result := varPattern.ReplaceAllStringFunc(value, func(match string) string {
// 提取变量名(去掉 ${ 和 })
varName := match[2 : len(match)-1]
// 检查是否是header变量
if strings.HasPrefix(varName, "header:") {
headerName := strings.TrimPrefix(varName, "header:")
headerValue := r.Header.Get(headerName)
if headerValue == "" {
return "" // 如果header不存在,返回空字符串
}
return headerValue
}
// 处理其他内置变量
switch varName {
case "client_ip":
return clientIP
case "remote_addr":
return r.RemoteAddr
case "host":
return r.Host
case "scheme":
if r.TLS != nil {
return "https"
}
return "http"
case "request_uri":
return r.RequestURI
case "request_method":
return r.Method
default:
// 未知变量,保持原样
return match
}
})
return result
}