mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
a1a997bcda
Add the React Flow rule editor, ordered graph APIs and runtime DAG execution.\n\nPublish rules only on OpenResty reload and reconcile checksum-driven IP group snapshots in bounded shared memory.
416 lines
14 KiB
Go
416 lines
14 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package model
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/db"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// OpenFlareWAFRuleGroup stores a WAF rule group.
|
|
type OpenFlareWAFRuleGroup struct {
|
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
|
Name string `json:"name" gorm:"size:255;not null"`
|
|
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
|
IsGlobal bool `json:"is_global" gorm:"not null;default:false;index"`
|
|
Graph string `json:"graph" gorm:"type:text;not null;default:''"`
|
|
Revision uint64 `json:"revision" gorm:"not null;default:1"`
|
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
|
}
|
|
|
|
// TableName returns the GORM table name.
|
|
func (OpenFlareWAFRuleGroup) TableName() string {
|
|
return "of_waf_rule_groups"
|
|
}
|
|
|
|
// OpenFlareWAFIPGroup stores a WAF IP group.
|
|
type OpenFlareWAFIPGroup struct {
|
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
|
Name string `json:"name" gorm:"size:255;not null"`
|
|
Type string `json:"type" gorm:"size:32;not null;index"`
|
|
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
|
IPList string `json:"ip_list" gorm:"type:text;not null;default:'[]'"`
|
|
AutoConfig string `json:"auto_config" gorm:"type:text;not null;default:'{}'"`
|
|
ExtIPs string `json:"ext_ips" gorm:"type:text;not null;default:'[]'"`
|
|
SubscriptionURL string `json:"subscription_url" gorm:"size:2048;not null;default:''"`
|
|
SubscriptionFormat string `json:"subscription_format" gorm:"size:32;not null;default:'text'"`
|
|
SubscriptionMappingRule string `json:"subscription_mapping_rule" gorm:"size:255;not null;default:''"`
|
|
SyncIntervalMinutes int `json:"sync_interval_minutes" gorm:"not null;default:1440"`
|
|
LastSyncedAt *time.Time `json:"last_synced_at"`
|
|
NextSyncAt *time.Time `json:"next_sync_at" gorm:"index"`
|
|
LastSyncStatus string `json:"last_sync_status" gorm:"size:32;not null;default:''"`
|
|
LastSyncMessage string `json:"last_sync_message" gorm:"type:text;not null;default:''"`
|
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
|
}
|
|
|
|
// TableName returns the GORM table name.
|
|
func (OpenFlareWAFIPGroup) TableName() string {
|
|
return "of_waf_ip_groups"
|
|
}
|
|
|
|
// OpenFlareWAFRuleGroupBinding binds a rule group to a proxy route.
|
|
type OpenFlareWAFRuleGroupBinding struct {
|
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
|
RuleGroupID uint `json:"rule_group_id" gorm:"not null;uniqueIndex:idx_of_waf_group_route"`
|
|
ProxyRouteID uint `json:"proxy_route_id" gorm:"not null;uniqueIndex:idx_of_waf_group_route;index"`
|
|
Sequence int `json:"sequence" gorm:"not null;default:0"`
|
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
|
}
|
|
|
|
// ErrWAFRuleRevisionConflict indicates that a rule graph was updated from a stale revision.
|
|
var ErrWAFRuleRevisionConflict = errors.New("waf rule revision conflict")
|
|
|
|
// TableName returns the GORM table name.
|
|
func (OpenFlareWAFRuleGroupBinding) TableName() string {
|
|
return "of_waf_rule_group_bindings"
|
|
}
|
|
|
|
func wafDB(ctx context.Context) (*gorm.DB, error) {
|
|
conn := db.DB(ctx)
|
|
if conn == nil {
|
|
return nil, errors.New(errDatabaseNotInitialized)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// ListOpenFlareWAFRuleGroups returns all rule groups.
|
|
func ListOpenFlareWAFRuleGroups(ctx context.Context) ([]*OpenFlareWAFRuleGroup, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var groups []*OpenFlareWAFRuleGroup
|
|
if err = conn.Order("is_global desc").Order("id asc").Find(&groups).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// GetOpenFlareWAFRuleGroupByID returns a rule group by id.
|
|
func GetOpenFlareWAFRuleGroupByID(ctx context.Context, id uint) (*OpenFlareWAFRuleGroup, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var group OpenFlareWAFRuleGroup
|
|
if err = conn.First(&group, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &group, nil
|
|
}
|
|
|
|
// GetGlobalOpenFlareWAFRuleGroup returns the global rule group if present.
|
|
func GetGlobalOpenFlareWAFRuleGroup(ctx context.Context) (*OpenFlareWAFRuleGroup, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var group OpenFlareWAFRuleGroup
|
|
if err = conn.Where("is_global = ?", true).Order("id asc").First(&group).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &group, nil
|
|
}
|
|
|
|
// CreateOpenFlareWAFRuleGroup inserts a rule group.
|
|
func CreateOpenFlareWAFRuleGroup(ctx context.Context, group *OpenFlareWAFRuleGroup) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Create(group).Error
|
|
}
|
|
|
|
// UpdateOpenFlareWAFRuleGroup updates mutable rule group fields.
|
|
func UpdateOpenFlareWAFRuleGroup(ctx context.Context, group *OpenFlareWAFRuleGroup) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Model(&OpenFlareWAFRuleGroup{}).Where("id = ?", group.ID).Updates(map[string]any{
|
|
"name": group.Name,
|
|
colEnabled: group.Enabled,
|
|
"is_global": group.IsGlobal,
|
|
}).Error
|
|
}
|
|
|
|
// UpdateOpenFlareWAFRuleGraph atomically replaces a graph when revision is current.
|
|
func UpdateOpenFlareWAFRuleGraph(ctx context.Context, id uint, revision uint64, graph string) (uint64, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
result := conn.Model(&OpenFlareWAFRuleGroup{}).
|
|
Where("id = ? AND revision = ?", id, revision).
|
|
Updates(map[string]any{"graph": graph, "revision": gorm.Expr("revision + 1")})
|
|
if result.Error != nil {
|
|
return 0, result.Error
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
return 0, ErrWAFRuleRevisionConflict
|
|
}
|
|
return revision + 1, nil
|
|
}
|
|
|
|
// DeleteOpenFlareWAFRuleGroup removes a rule group.
|
|
func DeleteOpenFlareWAFRuleGroup(ctx context.Context, id uint) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Delete(&OpenFlareWAFRuleGroup{}, id).Error
|
|
}
|
|
|
|
// ListOpenFlareWAFIPGroups returns all IP groups.
|
|
func ListOpenFlareWAFIPGroups(ctx context.Context) ([]*OpenFlareWAFIPGroup, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var groups []*OpenFlareWAFIPGroup
|
|
if err = conn.Order("type asc").Order("id asc").Find(&groups).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// ListOpenFlareWAFIPGroupsByIDs returns IP groups for the given ids.
|
|
func ListOpenFlareWAFIPGroupsByIDs(ctx context.Context, ids []uint) ([]*OpenFlareWAFIPGroup, error) {
|
|
if len(ids) == 0 {
|
|
return []*OpenFlareWAFIPGroup{}, nil
|
|
}
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var groups []*OpenFlareWAFIPGroup
|
|
if err = conn.Where("id IN ?", ids).Find(&groups).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// GetOpenFlareWAFIPGroupByID returns an IP group by id.
|
|
func GetOpenFlareWAFIPGroupByID(ctx context.Context, id uint) (*OpenFlareWAFIPGroup, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var group OpenFlareWAFIPGroup
|
|
if err = conn.First(&group, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &group, nil
|
|
}
|
|
|
|
// CreateOpenFlareWAFIPGroup inserts an IP group.
|
|
func CreateOpenFlareWAFIPGroup(ctx context.Context, group *OpenFlareWAFIPGroup) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Create(group).Error
|
|
}
|
|
|
|
// UpdateOpenFlareWAFIPGroup updates mutable IP group fields.
|
|
func UpdateOpenFlareWAFIPGroup(ctx context.Context, group *OpenFlareWAFIPGroup) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Model(&OpenFlareWAFIPGroup{}).Where("id = ?", group.ID).Updates(map[string]any{
|
|
"name": group.Name,
|
|
"type": group.Type,
|
|
"enabled": group.Enabled,
|
|
"ip_list": group.IPList,
|
|
"auto_config": group.AutoConfig,
|
|
"ext_ips": group.ExtIPs,
|
|
"subscription_url": group.SubscriptionURL,
|
|
"subscription_format": group.SubscriptionFormat,
|
|
"subscription_mapping_rule": group.SubscriptionMappingRule,
|
|
"sync_interval_minutes": group.SyncIntervalMinutes,
|
|
"next_sync_at": group.NextSyncAt,
|
|
"last_sync_status": group.LastSyncStatus,
|
|
"last_sync_message": group.LastSyncMessage,
|
|
}).Error
|
|
}
|
|
|
|
// ListDueOpenFlareWAFIPGroups returns enabled automatic/subscription groups due for sync.
|
|
func ListDueOpenFlareWAFIPGroups(ctx context.Context, now time.Time) ([]*OpenFlareWAFIPGroup, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var groups []*OpenFlareWAFIPGroup
|
|
err = conn.Where(
|
|
"enabled = ? AND (type = ? OR (type = ? AND subscription_url <> '')) AND (next_sync_at IS NULL OR next_sync_at <= ?)",
|
|
true, "automatic", "subscription", now,
|
|
).Order("id asc").Find(&groups).Error
|
|
return groups, err
|
|
}
|
|
|
|
// UpdateOpenFlareWAFIPGroupSyncResult persists IP group sync outcome fields.
|
|
func UpdateOpenFlareWAFIPGroupSyncResult(ctx context.Context, group *OpenFlareWAFIPGroup) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Model(&OpenFlareWAFIPGroup{}).Where("id = ?", group.ID).Updates(map[string]any{
|
|
"ip_list": group.IPList,
|
|
"ext_ips": group.ExtIPs,
|
|
"last_synced_at": group.LastSyncedAt,
|
|
"next_sync_at": group.NextSyncAt,
|
|
"last_sync_status": group.LastSyncStatus,
|
|
"last_sync_message": group.LastSyncMessage,
|
|
"subscription_format": group.SubscriptionFormat,
|
|
}).Error
|
|
}
|
|
|
|
// DeleteOpenFlareWAFIPGroup removes an IP group.
|
|
func DeleteOpenFlareWAFIPGroup(ctx context.Context, id uint) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Delete(&OpenFlareWAFIPGroup{}, id).Error
|
|
}
|
|
|
|
// ListOpenFlareWAFRuleGroupBindings returns all bindings.
|
|
func ListOpenFlareWAFRuleGroupBindings(ctx context.Context) ([]OpenFlareWAFRuleGroupBinding, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var bindings []OpenFlareWAFRuleGroupBinding
|
|
if err = conn.Order("sequence asc").Order("id asc").Find(&bindings).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return bindings, nil
|
|
}
|
|
|
|
// ListOpenFlareWAFRuleGroupBindingsByRouteID returns bindings for a proxy route.
|
|
func ListOpenFlareWAFRuleGroupBindingsByRouteID(ctx context.Context, routeID uint) ([]OpenFlareWAFRuleGroupBinding, error) {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var bindings []OpenFlareWAFRuleGroupBinding
|
|
if err = conn.Where("proxy_route_id = ?", routeID).Order("sequence asc").Order("id asc").Find(&bindings).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return bindings, nil
|
|
}
|
|
|
|
func syncWAFBindingIDSequence(tx *gorm.DB) error {
|
|
if tx == nil || tx.Dialector.Name() != "postgres" { //nolint:staticcheck // QF1008: keep explicit Dialector field access
|
|
return nil
|
|
}
|
|
return tx.Exec(`
|
|
SELECT setval(
|
|
pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'),
|
|
GREATEST(COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0), 1),
|
|
COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) > 0
|
|
)
|
|
`).Error
|
|
}
|
|
|
|
func insertOpenFlareWAFRuleGroupBindings(tx *gorm.DB, bindings []OpenFlareWAFRuleGroupBinding) error {
|
|
if len(bindings) == 0 {
|
|
return nil
|
|
}
|
|
if err := syncWAFBindingIDSequence(tx); err != nil {
|
|
return err
|
|
}
|
|
return tx.Create(&bindings).Error
|
|
}
|
|
|
|
// ReplaceOpenFlareWAFRuleGroupBindings replaces bindings for a rule group.
|
|
func ReplaceOpenFlareWAFRuleGroupBindings(ctx context.Context, groupID uint, routeIDs []uint) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Transaction(func(tx *gorm.DB) error {
|
|
if err = tx.Where("rule_group_id = ?", groupID).Delete(&OpenFlareWAFRuleGroupBinding{}).Error; err != nil {
|
|
return err
|
|
}
|
|
bindings := make([]OpenFlareWAFRuleGroupBinding, 0, len(routeIDs))
|
|
for index, routeID := range routeIDs {
|
|
bindings = append(bindings, OpenFlareWAFRuleGroupBinding{
|
|
RuleGroupID: groupID,
|
|
ProxyRouteID: routeID,
|
|
Sequence: index,
|
|
})
|
|
}
|
|
return insertOpenFlareWAFRuleGroupBindings(tx, bindings)
|
|
})
|
|
}
|
|
|
|
// ReplaceOpenFlareWAFSiteRuleGroupBindings replaces bindings for a proxy route.
|
|
func ReplaceOpenFlareWAFSiteRuleGroupBindings(ctx context.Context, routeID uint, groupIDs []uint) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Transaction(func(tx *gorm.DB) error {
|
|
if err = tx.Where("proxy_route_id = ?", routeID).Delete(&OpenFlareWAFRuleGroupBinding{}).Error; err != nil {
|
|
return err
|
|
}
|
|
bindings := make([]OpenFlareWAFRuleGroupBinding, 0, len(groupIDs))
|
|
for index, groupID := range groupIDs {
|
|
bindings = append(bindings, OpenFlareWAFRuleGroupBinding{
|
|
RuleGroupID: groupID,
|
|
ProxyRouteID: routeID,
|
|
Sequence: index,
|
|
})
|
|
}
|
|
return insertOpenFlareWAFRuleGroupBindings(tx, bindings)
|
|
})
|
|
}
|
|
|
|
// DeleteOpenFlareWAFRuleGroupBindingsByGroupID removes bindings for a rule group.
|
|
func DeleteOpenFlareWAFRuleGroupBindingsByGroupID(ctx context.Context, groupID uint) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Where("rule_group_id = ?", groupID).Delete(&OpenFlareWAFRuleGroupBinding{}).Error
|
|
}
|
|
|
|
// DeleteOpenFlareWAFRuleGroupWithBindings removes a rule group and its bindings.
|
|
func DeleteOpenFlareWAFRuleGroupWithBindings(ctx context.Context, groupID uint) error {
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Transaction(func(tx *gorm.DB) error {
|
|
if err = tx.Where("rule_group_id = ?", groupID).Delete(&OpenFlareWAFRuleGroupBinding{}).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Delete(&OpenFlareWAFRuleGroup{}, groupID).Error
|
|
})
|
|
}
|
|
|
|
// GetOpenFlareProxyRouteByID returns a proxy route by id when the table exists.
|
|
func GetOpenFlareProxyRouteByID(ctx context.Context, id uint) (*OriginProxyRoute, error) {
|
|
if !HasProxyRoutesTable(ctx) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
conn, err := wafDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var route OriginProxyRoute
|
|
if err = conn.First(&route, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &route, nil
|
|
}
|