mirror of
				https://github.com/TeaOSLab/EdgeNode.git
				synced 2025-11-04 07:40:56 +08:00 
			
		
		
		
	* 信息加密使用struct代替map,以缩短加密后内容长度 * 拦截动作、人机识别动作增加是否尝试全局封禁选项 * JSCookie识别动作增加默认设置选项 * 人机识别中传入info参数异常时,尝试跳转到来源地址,避免直接提示invalid request
		
			
				
	
	
		
			491 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			491 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package waf
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
 | 
						|
	teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
 | 
						|
	"github.com/TeaOSLab/EdgeNode/internal/waf/checkpoints"
 | 
						|
	"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
 | 
						|
	"github.com/iwind/TeaGo/Tea"
 | 
						|
	"github.com/iwind/TeaGo/files"
 | 
						|
	"gopkg.in/yaml.v3"
 | 
						|
	"net/http"
 | 
						|
	"os"
 | 
						|
	"reflect"
 | 
						|
)
 | 
						|
 | 
						|
type WAF struct {
 | 
						|
	Id               int64                           `yaml:"id" json:"id"`
 | 
						|
	IsOn             bool                            `yaml:"isOn" json:"isOn"`
 | 
						|
	Name             string                          `yaml:"name" json:"name"`
 | 
						|
	Inbound          []*RuleGroup                    `yaml:"inbound" json:"inbound"`
 | 
						|
	Outbound         []*RuleGroup                    `yaml:"outbound" json:"outbound"`
 | 
						|
	CreatedVersion   string                          `yaml:"createdVersion" json:"createdVersion"`
 | 
						|
	Mode             firewallconfigs.FirewallMode    `yaml:"mode" json:"mode"`
 | 
						|
	UseLocalFirewall bool                            `yaml:"useLocalFirewall" json:"useLocalFirewall"`
 | 
						|
	SYNFlood         *firewallconfigs.SYNFloodConfig `yaml:"synFlood" json:"synFlood"`
 | 
						|
 | 
						|
	DefaultBlockAction    *BlockAction
 | 
						|
	DefaultPageAction     *PageAction
 | 
						|
	DefaultCaptchaAction  *CaptchaAction
 | 
						|
	DefaultJSCookieAction *JSCookieAction
 | 
						|
	DefaultPost307Action  *Post307Action
 | 
						|
	DefaultGet302Action   *Get302Action
 | 
						|
 | 
						|
	hasInboundRules  bool
 | 
						|
	hasOutboundRules bool
 | 
						|
 | 
						|
	checkpointsMap map[string]checkpoints.CheckpointInterface // prefix => checkpoint
 | 
						|
	actionMap      map[int64]ActionInterface                  // actionId => ActionInterface
 | 
						|
}
 | 
						|
 | 
						|
func NewWAF() *WAF {
 | 
						|
	return &WAF{
 | 
						|
		IsOn:      true,
 | 
						|
		actionMap: map[int64]ActionInterface{},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func NewWAFFromFile(path string) (waf *WAF, err error) {
 | 
						|
	if len(path) == 0 {
 | 
						|
		return nil, errors.New("'path' should not be empty")
 | 
						|
	}
 | 
						|
	file := files.NewFile(path)
 | 
						|
	if !file.Exists() {
 | 
						|
		return nil, errors.New("'" + path + "' not exist")
 | 
						|
	}
 | 
						|
 | 
						|
	reader, err := file.Reader()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer func() {
 | 
						|
		_ = reader.Close()
 | 
						|
	}()
 | 
						|
 | 
						|
	waf = &WAF{}
 | 
						|
	err = reader.ReadYAML(waf)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return waf, nil
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) Init() (resultErrors []error) {
 | 
						|
	// checkpoint
 | 
						|
	this.checkpointsMap = map[string]checkpoints.CheckpointInterface{}
 | 
						|
	for _, def := range checkpoints.AllCheckpoints {
 | 
						|
		instance := reflect.New(reflect.Indirect(reflect.ValueOf(def.Instance)).Type()).Interface().(checkpoints.CheckpointInterface)
 | 
						|
		instance.Init()
 | 
						|
		instance.SetPriority(def.Priority)
 | 
						|
		this.checkpointsMap[def.Prefix] = instance
 | 
						|
	}
 | 
						|
 | 
						|
	// action map
 | 
						|
	this.actionMap = map[int64]ActionInterface{}
 | 
						|
 | 
						|
	// rules
 | 
						|
	this.hasInboundRules = len(this.Inbound) > 0
 | 
						|
	this.hasOutboundRules = len(this.Outbound) > 0
 | 
						|
 | 
						|
	if this.hasInboundRules {
 | 
						|
		for _, group := range this.Inbound {
 | 
						|
			// finder
 | 
						|
			for _, set := range group.RuleSets {
 | 
						|
				for _, rule := range set.Rules {
 | 
						|
					rule.SetCheckpointFinder(this.FindCheckpointInstance)
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			err := group.Init(this)
 | 
						|
			if err != nil {
 | 
						|
				// 这里我们不阻止其他规则正常加入
 | 
						|
				resultErrors = append(resultErrors, fmt.Errorf("init group '%d' failed: %w", group.Id, err))
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if this.hasOutboundRules {
 | 
						|
		for _, group := range this.Outbound {
 | 
						|
			// finder
 | 
						|
			for _, set := range group.RuleSets {
 | 
						|
				for _, rule := range set.Rules {
 | 
						|
					rule.SetCheckpointFinder(this.FindCheckpointInstance)
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			err := group.Init(this)
 | 
						|
			if err != nil {
 | 
						|
				// 这里我们不阻止其他规则正常加入
 | 
						|
				resultErrors = append(resultErrors, err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) AddRuleGroup(ruleGroup *RuleGroup) {
 | 
						|
	if ruleGroup.IsInbound {
 | 
						|
		this.Inbound = append(this.Inbound, ruleGroup)
 | 
						|
	} else {
 | 
						|
		this.Outbound = append(this.Outbound, ruleGroup)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) RemoveRuleGroup(ruleGroupId int64) {
 | 
						|
	{
 | 
						|
		result := []*RuleGroup{}
 | 
						|
		for _, group := range this.Inbound {
 | 
						|
			if group.Id == ruleGroupId {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			result = append(result, group)
 | 
						|
		}
 | 
						|
		this.Inbound = result
 | 
						|
	}
 | 
						|
 | 
						|
	{
 | 
						|
		result := []*RuleGroup{}
 | 
						|
		for _, group := range this.Outbound {
 | 
						|
			if group.Id == ruleGroupId {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			result = append(result, group)
 | 
						|
		}
 | 
						|
		this.Outbound = result
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) FindRuleGroup(ruleGroupId int64) *RuleGroup {
 | 
						|
	for _, group := range this.Inbound {
 | 
						|
		if group.Id == ruleGroupId {
 | 
						|
			return group
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for _, group := range this.Outbound {
 | 
						|
		if group.Id == ruleGroupId {
 | 
						|
			return group
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) FindRuleGroupWithCode(ruleGroupCode string) *RuleGroup {
 | 
						|
	if len(ruleGroupCode) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	for _, group := range this.Inbound {
 | 
						|
		if group.Code == ruleGroupCode {
 | 
						|
			return group
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for _, group := range this.Outbound {
 | 
						|
		if group.Code == ruleGroupCode {
 | 
						|
			return group
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) MoveInboundRuleGroup(fromIndex int, toIndex int) {
 | 
						|
	if fromIndex < 0 || fromIndex >= len(this.Inbound) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if toIndex < 0 || toIndex >= len(this.Inbound) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if fromIndex == toIndex {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	group := this.Inbound[fromIndex]
 | 
						|
	result := []*RuleGroup{}
 | 
						|
	for i := 0; i < len(this.Inbound); i++ {
 | 
						|
		if i == fromIndex {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if fromIndex > toIndex && i == toIndex {
 | 
						|
			result = append(result, group)
 | 
						|
		}
 | 
						|
		result = append(result, this.Inbound[i])
 | 
						|
		if fromIndex < toIndex && i == toIndex {
 | 
						|
			result = append(result, group)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	this.Inbound = result
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) MoveOutboundRuleGroup(fromIndex int, toIndex int) {
 | 
						|
	if fromIndex < 0 || fromIndex >= len(this.Outbound) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if toIndex < 0 || toIndex >= len(this.Outbound) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if fromIndex == toIndex {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	group := this.Outbound[fromIndex]
 | 
						|
	result := []*RuleGroup{}
 | 
						|
	for i := 0; i < len(this.Outbound); i++ {
 | 
						|
		if i == fromIndex {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if fromIndex > toIndex && i == toIndex {
 | 
						|
			result = append(result, group)
 | 
						|
		}
 | 
						|
		result = append(result, this.Outbound[i])
 | 
						|
		if fromIndex < toIndex && i == toIndex {
 | 
						|
			result = append(result, group)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	this.Outbound = result
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) MatchRequest(req requests.Request, writer http.ResponseWriter, defaultCaptchaType firewallconfigs.ServerCaptchaType) (result MatchResult, err error) {
 | 
						|
	if !this.hasInboundRules {
 | 
						|
		return MatchResult{
 | 
						|
			GoNext: true,
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	// validate captcha
 | 
						|
	var rawPath = req.WAFRaw().URL.Path
 | 
						|
	if rawPath == CaptchaPath {
 | 
						|
		req.DisableAccessLog()
 | 
						|
		req.DisableStat()
 | 
						|
		captchaValidator.Run(req, writer, defaultCaptchaType)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Get 302验证
 | 
						|
	if rawPath == Get302Path {
 | 
						|
		req.DisableAccessLog()
 | 
						|
		req.DisableStat()
 | 
						|
		get302Validator.Run(req, writer)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// match rules
 | 
						|
	var hasRequestBody bool
 | 
						|
	for _, group := range this.Inbound {
 | 
						|
		if !group.IsOn {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		b, hasCheckedRequestBody, set, matchErr := group.MatchRequest(req)
 | 
						|
		if hasCheckedRequestBody {
 | 
						|
			hasRequestBody = true
 | 
						|
		}
 | 
						|
		if matchErr != nil {
 | 
						|
			return MatchResult{
 | 
						|
				GoNext:         true,
 | 
						|
				HasRequestBody: hasRequestBody,
 | 
						|
			}, matchErr
 | 
						|
		}
 | 
						|
		if b {
 | 
						|
			var performResult = set.PerformActions(this, group, req, writer)
 | 
						|
			if !performResult.GoNextSet {
 | 
						|
				if performResult.GoNextGroup {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				return MatchResult{
 | 
						|
					GoNext:         performResult.ContinueRequest,
 | 
						|
					HasRequestBody: hasRequestBody,
 | 
						|
					Group:          group,
 | 
						|
					Set:            set,
 | 
						|
					IsAllowed:      performResult.IsAllowed,
 | 
						|
					AllowScope:     performResult.AllowScope,
 | 
						|
				}, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return MatchResult{
 | 
						|
		GoNext:         true,
 | 
						|
		HasRequestBody: hasRequestBody,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) MatchResponse(req requests.Request, rawResp *http.Response, writer http.ResponseWriter) (result MatchResult, err error) {
 | 
						|
	if !this.hasOutboundRules {
 | 
						|
		return MatchResult{
 | 
						|
			GoNext: true,
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
	var hasRequestBody bool
 | 
						|
	var resp = requests.NewResponse(rawResp)
 | 
						|
	for _, group := range this.Outbound {
 | 
						|
		if !group.IsOn {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		b, hasCheckedRequestBody, set, matchErr := group.MatchResponse(req, resp)
 | 
						|
		if hasCheckedRequestBody {
 | 
						|
			hasRequestBody = true
 | 
						|
		}
 | 
						|
		if matchErr != nil {
 | 
						|
			return MatchResult{
 | 
						|
				GoNext:         true,
 | 
						|
				HasRequestBody: hasRequestBody,
 | 
						|
			}, matchErr
 | 
						|
		}
 | 
						|
		if b {
 | 
						|
			var performResult = set.PerformActions(this, group, req, writer)
 | 
						|
			if !performResult.GoNextSet {
 | 
						|
				if performResult.GoNextGroup {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				return MatchResult{
 | 
						|
					GoNext:         performResult.ContinueRequest,
 | 
						|
					HasRequestBody: hasRequestBody,
 | 
						|
					Group:          group,
 | 
						|
					Set:            set,
 | 
						|
					IsAllowed:      performResult.IsAllowed,
 | 
						|
					AllowScope:     performResult.AllowScope,
 | 
						|
				}, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return MatchResult{
 | 
						|
		GoNext:         true,
 | 
						|
		HasRequestBody: hasRequestBody,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// Save to file path
 | 
						|
func (this *WAF) Save(path string) error {
 | 
						|
	if len(path) == 0 {
 | 
						|
		return errors.New("path should not be empty")
 | 
						|
	}
 | 
						|
	if len(this.CreatedVersion) == 0 {
 | 
						|
		this.CreatedVersion = teaconst.Version
 | 
						|
	}
 | 
						|
	data, err := yaml.Marshal(this)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return os.WriteFile(path, data, 0644)
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) ContainsGroupCode(code string) bool {
 | 
						|
	if len(code) == 0 {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	for _, group := range this.Inbound {
 | 
						|
		if group.Code == code {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for _, group := range this.Outbound {
 | 
						|
		if group.Code == code {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) AddAction(action ActionInterface) {
 | 
						|
	this.actionMap[action.ActionId()] = action
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) FindAction(actionId int64) ActionInterface {
 | 
						|
	return this.actionMap[actionId]
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) Copy() *WAF {
 | 
						|
	var waf = &WAF{
 | 
						|
		Id:       this.Id,
 | 
						|
		IsOn:     this.IsOn,
 | 
						|
		Name:     this.Name,
 | 
						|
		Inbound:  this.Inbound,
 | 
						|
		Outbound: this.Outbound,
 | 
						|
	}
 | 
						|
	return waf
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) CountInboundRuleSets() int {
 | 
						|
	count := 0
 | 
						|
	for _, group := range this.Inbound {
 | 
						|
		count += len(group.RuleSets)
 | 
						|
	}
 | 
						|
	return count
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) CountOutboundRuleSets() int {
 | 
						|
	count := 0
 | 
						|
	for _, group := range this.Outbound {
 | 
						|
		count += len(group.RuleSets)
 | 
						|
	}
 | 
						|
	return count
 | 
						|
}
 | 
						|
 | 
						|
func (this *WAF) FindCheckpointInstance(prefix string) checkpoints.CheckpointInterface {
 | 
						|
	instance, ok := this.checkpointsMap[prefix]
 | 
						|
	if ok {
 | 
						|
		return instance
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Start
 | 
						|
func (this *WAF) Start() {
 | 
						|
	for _, checkpoint := range this.checkpointsMap {
 | 
						|
		checkpoint.Start()
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Stop call stop() when the waf was deleted
 | 
						|
func (this *WAF) Stop() {
 | 
						|
	for _, checkpoint := range this.checkpointsMap {
 | 
						|
		checkpoint.Stop()
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// MergeTemplate merge with template
 | 
						|
func (this *WAF) MergeTemplate() (changedItems []string, err error) {
 | 
						|
	changedItems = []string{}
 | 
						|
 | 
						|
	// compare versions
 | 
						|
	if !Tea.IsTesting() && this.CreatedVersion == teaconst.Version {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	this.CreatedVersion = teaconst.Version
 | 
						|
 | 
						|
	template, err := Template()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	groups := []*RuleGroup{}
 | 
						|
	groups = append(groups, template.Inbound...)
 | 
						|
	groups = append(groups, template.Outbound...)
 | 
						|
 | 
						|
	var newGroupId int64 = 1_000_000_000
 | 
						|
 | 
						|
	for _, group := range groups {
 | 
						|
		oldGroup := this.FindRuleGroupWithCode(group.Code)
 | 
						|
		if oldGroup == nil {
 | 
						|
			newGroupId++
 | 
						|
			group.Id = newGroupId
 | 
						|
			this.AddRuleGroup(group)
 | 
						|
			changedItems = append(changedItems, "+group "+group.Name)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// check rule sets
 | 
						|
		for _, set := range group.RuleSets {
 | 
						|
			oldSet := oldGroup.FindRuleSetWithCode(set.Code)
 | 
						|
			if oldSet == nil {
 | 
						|
				oldGroup.AddRuleSet(set)
 | 
						|
				changedItems = append(changedItems, "+group "+group.Name+" rule set:"+set.Name)
 | 
						|
			} else if len(oldSet.Rules) < len(set.Rules) {
 | 
						|
				oldSet.Rules = set.Rules
 | 
						|
				changedItems = append(changedItems, "*group "+group.Name+" rule set:"+set.Name)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 |