feat:add plugin

#587
This commit is contained in:
samwaf
2025-12-26 23:48:28 +08:00
parent 986ddb9005
commit ae2321ed41
30 changed files with 3281 additions and 1 deletions
+1
View File
@@ -51,6 +51,7 @@ type APIGroup struct {
WafNotifySubscriptionApi
WafNotifyLogApi
WafFirewallIPBlockApi
WafPluginApi
}
var APIGroupAPP = new(APIGroup)
+314
View File
@@ -0,0 +1,314 @@
package api
import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/globalobj"
"SamWaf/model"
"SamWaf/model/common/response"
pluginconfig "SamWaf/plugin/config"
"encoding/json"
"github.com/gin-gonic/gin"
)
type WafPluginApi struct {
}
// GetListApi 获取插件列表
func (w *WafPluginApi) GetListApi(c *gin.Context) {
var plugins []model.WafPluginConfig
err := global.GWAF_LOCAL_DB.Find(&plugins).Error
if err != nil {
response.FailWithMessage("查询失败", c)
return
}
response.OkWithDetailed(plugins, "获取成功", c)
}
// GetDetailApi 获取插件详情
func (w *WafPluginApi) GetDetailApi(c *gin.Context) {
pluginID := c.Query("plugin_id")
if pluginID == "" {
response.FailWithMessage("插件ID不能为空", c)
return
}
var plugin model.WafPluginConfig
err := global.GWAF_LOCAL_DB.Where("plugin_id = ?", pluginID).First(&plugin).Error
if err != nil {
response.FailWithMessage("插件不存在", c)
return
}
response.OkWithDetailed(plugin, "获取成功", c)
}
// AddApi 添加插件
func (w *WafPluginApi) AddApi(c *gin.Context) {
var plugin model.WafPluginConfig
err := c.ShouldBindJSON(&plugin)
if err != nil {
response.FailWithMessage("参数解析失败", c)
return
}
// 检查插件ID是否已存在
var count int64
global.GWAF_LOCAL_DB.Model(&model.WafPluginConfig{}).Where("plugin_id = ?", plugin.PluginID).Count(&count)
if count > 0 {
response.FailWithMessage("插件ID已存在", c)
return
}
// 保存到数据库
err = global.GWAF_LOCAL_DB.Create(&plugin).Error
if err != nil {
response.FailWithMessage("添加失败", c)
return
}
// 如果插件已启用,尝试加载
if plugin.Enabled == 1 && globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
config := w.convertToPluginConfig(&plugin)
if err := globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.LoadPlugin(config); err != nil {
zlog.Error("加载插件失败", "plugin_id", plugin.PluginID, "error", err)
response.FailWithMessage("添加成功但加载失败: "+err.Error(), c)
return
}
}
response.OkWithMessage("添加成功", c)
}
// ModifyApi 修改插件
func (w *WafPluginApi) ModifyApi(c *gin.Context) {
var plugin model.WafPluginConfig
err := c.ShouldBindJSON(&plugin)
if err != nil {
response.FailWithMessage("参数解析失败", c)
return
}
// 更新数据库
err = global.GWAF_LOCAL_DB.Where("plugin_id = ?", plugin.PluginID).Updates(&plugin).Error
if err != nil {
response.FailWithMessage("更新失败", c)
return
}
// 重新加载插件
if globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
// 先卸载旧插件
globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.UnloadPlugin(plugin.PluginID)
// 如果启用,重新加载
if plugin.Enabled == 1 {
config := w.convertToPluginConfig(&plugin)
if err := globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.LoadPlugin(config); err != nil {
zlog.Error("重新加载插件失败", "plugin_id", plugin.PluginID, "error", err)
}
}
}
response.OkWithMessage("更新成功", c)
}
// DeleteApi 删除插件
func (w *WafPluginApi) DeleteApi(c *gin.Context) {
pluginID := c.Query("plugin_id")
if pluginID == "" {
response.FailWithMessage("插件ID不能为空", c)
return
}
// 从数据库删除
err := global.GWAF_LOCAL_DB.Where("plugin_id = ?", pluginID).Delete(&model.WafPluginConfig{}).Error
if err != nil {
response.FailWithMessage("删除失败", c)
return
}
// 卸载插件
if globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.UnloadPlugin(pluginID)
}
response.OkWithMessage("删除成功", c)
}
// ToggleApi 启用/禁用插件
func (w *WafPluginApi) ToggleApi(c *gin.Context) {
pluginID := c.Query("plugin_id")
if pluginID == "" {
response.FailWithMessage("插件ID不能为空", c)
return
}
// 查询插件
var plugin model.WafPluginConfig
err := global.GWAF_LOCAL_DB.Where("plugin_id = ?", pluginID).First(&plugin).Error
if err != nil {
response.FailWithMessage("插件不存在", c)
return
}
// 切换状态
newStatus := 0
if plugin.Enabled == 0 {
newStatus = 1
}
// 更新数据库
err = global.GWAF_LOCAL_DB.Model(&plugin).Update("enabled", newStatus).Error
if err != nil {
response.FailWithMessage("更新失败", c)
return
}
// 加载或卸载插件
if globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
if newStatus == 1 {
// 启用:加载插件
plugin.Enabled = 1
config := w.convertToPluginConfig(&plugin)
if err := globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.LoadPlugin(config); err != nil {
zlog.Error("加载插件失败", "plugin_id", pluginID, "error", err)
response.FailWithMessage("启用失败: "+err.Error(), c)
return
}
} else {
// 禁用:卸载插件
globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.UnloadPlugin(pluginID)
}
}
response.OkWithMessage("操作成功", c)
}
// GetSystemConfigApi 获取插件系统配置
func (w *WafPluginApi) GetSystemConfigApi(c *gin.Context) {
var configs []model.WafPluginSystemConfig
err := global.GWAF_LOCAL_DB.Find(&configs).Error
if err != nil {
response.FailWithMessage("查询失败", c)
return
}
// 转换为map格式
configMap := make(map[string]string)
for _, config := range configs {
configMap[config.Key] = config.Value
}
response.OkWithDetailed(configMap, "获取成功", c)
}
// UpdateSystemConfigApi 更新插件系统配置
func (w *WafPluginApi) UpdateSystemConfigApi(c *gin.Context) {
var req map[string]string
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage("参数解析失败", c)
return
}
// 更新配置
for key, value := range req {
var config model.WafPluginSystemConfig
err := global.GWAF_LOCAL_DB.Where("key = ?", key).First(&config).Error
if err == nil {
// 更新
global.GWAF_LOCAL_DB.Model(&config).Update("value", value)
} else {
// 创建
config = model.WafPluginSystemConfig{
Key: key,
Value: value,
}
global.GWAF_LOCAL_DB.Create(&config)
}
}
// 如果修改了enabled,更新插件管理器状态
if enabled, ok := req["enabled"]; ok && globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.SetEnabled(enabled == "1" || enabled == "true")
}
response.OkWithMessage("更新成功", c)
}
// GetPluginLogsApi 获取插件日志
func (w *WafPluginApi) GetPluginLogsApi(c *gin.Context) {
pluginID := c.Query("plugin_id")
page := c.DefaultQuery("page", "1")
pageSize := c.DefaultQuery("page_size", "20")
var logs []model.WafPluginLog
query := global.GWAF_LOCAL_DB.Model(&model.WafPluginLog{})
if pluginID != "" {
query = query.Where("plugin_id = ?", pluginID)
}
// 分页
var total int64
query.Count(&total)
var pageInt, pageSizeInt int
json.Unmarshal([]byte(page), &pageInt)
json.Unmarshal([]byte(pageSize), &pageSizeInt)
offset := (pageInt - 1) * pageSizeInt
err := query.Order("created_at desc").Offset(offset).Limit(pageSizeInt).Find(&logs).Error
if err != nil {
response.FailWithMessage("查询失败", c)
return
}
result := map[string]interface{}{
"list": logs,
"total": total,
"page": pageInt,
"page_size": pageSizeInt,
}
response.OkWithDetailed(result, "获取成功", c)
}
// convertToPluginConfig 转换数据库模型为插件配置
func (w *WafPluginApi) convertToPluginConfig(plugin *model.WafPluginConfig) *pluginconfig.PluginConfig {
config := &pluginconfig.PluginConfig{
ID: plugin.PluginID,
Name: plugin.Name,
Description: plugin.Description,
Type: plugin.Type,
Version: plugin.Version,
Enabled: plugin.Enabled == 1,
BinaryPath: plugin.BinaryPath,
Priority: plugin.Priority,
}
// 解析JSON字段
if plugin.Groups != "" {
json.Unmarshal([]byte(plugin.Groups), &config.Groups)
}
if plugin.Params != "" {
json.Unmarshal([]byte(plugin.Params), &config.Params)
}
if plugin.InputSchema != "" {
json.Unmarshal([]byte(plugin.InputSchema), &config.InputSchema)
}
if plugin.OutputSchema != "" {
json.Unmarshal([]byte(plugin.OutputSchema), &config.OutputSchema)
}
return config
}
+85
View File
@@ -0,0 +1,85 @@
# SamWaf 插件系统配置文件
# 详细说明请参考: SamWafDoc/plugin_dev/plugin-system-design.md
plugins:
# 插件系统总开关
enabled: true
# 插件二进制文件存放目录
binary_dir: "./data/plugins/binaries"
# 插件日志目录
log_dir: "./data/plugins/logs"
# 插件配置目录
config_dir: "./data/plugins/configs"
# 插件通信超时设置(秒)
timeout: 30
# go-plugin 框架日志级别
# 可选值: off, error, warn, info, debug, trace
# off - 关闭所有 go-plugin 框架日志
# error - 仅显示错误
# warn - 显示警告和错误
# info - 显示信息、警告和错误(推荐)
# debug - 显示调试信息(用于开发调试)
# trace - 显示所有跟踪信息(非常详细)
log_level: "warn"
# 插件自动重启配置
auto_restart: true # 是否启用自动重启
restart_interval: 3 # 重启间隔(秒)
max_restart_count: 5 # 单个插件最大重启次数(-1 表示无限制)
health_check_interval: 10 # 健康检查间隔(秒)
# 插件列表
list:
# Simple IP Blocker 插件 - 简单的IP屏蔽功能
- id: "simple_ip_blocker_001"
name: "Simple IP Blocker"
description: "屏蔽指定的IP地址(示例:8.8.8.8"
type: "ip_filter"
version: "1.0.0"
enabled: true # 默认关闭,编译插件后可启用
binary_path: "./data/plugins/binaries/simple_ip_blocker.exe"
priority: 100
# 插件分组(用于不同场景调用)
groups:
- "pre_check" # 预检查组(最早执行)
- "ip_filter" # IP过滤组
# 插件参数
params:
blocked_ips: # 要屏蔽的IP列表
- "8.8.8.8" # Google DNS
- "127.0.0.1" # Google DNS
block_reason: "DNS服务器 - 测试屏蔽"
# 输入参数定义
input_schema:
- name: "ip"
type: "string"
required: true
description: "待检查的IP地址"
- name: "request_path"
type: "string"
required: false
description: "请求路径"
- name: "user_agent"
type: "string"
required: false
description: "用户代理"
# 输出参数定义
output_schema:
- name: "allowed"
type: "boolean"
description: "是否允许通过"
- name: "reason"
type: "string"
description: "拒绝原因"
- name: "risk_level"
type: "int"
description: "风险等级(0-10"
+2
View File
@@ -1,6 +1,7 @@
package globalobj
import (
"SamWaf/plugin/manager"
"SamWaf/wafenginecore"
"SamWaf/waftask"
"SamWaf/waftunnelengine"
@@ -14,4 +15,5 @@ var (
GWAF_RUNTIME_OBJ_TUNNEL_ENGINE *waftunnelengine.WafTunnelEngine //当前tunnel引擎对象
GWAF_RUNTIME_OBJ_WAF_TaskRegistry *waftask.TaskRegistry // 任务执行器
GWAF_RUNTIME_OBJ_WAF_TaskScheduler *waftask.TaskScheduler // 任务计划
GWAF_RUNTIME_OBJ_PLUGIN_MANAGER *manager.PluginManager // 插件管理器
)
+8 -1
View File
@@ -91,6 +91,10 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.9.8 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.7.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.170 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
@@ -108,6 +112,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
@@ -153,7 +158,9 @@ require (
golang.org/x/sync v0.17.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/protobuf v1.36.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+25
View File
@@ -118,6 +118,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
@@ -191,6 +192,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -208,6 +211,12 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.170 h1:s/J9EsowKGHvmBO4CeY1RzlpYkczOpXxV/6v/+NJVVA=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.170/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/hyperjumptech/grule-rule-engine v1.15.0 h1:HqCjhZK+YsNC6udTR6/O90xRwxcefTwStheATUjYK34=
@@ -255,9 +264,12 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/magefile/mage v1.15.1-0.20231118170541-2385abb49a1f h1:iiLWLoibjCL0XND6inF7bs2nc20lU/FYkiR//VIOLUc=
github.com/magefile/mage v1.15.1-0.20231118170541-2385abb49a1f/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -274,6 +286,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
@@ -362,6 +376,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -523,7 +538,10 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -600,11 +618,16 @@ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNq
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -613,6 +636,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+20
View File
@@ -9,6 +9,7 @@ import (
"SamWaf/globalobj"
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"SamWaf/plugin"
"SamWaf/utils"
"SamWaf/wafconfig"
"SamWaf/wafdb"
@@ -260,6 +261,12 @@ func (m *wafSystenService) run() {
wafdb.InitLogDb("")
wafdb.InitStatsDb("")
//初始化插件系统(从配置文件加载)
if err := plugin.InitPluginSystem(); err != nil {
zlog.Error("初始化插件系统失败", "error", err)
// 插件系统初始化失败不影响主程序启动
}
//初始化队列引擎
wafqueue.InitDequeEngine()
//启动队列消费
@@ -287,6 +294,7 @@ func (m *wafSystenService) run() {
},
EngineCurrentStatus: 0, // 当前waf引擎状态
Sensitive: make([]model.Sensitive, 0),
PluginManager: globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER, // 设置插件管理器
}
http.Handle("/", globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartWaf()
@@ -705,6 +713,18 @@ func (m *wafSystenService) stopSamWaf() {
utils.CloseIPDatabase()
zlog.Info("Shutdown SamWaf IPDatabase finished")
// 关闭插件系统
zlog.Info("Shutdown SamWaf Plugin System...")
if globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
if err := globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.Shutdown(); err != nil {
zlog.Error("Shutdown Plugin System failed", zap.Error(err))
} else {
zlog.Info("Shutdown SamWaf Plugin System finished")
}
} else {
zlog.Warn("Plugin Manager is nil, skipping shutdown")
}
}
// 优雅升级
+42
View File
@@ -0,0 +1,42 @@
package model
import (
"SamWaf/model/baseorm"
)
// WafPluginConfig 插件配置表
type WafPluginConfig struct {
baseorm.BaseOrm
PluginID string `json:"plugin_id" gorm:"uniqueIndex;type:varchar(100);not null;comment:插件唯一ID"` // 插件唯一ID
Name string `json:"name" gorm:"type:varchar(100);not null;comment:插件名称"` // 插件名称
Description string `json:"description" gorm:"type:text;comment:插件描述"` // 插件描述
Type string `json:"type" gorm:"type:varchar(50);not null;comment:插件类型"` // 插件类型
Version string `json:"version" gorm:"type:varchar(20);not null;comment:插件版本"` // 插件版本
Enabled int `json:"enabled" gorm:"default:1;comment:是否启用 1启用 0禁用"` // 是否启用(1启用,0禁用)
BinaryPath string `json:"binary_path" gorm:"type:varchar(500);not null;comment:插件二进制路径"` // 插件二进制路径
Priority int `json:"priority" gorm:"default:50;comment:优先级 数字越大优先级越高"` // 优先级
Groups string `json:"groups" gorm:"type:text;comment:插件分组 JSON数组"` // 插件分组(JSON数组)
Params string `json:"params" gorm:"type:text;comment:插件参数 JSON对象"` // 插件参数(JSON对象)
InputSchema string `json:"input_schema" gorm:"type:text;comment:输入参数定义 JSON"` // 输入参数定义(JSON
OutputSchema string `json:"output_schema" gorm:"type:text;comment:输出参数定义 JSON"` // 输出参数定义(JSON
}
// WafPluginLog 插件执行日志表
type WafPluginLog struct {
baseorm.BaseOrm
PluginID string `json:"plugin_id" gorm:"type:varchar(100);not null;index;comment:插件ID"` // 插件ID
RequestID string `json:"request_id" gorm:"type:varchar(100);index;comment:请求ID"` // 请求ID
InputData string `json:"input_data" gorm:"type:text;comment:输入数据 JSON"` // 输入数据(JSON
OutputData string `json:"output_data" gorm:"type:text;comment:输出数据 JSON"` // 输出数据(JSON
ExecutionTime int64 `json:"execution_time" gorm:"comment:执行时间 毫秒"` // 执行时间(毫秒)
Status string `json:"status" gorm:"type:varchar(20);comment:执行状态 success/error"` // 执行状态(success/error
ErrorMsg string `json:"error_msg" gorm:"type:text;comment:错误信息"` // 错误信息
}
// WafPluginSystemConfig 插件系统配置表
type WafPluginSystemConfig struct {
baseorm.BaseOrm
Key string `json:"key" gorm:"uniqueIndex;type:varchar(100);not null;comment:配置键"` // 配置键
Value string `json:"value" gorm:"type:text;comment:配置值"` // 配置值
Description string `json:"description" gorm:"type:text;comment:配置描述"` // 配置描述
}
+57
View File
@@ -0,0 +1,57 @@
package config
// PluginSystemConfig 插件系统配置
type PluginSystemConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"` // 插件系统是否启用
BinaryDir string `yaml:"binary_dir" json:"binary_dir"` // 插件二进制文件目录
LogDir string `yaml:"log_dir" json:"log_dir"` // 插件日志目录
ConfigDir string `yaml:"config_dir" json:"config_dir"` // 插件配置目录
Timeout int `yaml:"timeout" json:"timeout"` // 插件通信超时时间(秒)
LogLevel string `yaml:"log_level" json:"log_level"` // go-plugin 框架日志级别 (off/error/warn/info/debug/trace)
AutoRestart bool `yaml:"auto_restart" json:"auto_restart"` // 是否启用自动重启
RestartInterval int `yaml:"restart_interval" json:"restart_interval"` // 重启间隔(秒)
MaxRestartCount int `yaml:"max_restart_count" json:"max_restart_count"` // 最大重启次数(-1 表示无限制)
HealthCheckInterval int `yaml:"health_check_interval" json:"health_check_interval"` // 健康检查间隔(秒)
List []PluginConfig `yaml:"list" json:"list"` // 插件列表
}
// PluginConfig 单个插件配置
type PluginConfig struct {
ID string `yaml:"id" json:"id"` // 插件唯一ID
Name string `yaml:"name" json:"name"` // 插件名称
Description string `yaml:"description" json:"description"` // 插件描述
Type string `yaml:"type" json:"type"` // 插件类型
Version string `yaml:"version" json:"version"` // 插件版本
Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用
BinaryPath string `yaml:"binary_path" json:"binary_path"` // 插件二进制路径
Priority int `yaml:"priority" json:"priority"` // 优先级(数字越大优先级越高)
Groups []string `yaml:"groups" json:"groups"` // 插件分组
Params map[string]interface{} `yaml:"params" json:"params"` // 插件参数
InputSchema []FieldSchema `yaml:"input_schema" json:"input_schema"` // 输入参数定义
OutputSchema []FieldSchema `yaml:"output_schema" json:"output_schema"` // 输出参数定义
}
// FieldSchema 字段定义
type FieldSchema struct {
Name string `yaml:"name" json:"name"` // 字段名称
Type string `yaml:"type" json:"type"` // 字段类型
Required bool `yaml:"required" json:"required"` // 是否必填
Description string `yaml:"description" json:"description"` // 字段描述
}
// DefaultPluginSystemConfig 默认插件系统配置
func DefaultPluginSystemConfig() *PluginSystemConfig {
return &PluginSystemConfig{
Enabled: true,
BinaryDir: "./data/plugins/binaries",
LogDir: "./data/plugins/logs",
ConfigDir: "./data/plugins/configs",
Timeout: 30,
LogLevel: "warn", // 默认只显示警告和错误
AutoRestart: true, // 默认启用自动重启
RestartInterval: 3, // 3 秒重启间隔
MaxRestartCount: 5, // 最多重启 5 次
HealthCheckInterval: 10, // 10 秒健康检查间隔
List: []PluginConfig{},
}
}
+141
View File
@@ -0,0 +1,141 @@
package plugin
import (
"SamWaf/common/zlog"
"SamWaf/globalobj"
pluginconfig "SamWaf/plugin/config"
"SamWaf/plugin/manager"
"SamWaf/utils"
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// InitPluginSystem 初始化插件系统
// 从 plugins.yml 配置文件加载插件配置(不使用数据库)
func InitPluginSystem() error {
zlog.Info("开始初始化插件系统...")
// 1. 读取插件配置文件
configPath := filepath.Join(utils.GetCurrentDir(), "conf", "plugins.yml")
systemConfig, err := loadPluginConfigFromFile(configPath)
if err != nil {
zlog.Error("加载插件配置文件失败", "path", configPath, "error", err)
// 配置文件加载失败不应该阻止系统启动,使用默认配置
zlog.Info("使用默认插件配置")
systemConfig = pluginconfig.DefaultPluginSystemConfig()
systemConfig.Enabled = false // 默认禁用
}
// 2. 创建插件管理器
pluginManager := manager.NewPluginManager(systemConfig)
globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER = pluginManager
// 3. 如果插件系统未启用,直接返回
if !systemConfig.Enabled {
zlog.Info("插件系统未启用,跳过插件加载")
return nil
}
// 4. 加载每个启用的插件
loadedCount := 0
for _, pluginConfig := range systemConfig.List {
if !pluginConfig.Enabled {
zlog.Debug("插件未启用,跳过加载", "plugin_id", pluginConfig.ID, "plugin_name", pluginConfig.Name)
continue
}
zlog.Info("加载插件", "plugin_id", pluginConfig.ID, "plugin_name", pluginConfig.Name)
if err := pluginManager.LoadPlugin(&pluginConfig); err != nil {
zlog.Error("加载插件失败", "plugin_id", pluginConfig.ID, "error", err)
continue
}
loadedCount++
zlog.Info("插件加载成功", "plugin_id", pluginConfig.ID, "plugin_name", pluginConfig.Name)
}
zlog.Info("插件系统初始化完成", "总插件数", len(systemConfig.List), "已加载", loadedCount)
return nil
}
// loadPluginConfigFromFile 从 YAML 配置文件加载插件配置
func loadPluginConfigFromFile(configPath string) (*pluginconfig.PluginSystemConfig, error) {
// 检查文件是否存在
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return nil, fmt.Errorf("配置文件不存在: %s", configPath)
}
// 读取文件内容
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
// 定义配置文件结构
type ConfigFile struct {
Plugins pluginconfig.PluginSystemConfig `yaml:"plugins"`
}
var configFile ConfigFile
if err := yaml.Unmarshal(data, &configFile); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
zlog.Info("成功加载插件配置文件",
"enabled", configFile.Plugins.Enabled,
"plugin_count", len(configFile.Plugins.List))
return &configFile.Plugins, nil
}
// SavePluginConfigToFile 保存插件配置到文件
// 供 API 使用,用于在线修改配置
func SavePluginConfigToFile(systemConfig *pluginconfig.PluginSystemConfig) error {
configPath := filepath.Join(utils.GetCurrentDir(), "conf", "plugins.yml")
// 构造配置文件结构
type ConfigFile struct {
Plugins pluginconfig.PluginSystemConfig `yaml:"plugins"`
}
configFile := ConfigFile{
Plugins: *systemConfig,
}
// 序列化为 YAML
data, err := yaml.Marshal(&configFile)
if err != nil {
return fmt.Errorf("序列化配置失败: %w", err)
}
// 备份原配置文件
backupPath := configPath + ".backup"
if _, err := os.Stat(configPath); err == nil {
os.Rename(configPath, backupPath)
}
// 写入文件
if err := os.WriteFile(configPath, data, 0644); err != nil {
// 如果写入失败,恢复备份
if _, err := os.Stat(backupPath); err == nil {
os.Rename(backupPath, configPath)
}
return fmt.Errorf("写入配置文件失败: %w", err)
}
zlog.Info("插件配置已保存", "path", configPath)
return nil
}
// ShutdownPluginSystem 关闭插件系统
func ShutdownPluginSystem() error {
if globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER == nil {
return nil
}
zlog.Info("关闭插件系统...")
return globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.Shutdown()
}
+28
View File
@@ -0,0 +1,28 @@
package plugininterface
import (
"context"
)
// IPFilterRequest IP过滤请求
type IPFilterRequest struct {
IP string `json:"ip"` // IP地址
RequestPath string `json:"request_path"` // 请求路径
UserAgent string `json:"user_agent"` // 用户代理
Extra map[string]interface{} `json:"extra"` // 额外信息
}
// IPFilterResponse IP过滤响应
type IPFilterResponse struct {
Allowed bool `json:"allowed"` // 是否允许
Reason string `json:"reason"` // 原因
RiskLevel int `json:"risk_level"` // 风险等级(0-10
}
// IPFilterPlugin IP过滤插件接口
type IPFilterPlugin interface {
Plugin
// Filter 执行IP过滤
Filter(ctx context.Context, req *IPFilterRequest) (*IPFilterResponse, error)
}
+37
View File
@@ -0,0 +1,37 @@
package plugininterface
import (
"context"
)
// Plugin 基础插件接口
type Plugin interface {
// Name 返回插件名称
Name() string
// Version 返回插件版本
Version() string
// Type 返回插件类型
Type() string
// Init 初始化插件
Init(config map[string]interface{}) error
// Shutdown 关闭插件
Shutdown() error
// HealthCheck 健康检查
HealthCheck(ctx context.Context) error
}
// PluginInfo 插件信息
type PluginInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
Version string `json:"version"`
Author string `json:"author"`
License string `json:"license"`
}
+34
View File
@@ -0,0 +1,34 @@
package plugininterface
import (
"context"
)
// WafCheckRequest WAF检查请求
type WafCheckRequest struct {
RequestID string `json:"request_id"` // 请求ID
IP string `json:"ip"` // 客户端IP
Method string `json:"method"` // 请求方法
URL string `json:"url"` // 请求URL
Headers map[string]string `json:"headers"` // 请求头
Body string `json:"body"` // 请求体
QueryParams map[string]string `json:"query_params"` // 查询参数
Extra map[string]interface{} `json:"extra"` // 额外信息
}
// WafCheckResponse WAF检查响应
type WafCheckResponse struct {
Allowed bool `json:"allowed"` // 是否允许通过
Reason string `json:"reason"` // 拒绝原因
RiskLevel int `json:"risk_level"` // 风险等级(0-10
Action string `json:"action"` // 建议动作(allow/block/captcha
Extra map[string]interface{} `json:"extra"` // 额外信息
}
// WafCheckPlugin WAF检查插件接口
type WafCheckPlugin interface {
Plugin
// Check 执行WAF检查
Check(ctx context.Context, req *WafCheckRequest) (*WafCheckResponse, error)
}
+618
View File
@@ -0,0 +1,618 @@
package manager
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"sort"
"strings"
"sync"
"time"
pluginconfig "SamWaf/plugin/config"
plugininterface "SamWaf/plugin/interface"
"SamWaf/plugin/registry"
"SamWaf/plugin/shared"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
)
// PluginInstance 插件实例
type PluginInstance struct {
ID string // 插件ID
Name string // 插件名称
Type string // 插件类型
Version string // 版本
Enabled bool // 是否启用
Priority int // 优先级
Groups []string // 分组
Plugin plugininterface.Plugin // 插件接口
Config map[string]interface{} // 插件配置
Client *plugin.Client // go-plugin 客户端(用于管理插件进程)
PluginConfig *pluginconfig.PluginConfig // 插件配置(用于重启)
RestartCount int // 重启次数
}
// PluginManager 插件管理器
type PluginManager struct {
mu sync.RWMutex
enabled bool // 插件系统是否启用
plugins map[string]*PluginInstance // 已加载的插件 key: plugin ID
registry *registry.Registry // 插件注册表
config *pluginconfig.PluginSystemConfig // 系统配置
stopChan chan struct{} // 停止信号
healthTicker *time.Ticker // 健康检查定时器
}
// NewPluginManager 创建插件管理器
func NewPluginManager(config *pluginconfig.PluginSystemConfig) *PluginManager {
if config == nil {
config = pluginconfig.DefaultPluginSystemConfig()
}
pm := &PluginManager{
enabled: config.Enabled,
plugins: make(map[string]*PluginInstance),
registry: registry.NewRegistry(),
config: config,
stopChan: make(chan struct{}),
}
// 如果启用自动重启,启动健康检查
if config.AutoRestart && config.HealthCheckInterval > 0 {
pm.startHealthCheck()
}
return pm
}
// IsEnabled 检查插件系统是否启用
func (pm *PluginManager) IsEnabled() bool {
pm.mu.RLock()
defer pm.mu.RUnlock()
return pm.enabled
}
// SetEnabled 设置插件系统启用状态
func (pm *PluginManager) SetEnabled(enabled bool) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.enabled = enabled
}
// LoadPlugin 加载插件
func (pm *PluginManager) LoadPlugin(pluginConfig *pluginconfig.PluginConfig) error {
pm.mu.Lock()
defer pm.mu.Unlock()
// 检查插件是否已加载
if _, exists := pm.plugins[pluginConfig.ID]; exists {
return fmt.Errorf("plugin already loaded: %s", pluginConfig.ID)
}
// 创建 go-plugin 客户端
fmt.Printf("[DEBUG] 正在创建插件客户端: %s\n", pluginConfig.ID)
fmt.Printf("[DEBUG] 插件路径: %s\n", pluginConfig.BinaryPath)
// 解析日志级别
logLevel, logOutput := parseLogLevel(pm.config.LogLevel)
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: shared.Handshake,
Plugins: shared.PluginMap,
Cmd: exec.Command(pluginConfig.BinaryPath),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC,
},
Logger: hclog.New(&hclog.LoggerOptions{
Name: pluginConfig.ID,
Output: logOutput, // 根据级别决定输出位置
Level: logLevel, // 使用配置的日志级别
}),
})
fmt.Printf("[DEBUG] 插件客户端已创建,正在连接...\n")
// 连接到插件
rpcClient, err := client.Client()
if err != nil {
fmt.Printf("[ERROR] 连接插件失败: %v\n", err)
client.Kill()
return fmt.Errorf("failed to get RPC client for plugin %s: %w", pluginConfig.ID, err)
}
fmt.Printf("[DEBUG] 已连接到插件,正在请求接口...\n")
// 请求插件接口
raw, err := rpcClient.Dispense("ip_filter")
if err != nil {
fmt.Printf("[ERROR] Dispense 插件失败: %v\n", err)
client.Kill()
return fmt.Errorf("failed to dispense plugin %s: %w", pluginConfig.ID, err)
}
fmt.Printf("[DEBUG] 已获取插件接口,类型: %T\n", raw)
// 类型断言为插件接口
pluginImpl, ok := raw.(plugininterface.IPFilterPlugin)
if !ok {
fmt.Printf("[ERROR] 类型断言失败,实际类型: %T\n", raw)
client.Kill()
return fmt.Errorf("plugin %s does not implement IPFilterPlugin interface", pluginConfig.ID)
}
fmt.Printf("[DEBUG] 类型断言成功,正在初始化插件...\n")
// 初始化插件
if err := pluginImpl.Init(pluginConfig.Params); err != nil {
fmt.Printf("[ERROR] 初始化插件失败: %v\n", err)
client.Kill()
return fmt.Errorf("failed to initialize plugin %s: %w", pluginConfig.ID, err)
}
fmt.Printf("[DEBUG] 插件初始化成功!\n")
// 创建插件实例
instance := &PluginInstance{
ID: pluginConfig.ID,
Name: pluginConfig.Name,
Type: pluginConfig.Type,
Version: pluginConfig.Version,
Enabled: pluginConfig.Enabled,
Priority: pluginConfig.Priority,
Groups: pluginConfig.Groups,
Config: pluginConfig.Params,
Plugin: pluginImpl, // 实际的插件实例(通过 gRPC 通信)
Client: client, // 保存客户端以便后续关闭
PluginConfig: pluginConfig, // 保存配置以便重启
RestartCount: 0, // 初始重启次数为 0
}
pm.plugins[pluginConfig.ID] = instance
// 注册到注册表
regInfo := &registry.PluginInfo{
ID: pluginConfig.ID,
Name: pluginConfig.Name,
Type: pluginConfig.Type,
Version: pluginConfig.Version,
Priority: pluginConfig.Priority,
Groups: pluginConfig.Groups,
Enabled: pluginConfig.Enabled,
}
return pm.registry.Register(regInfo)
}
// UnloadPlugin 卸载插件
func (pm *PluginManager) UnloadPlugin(pluginID string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
instance, exists := pm.plugins[pluginID]
if !exists {
return fmt.Errorf("plugin not found: %s", pluginID)
}
// 关闭插件
if instance.Plugin != nil {
if err := instance.Plugin.Shutdown(); err != nil {
return fmt.Errorf("failed to shutdown plugin: %w", err)
}
}
// 杀死插件进程
if instance.Client != nil {
instance.Client.Kill()
}
// 从映射中删除
delete(pm.plugins, pluginID)
// 从注册表注销
return pm.registry.Unregister(pluginID)
}
// GetPlugin 获取插件
func (pm *PluginManager) GetPlugin(pluginID string) (*PluginInstance, error) {
pm.mu.RLock()
defer pm.mu.RUnlock()
instance, exists := pm.plugins[pluginID]
if !exists {
return nil, fmt.Errorf("plugin not found: %s", pluginID)
}
return instance, nil
}
// GetPluginsByGroup 根据分组获取插件
func (pm *PluginManager) GetPluginsByGroup(group string) []*PluginInstance {
pm.mu.RLock()
defer pm.mu.RUnlock()
var instances []*PluginInstance
for _, instance := range pm.plugins {
if instance.Enabled && contains(instance.Groups, group) {
instances = append(instances, instance)
}
}
// 按优先级排序(优先级高的在前)
sort.Slice(instances, func(i, j int) bool {
return instances[i].Priority > instances[j].Priority
})
return instances
}
// GetAllPlugins 获取所有插件
func (pm *PluginManager) GetAllPlugins() []*PluginInstance {
pm.mu.RLock()
defer pm.mu.RUnlock()
instances := make([]*PluginInstance, 0, len(pm.plugins))
for _, instance := range pm.plugins {
instances = append(instances, instance)
}
return instances
}
// CallWafCheck 调用WAF检查插件
func (pm *PluginManager) CallWafCheck(ctx context.Context, pluginID string, req *plugininterface.WafCheckRequest) (*plugininterface.WafCheckResponse, error) {
instance, err := pm.GetPlugin(pluginID)
if err != nil {
return nil, err
}
if !instance.Enabled {
return nil, fmt.Errorf("plugin is disabled: %s", pluginID)
}
if instance.Plugin == nil {
return nil, fmt.Errorf("plugin not initialized: %s", pluginID)
}
wafPlugin, ok := instance.Plugin.(plugininterface.WafCheckPlugin)
if !ok {
return nil, fmt.Errorf("plugin is not a WafCheckPlugin: %s", pluginID)
}
return wafPlugin.Check(ctx, req)
}
// CallWafCheckByGroup 调用指定分组的所有WAF检查插件
func (pm *PluginManager) CallWafCheckByGroup(ctx context.Context, group string, req *plugininterface.WafCheckRequest) ([]*plugininterface.WafCheckResponse, error) {
instances := pm.GetPluginsByGroup(group)
var responses []*plugininterface.WafCheckResponse
for _, instance := range instances {
// 跳过未初始化的插件
if instance.Plugin == nil {
continue
}
wafPlugin, ok := instance.Plugin.(plugininterface.WafCheckPlugin)
if !ok {
continue
}
resp, err := wafPlugin.Check(ctx, req)
if err != nil {
// 记录错误但继续执行其他插件
// TODO: 添加日志记录
continue
}
responses = append(responses, resp)
// 如果某个插件返回不允许,可以选择立即返回或继续执行
// 这里继续执行所有插件,让调用者决定如何处理
}
return responses, nil
}
// CallIPFilter 调用IP过滤插件
func (pm *PluginManager) CallIPFilter(ctx context.Context, pluginID string, req *plugininterface.IPFilterRequest) (*plugininterface.IPFilterResponse, error) {
instance, err := pm.GetPlugin(pluginID)
if err != nil {
return nil, err
}
if !instance.Enabled {
return nil, fmt.Errorf("plugin is disabled: %s", pluginID)
}
if instance.Plugin == nil {
return nil, fmt.Errorf("plugin not initialized: %s", pluginID)
}
ipPlugin, ok := instance.Plugin.(plugininterface.IPFilterPlugin)
if !ok {
return nil, fmt.Errorf("plugin is not an IPFilterPlugin: %s", pluginID)
}
return ipPlugin.Filter(ctx, req)
}
// CallIPFilterByGroup 调用指定分组的所有IP过滤插件
func (pm *PluginManager) CallIPFilterByGroup(ctx context.Context, group string, req *plugininterface.IPFilterRequest) ([]*plugininterface.IPFilterResponse, error) {
instances := pm.GetPluginsByGroup(group)
var responses []*plugininterface.IPFilterResponse
for _, instance := range instances {
// 跳过未初始化的插件
if instance.Plugin == nil {
continue
}
ipPlugin, ok := instance.Plugin.(plugininterface.IPFilterPlugin)
if !ok {
continue
}
resp, err := ipPlugin.Filter(ctx, req)
if err != nil {
// 记录错误但继续执行其他插件
continue
}
responses = append(responses, resp)
}
return responses, nil
}
// ReloadPlugin 重新加载插件
func (pm *PluginManager) ReloadPlugin(pluginID string) error {
// TODO: 实现重新加载逻辑
// 1. 卸载插件
// 2. 重新从配置加载
// 3. 初始化插件
return fmt.Errorf("reload plugin not implemented yet")
}
// CheckRequest 通用请求检查方法(供 WAF 引擎调用)
// 返回:isBlock bool, reason string
func (pm *PluginManager) CheckRequest(ctx context.Context, group string, ip string, requestPath string, userAgent string, method string, host string) (bool, string) {
// 检查插件系统是否启用
if !pm.IsEnabled() {
return false, ""
}
// 获取该分组的插件
instances := pm.GetPluginsByGroup(group)
if len(instances) == 0 {
return false, ""
}
// 构造请求
req := &plugininterface.IPFilterRequest{
IP: ip,
RequestPath: requestPath,
UserAgent: userAgent,
Extra: map[string]interface{}{
"method": method,
"host": host,
},
}
// 调用每个插件
for _, instance := range instances {
// 跳过未初始化的插件
if instance.Plugin == nil {
continue
}
// 尝试作为 IPFilterPlugin 调用
if ipPlugin, ok := instance.Plugin.(plugininterface.IPFilterPlugin); ok {
resp, err := ipPlugin.Filter(ctx, req)
if err != nil {
// 记录错误但继续
fmt.Printf("plugin %s filter error: %v\n", instance.ID, err)
continue
}
// 如果插件返回不允许,立即返回拦截
if resp != nil && !resp.Allowed {
return true, fmt.Sprintf("[插件:%s] %s", instance.Name, resp.Reason)
}
}
// 可以继续添加其他类型的插件调用(WafCheckPlugin 等)
}
// 所有插件都通过,不拦截
return false, ""
}
// Shutdown 关闭所有插件
func (pm *PluginManager) Shutdown() error {
// 停止健康检查
close(pm.stopChan)
if pm.healthTicker != nil {
pm.healthTicker.Stop()
}
pm.mu.Lock()
defer pm.mu.Unlock()
fmt.Printf("[PluginManager] 正在关闭所有插件,共 %d 个\n", len(pm.plugins))
for id, instance := range pm.plugins {
fmt.Printf("[PluginManager] 关闭插件: %s\n", id)
// 调用插件的 Shutdown 方法
if instance.Plugin != nil {
if err := instance.Plugin.Shutdown(); err != nil {
fmt.Printf("[PluginManager] 警告: 插件 %s 关闭失败: %v\n", id, err)
} else {
fmt.Printf("[PluginManager] 插件 %s 已正常关闭\n", id)
}
}
// 杀死插件进程
if instance.Client != nil {
instance.Client.Kill()
fmt.Printf("[PluginManager] 插件进程 %s 已终止\n", id)
}
}
// 清空插件映射
pm.plugins = make(map[string]*PluginInstance)
fmt.Printf("[PluginManager] 所有插件已关闭\n")
return nil
}
// contains 检查字符串是否在切片中
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// parseLogLevel 解析日志级别字符串
func parseLogLevel(levelStr string) (hclog.Level, io.Writer) {
levelStr = strings.ToLower(strings.TrimSpace(levelStr))
switch levelStr {
case "off", "none":
// 完全关闭日志输出
return hclog.Off, io.Discard
case "error":
return hclog.Error, os.Stderr
case "warn", "warning":
return hclog.Warn, os.Stderr
case "info":
return hclog.Info, os.Stderr
case "debug":
return hclog.Debug, os.Stderr
case "trace":
return hclog.Trace, os.Stderr
default:
// 默认使用 warn 级别
return hclog.Warn, os.Stderr
}
}
// startHealthCheck 启动健康检查协程
func (pm *PluginManager) startHealthCheck() {
interval := time.Duration(pm.config.HealthCheckInterval) * time.Second
pm.healthTicker = time.NewTicker(interval)
fmt.Printf("[PluginManager] 健康检查已启动,间隔: %d 秒\n", pm.config.HealthCheckInterval)
go func() {
for {
select {
case <-pm.stopChan:
fmt.Printf("[PluginManager] 健康检查已停止\n")
return
case <-pm.healthTicker.C:
pm.checkPluginHealth()
}
}
}()
}
// checkPluginHealth 检查所有插件的健康状态
func (pm *PluginManager) checkPluginHealth() {
pm.mu.RLock()
pluginsToCheck := make(map[string]*PluginInstance)
for id, instance := range pm.plugins {
if instance.Enabled {
pluginsToCheck[id] = instance
}
}
pm.mu.RUnlock()
for id, instance := range pluginsToCheck {
if instance.Client != nil && instance.Client.Exited() {
fmt.Printf("[PluginManager] 检测到插件 %s 已退出,准备重启\n", id)
pm.restartPlugin(id, instance)
}
}
}
// restartPlugin 重启插件
func (pm *PluginManager) restartPlugin(pluginID string, instance *PluginInstance) {
// 检查是否启用自动重启
if !pm.config.AutoRestart {
fmt.Printf("[PluginManager] 自动重启已禁用,插件 %s 不会重启\n", pluginID)
return
}
// 检查重启次数限制
if pm.config.MaxRestartCount >= 0 && instance.RestartCount >= pm.config.MaxRestartCount {
fmt.Printf("[PluginManager] 插件 %s 已达到最大重启次数 (%d),不再重启\n",
pluginID, pm.config.MaxRestartCount)
return
}
// 等待重启间隔
if pm.config.RestartInterval > 0 {
fmt.Printf("[PluginManager] 等待 %d 秒后重启插件 %s...\n",
pm.config.RestartInterval, pluginID)
time.Sleep(time.Duration(pm.config.RestartInterval) * time.Second)
}
pm.mu.Lock()
defer pm.mu.Unlock()
// 再次检查插件是否还在映射中(可能已被手动卸载)
currentInstance, exists := pm.plugins[pluginID]
if !exists || currentInstance != instance {
fmt.Printf("[PluginManager] 插件 %s 已被移除,取消重启\n", pluginID)
return
}
fmt.Printf("[PluginManager] 正在重启插件 %s (第 %d 次重启)\n",
pluginID, instance.RestartCount+1)
// 清理旧的客户端
if instance.Client != nil {
instance.Client.Kill()
}
// 从映射中删除旧实例
delete(pm.plugins, pluginID)
// 从注册表中注销(重要!避免 "already registered" 错误)
if err := pm.registry.Unregister(pluginID); err != nil {
fmt.Printf("[PluginManager] 警告: 注销插件 %s 失败: %v\n", pluginID, err)
}
// 暂时释放锁以避免死锁
pm.mu.Unlock()
// 重新加载插件
err := pm.LoadPlugin(instance.PluginConfig)
// 重新获取锁
pm.mu.Lock()
if err != nil {
fmt.Printf("[PluginManager] ❌ 插件 %s 重启失败: %v\n", pluginID, err)
// 不恢复旧实例,因为它已经被清理且注销了
// 如果需要继续尝试,下次健康检查会检测到插件不在映射中
fmt.Printf("[PluginManager] 插件 %s 已从管理器中移除,重启计数: %d\n",
pluginID, instance.RestartCount+1)
} else {
fmt.Printf("[PluginManager] ✅ 插件 %s 重启成功\n", pluginID)
// 更新重启计数
if newInstance, exists := pm.plugins[pluginID]; exists {
newInstance.RestartCount = instance.RestartCount + 1
fmt.Printf("[PluginManager] 插件 %s 重启计数已更新: %d\n",
pluginID, newInstance.RestartCount)
}
}
}
+60
View File
@@ -0,0 +1,60 @@
syntax = "proto3";
package proto;
option go_package = "SamWaf/plugin/proto";
// IPFilterRequest IP过滤请求
message IPFilterRequest {
string ip = 1; // IP地址
string request_path = 2; // 请求路径
string user_agent = 3; // 用户代理
map<string, string> extra = 4; // 额外信息
}
// IPFilterResponse IP过滤响应
message IPFilterResponse {
bool allowed = 1; // 是否允许
string reason = 2; // 原因
int32 risk_level = 3; // 风险等级(0-10
}
// PluginInfo 插件信息
message PluginInfo {
string name = 1; // 插件名称
string version = 2; // 插件版本
string type = 3; // 插件类型
}
// InitRequest 初始化请求
message InitRequest {
string config_json = 1; // 配置信息(JSON字符串)
}
// InitResponse 初始化响应
message InitResponse {
bool success = 1; // 是否成功
string error = 2; // 错误信息
}
// Empty 空消息
message Empty {}
// IPFilterPlugin IP过滤插件服务
service IPFilterPlugin {
// Name 获取插件名称
rpc Name(Empty) returns (PluginInfo);
// Init 初始化插件
rpc Init(InitRequest) returns (InitResponse);
// Filter 执行IP过滤
rpc Filter(IPFilterRequest) returns (IPFilterResponse);
// Shutdown 关闭插件
rpc Shutdown(Empty) returns (Empty);
// HealthCheck 健康检查
rpc HealthCheck(Empty) returns (Empty);
}
+422
View File
@@ -0,0 +1,422 @@
// Code generated by protoc-gen-go-grpc. (手写版本)
package proto
import (
"context"
"encoding/json"
plugininterface "SamWaf/plugin/interface"
"google.golang.org/grpc"
)
// IPFilterPluginServer gRPC 服务端接口
type IPFilterPluginServer interface {
Name(context.Context, *Empty) (*PluginInfo, error)
Init(context.Context, *InitRequest) (*InitResponse, error)
Filter(context.Context, *IPFilterRequest) (*IPFilterResponse, error)
Shutdown(context.Context, *Empty) (*Empty, error)
HealthCheck(context.Context, *Empty) (*Empty, error)
}
// IPFilterPluginClient gRPC 客户端接口
type IPFilterPluginClient interface {
Name(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PluginInfo, error)
Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error)
Filter(ctx context.Context, in *IPFilterRequest, opts ...grpc.CallOption) (*IPFilterResponse, error)
Shutdown(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
HealthCheck(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
}
// IPFilterRequest protobuf 消息
type IPFilterRequest struct {
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
RequestPath string `protobuf:"bytes,2,opt,name=request_path,json=requestPath,proto3" json:"request_path,omitempty"`
UserAgent string `protobuf:"bytes,3,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"`
Extra map[string]string `protobuf:"bytes,4,rep,name=extra,proto3" json:"extra,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *IPFilterRequest) Reset() {}
func (x *IPFilterRequest) String() string { return "IPFilterRequest" }
func (x *IPFilterRequest) ProtoMessage() {}
// IPFilterResponse protobuf 消息
type IPFilterResponse struct {
Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"`
Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"`
RiskLevel int32 `protobuf:"varint,3,opt,name=risk_level,json=riskLevel,proto3" json:"risk_level,omitempty"`
}
func (x *IPFilterResponse) Reset() {}
func (x *IPFilterResponse) String() string { return "IPFilterResponse" }
func (x *IPFilterResponse) ProtoMessage() {}
// PluginInfo protobuf 消息
type PluginInfo struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
}
func (x *PluginInfo) Reset() {}
func (x *PluginInfo) String() string { return "PluginInfo" }
func (x *PluginInfo) ProtoMessage() {}
// InitRequest protobuf 消息
type InitRequest struct {
ConfigJson string `protobuf:"bytes,1,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"`
}
func (x *InitRequest) Reset() {}
func (x *InitRequest) String() string { return "InitRequest" }
func (x *InitRequest) ProtoMessage() {}
// InitResponse protobuf 消息
type InitResponse struct {
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
}
func (x *InitResponse) Reset() {}
func (x *InitResponse) String() string { return "InitResponse" }
func (x *InitResponse) ProtoMessage() {}
// Empty protobuf 消息
type Empty struct{}
func (x *Empty) Reset() {}
func (x *Empty) String() string { return "Empty" }
func (x *Empty) ProtoMessage() {}
// GRPCServer 插件端的 gRPC 服务器适配器
type GRPCServer struct {
Impl plugininterface.IPFilterPlugin
}
func (m *GRPCServer) Name(ctx context.Context, req *Empty) (*PluginInfo, error) {
return &PluginInfo{
Name: m.Impl.Name(),
Version: m.Impl.Version(),
Type: m.Impl.Type(),
}, nil
}
func (m *GRPCServer) Init(ctx context.Context, req *InitRequest) (*InitResponse, error) {
var config map[string]interface{}
if err := json.Unmarshal([]byte(req.ConfigJson), &config); err != nil {
return &InitResponse{Success: false, Error: err.Error()}, nil
}
if err := m.Impl.Init(config); err != nil {
return &InitResponse{Success: false, Error: err.Error()}, nil
}
return &InitResponse{Success: true}, nil
}
func (m *GRPCServer) Filter(ctx context.Context, req *IPFilterRequest) (*IPFilterResponse, error) {
// 转换请求
pluginReq := &plugininterface.IPFilterRequest{
IP: req.Ip,
RequestPath: req.RequestPath,
UserAgent: req.UserAgent,
Extra: make(map[string]interface{}),
}
for k, v := range req.Extra {
pluginReq.Extra[k] = v
}
// 调用插件
resp, err := m.Impl.Filter(ctx, pluginReq)
if err != nil {
return nil, err
}
// 转换响应
return &IPFilterResponse{
Allowed: resp.Allowed,
Reason: resp.Reason,
RiskLevel: int32(resp.RiskLevel),
}, nil
}
func (m *GRPCServer) Shutdown(ctx context.Context, req *Empty) (*Empty, error) {
return &Empty{}, m.Impl.Shutdown()
}
func (m *GRPCServer) HealthCheck(ctx context.Context, req *Empty) (*Empty, error) {
return &Empty{}, m.Impl.HealthCheck(ctx)
}
// GRPCClient 主程序端的 gRPC 客户端适配器
type GRPCClient struct {
client IPFilterPluginClient
}
// NewGRPCClient 创建 GRPCClient 实例
func NewGRPCClient(client IPFilterPluginClient) *GRPCClient {
return &GRPCClient{client: client}
}
func (m *GRPCClient) Name() string {
resp, err := m.client.Name(context.Background(), &Empty{})
if err != nil {
return ""
}
return resp.Name
}
func (m *GRPCClient) Version() string {
resp, err := m.client.Name(context.Background(), &Empty{})
if err != nil {
return ""
}
return resp.Version
}
func (m *GRPCClient) Type() string {
resp, err := m.client.Name(context.Background(), &Empty{})
if err != nil {
return ""
}
return resp.Type
}
func (m *GRPCClient) Init(config map[string]interface{}) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
resp, err := m.client.Init(context.Background(), &InitRequest{
ConfigJson: string(configJSON),
})
if err != nil {
return err
}
if !resp.Success {
return json.Unmarshal([]byte(resp.Error), &err)
}
return nil
}
func (m *GRPCClient) Filter(ctx context.Context, req *plugininterface.IPFilterRequest) (*plugininterface.IPFilterResponse, error) {
// 转换请求
grpcReq := &IPFilterRequest{
Ip: req.IP,
RequestPath: req.RequestPath,
UserAgent: req.UserAgent,
Extra: make(map[string]string),
}
for k, v := range req.Extra {
if str, ok := v.(string); ok {
grpcReq.Extra[k] = str
}
}
// 调用 gRPC
resp, err := m.client.Filter(ctx, grpcReq)
if err != nil {
return nil, err
}
// 转换响应
return &plugininterface.IPFilterResponse{
Allowed: resp.Allowed,
Reason: resp.Reason,
RiskLevel: int(resp.RiskLevel),
}, nil
}
func (m *GRPCClient) Shutdown() error {
_, err := m.client.Shutdown(context.Background(), &Empty{})
return err
}
func (m *GRPCClient) HealthCheck(ctx context.Context) error {
_, err := m.client.HealthCheck(ctx, &Empty{})
return err
}
// 服务注册和客户端创建函数
// RegisterIPFilterPluginServer 注册服务
func RegisterIPFilterPluginServer(s *grpc.Server, srv IPFilterPluginServer) {
desc := &grpc.ServiceDesc{
ServiceName: "proto.IPFilterPlugin",
HandlerType: (*IPFilterPluginServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Name",
Handler: _IPFilterPlugin_Name_Handler,
},
{
MethodName: "Init",
Handler: _IPFilterPlugin_Init_Handler,
},
{
MethodName: "Filter",
Handler: _IPFilterPlugin_Filter_Handler,
},
{
MethodName: "Shutdown",
Handler: _IPFilterPlugin_Shutdown_Handler,
},
{
MethodName: "HealthCheck",
Handler: _IPFilterPlugin_HealthCheck_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "ip_filter.proto",
}
s.RegisterService(desc, srv)
}
func _IPFilterPlugin_Name_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IPFilterPluginServer).Name(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.IPFilterPlugin/Name",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IPFilterPluginServer).Name(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _IPFilterPlugin_Init_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IPFilterPluginServer).Init(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.IPFilterPlugin/Init",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IPFilterPluginServer).Init(ctx, req.(*InitRequest))
}
return interceptor(ctx, in, info, handler)
}
func _IPFilterPlugin_Filter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IPFilterRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IPFilterPluginServer).Filter(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.IPFilterPlugin/Filter",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IPFilterPluginServer).Filter(ctx, req.(*IPFilterRequest))
}
return interceptor(ctx, in, info, handler)
}
func _IPFilterPlugin_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IPFilterPluginServer).Shutdown(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.IPFilterPlugin/Shutdown",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IPFilterPluginServer).Shutdown(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _IPFilterPlugin_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IPFilterPluginServer).HealthCheck(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.IPFilterPlugin/HealthCheck",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IPFilterPluginServer).HealthCheck(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
// ipFilterPluginClient 是客户端实现
type ipFilterPluginClient struct {
cc grpc.ClientConnInterface
}
// NewIPFilterPluginClient 创建客户端
func NewIPFilterPluginClient(cc grpc.ClientConnInterface) IPFilterPluginClient {
return &ipFilterPluginClient{cc}
}
func (c *ipFilterPluginClient) Name(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PluginInfo, error) {
out := new(PluginInfo)
err := c.cc.Invoke(ctx, "/proto.IPFilterPlugin/Name", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *ipFilterPluginClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) {
out := new(InitResponse)
err := c.cc.Invoke(ctx, "/proto.IPFilterPlugin/Init", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *ipFilterPluginClient) Filter(ctx context.Context, in *IPFilterRequest, opts ...grpc.CallOption) (*IPFilterResponse, error) {
out := new(IPFilterResponse)
err := c.cc.Invoke(ctx, "/proto.IPFilterPlugin/Filter", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *ipFilterPluginClient) Shutdown(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) {
out := new(Empty)
err := c.cc.Invoke(ctx, "/proto.IPFilterPlugin/Shutdown", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *ipFilterPluginClient) HealthCheck(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) {
out := new(Empty)
err := c.cc.Invoke(ctx, "/proto.IPFilterPlugin/HealthCheck", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
+107
View File
@@ -0,0 +1,107 @@
package registry
import (
"fmt"
"sync"
)
// PluginInfo 插件注册信息
type PluginInfo struct {
ID string // 插件ID
Name string // 插件名称
Type string // 插件类型
Version string // 插件版本
Priority int // 优先级
Groups []string // 所属分组
Enabled bool // 是否启用
}
// Registry 插件注册表
type Registry struct {
mu sync.RWMutex
plugins map[string]*PluginInfo // key: plugin ID
}
// NewRegistry 创建插件注册表
func NewRegistry() *Registry {
return &Registry{
plugins: make(map[string]*PluginInfo),
}
}
// Register 注册插件
func (r *Registry) Register(info *PluginInfo) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.plugins[info.ID]; exists {
return fmt.Errorf("plugin already registered: %s", info.ID)
}
r.plugins[info.ID] = info
return nil
}
// Unregister 注销插件
func (r *Registry) Unregister(pluginID string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.plugins[pluginID]; !exists {
return fmt.Errorf("plugin not found: %s", pluginID)
}
delete(r.plugins, pluginID)
return nil
}
// Get 获取插件信息
func (r *Registry) Get(pluginID string) (*PluginInfo, error) {
r.mu.RLock()
defer r.mu.RUnlock()
info, exists := r.plugins[pluginID]
if !exists {
return nil, fmt.Errorf("plugin not found: %s", pluginID)
}
return info, nil
}
// GetByGroup 获取指定分组的所有插件
func (r *Registry) GetByGroup(group string) []*PluginInfo {
r.mu.RLock()
defer r.mu.RUnlock()
var result []*PluginInfo
for _, info := range r.plugins {
if info.Enabled && contains(info.Groups, group) {
result = append(result, info)
}
}
return result
}
// GetAll 获取所有插件信息
func (r *Registry) GetAll() []*PluginInfo {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]*PluginInfo, 0, len(r.plugins))
for _, info := range r.plugins {
result = append(result, info)
}
return result
}
// contains 检查字符串是否在切片中
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
+38
View File
@@ -0,0 +1,38 @@
package shared
import (
"context"
"SamWaf/plugin/proto"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// Handshake 是客户端和服务器之间的握手配置
// 这必须匹配,否则插件将无法加载
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "SAMWAF_PLUGIN",
MagicCookieValue: "samwaf-ip-filter-plugin",
}
// PluginMap 是插件类型的映射
var PluginMap = map[string]plugin.Plugin{
"ip_filter": &IPFilterGRPCPlugin{},
}
// IPFilterGRPCPlugin 是 go-plugin 的 Plugin 实现
type IPFilterGRPCPlugin struct {
plugin.Plugin
Impl proto.IPFilterPluginServer
}
func (p *IPFilterGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
proto.RegisterIPFilterPluginServer(s, p.Impl)
return nil
}
func (p *IPFilterGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return proto.NewGRPCClient(proto.NewIPFilterPluginClient(c)), nil
}
+2
View File
@@ -0,0 +1,2 @@
# 内置插件目录
# 将来在这里实现具体的插件
+336
View File
@@ -0,0 +1,336 @@
# Simple IP Blocker 插件
## 📝 插件简介
这是一个简单的IP屏蔽插件,用于演示 SamWaf 插件系统的基本功能。
**主要功能**:
- 屏蔽指定的IP地址
- 支持自定义屏蔽原因
- 支持动态添加/移除屏蔽IP
- 同时实现 IPFilterPlugin 和 WafCheckPlugin 接口
**示例场景**: 屏蔽 8.8.8.8Google DNS)的访问请求
---
## 🚀 快速开始
### 1. 编译插件
**Linux/Mac**:
```bash
cd plugins/builtin/simple_ip_blocker
chmod +x build.sh
./build.sh
```
**Windows**:
```cmd
cd plugins\builtin\simple_ip_blocker
build.bat
```
### 2. 配置插件
**方式一: 通过配置文件**
编辑 `conf/plugins.yml`:
```yaml
plugins:
enabled: true
list:
- id: "simple_ip_blocker_001"
name: "Simple IP Blocker"
description: "屏蔽指定的IP地址"
type: "ip_filter"
version: "1.0.0"
enabled: true
binary_path: "./data/plugins/binaries/simple_ip_blocker"
priority: 100
groups:
- "pre_check" # 在预检查阶段执行
- "ip_filter" # 属于IP过滤组
params:
blocked_ips: # 要屏蔽的IP列表
- "8.8.8.8"
- "8.8.4.4"
block_reason: "DNS服务器 - 禁止访问"
```
**方式二: 通过API**
```bash
# 添加插件配置
curl -X POST http://localhost:26666/api/v1/wafplugin/add \
-H "Content-Type: application/json" \
-d '{
"plugin_id": "simple_ip_blocker_001",
"name": "Simple IP Blocker",
"description": "屏蔽指定的IP地址",
"type": "ip_filter",
"version": "1.0.0",
"enabled": 1,
"binary_path": "./data/plugins/binaries/simple_ip_blocker",
"priority": 100,
"groups": "[\"pre_check\",\"ip_filter\"]",
"params": "{\"blocked_ips\":[\"8.8.8.8\",\"8.8.4.4\"],\"block_reason\":\"DNS服务器 - 禁止访问\"}"
}'
```
### 3. 启动 SamWaf
```bash
cd ../../../
go run main.go
```
插件将自动加载并开始工作!
---
## 🔧 配置说明
### 插件参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| `blocked_ips` | 数组 | 否 | 要屏蔽的IP列表 | `["8.8.8.8", "1.1.1.1"]` |
| `block_reason` | 字符串 | 否 | 屏蔽原因 | `"安全策略禁止访问"` |
### 插件分组
建议将此插件配置在以下分组:
- **pre_check**: 在所有检测之前执行,实现快速过滤
- **ip_filter**: IP过滤阶段,与其他IP过滤器一起工作
---
## 📋 工作原理
### 流程图
```
客户端请求
[WAF接收请求]
[pre_check 插件组] ← Simple IP Blocker 在此执行
检查IP是否在屏蔽列表?
├─ 是 → 返回拦截(Allowed: false, RiskLevel: 8
│ ↓
│ [WAF拦截请求]
│ ↓
│ 返回403
└─ 否 → 返回允许(Allowed: true, RiskLevel: 0
[继续后续检测]
转发到后端
```
### 检测逻辑
```go
1. 接收请求中的IP地址
2. 查找IP是否在屏蔽列表中
3. 如果在列表中
- 返回 Allowed: false
- 返回 RiskLevel: 8高风险
- 返回原因
4. 如果不在列表中
- 返回 Allowed: true
- 返回 RiskLevel: 0
```
---
## 🧪 测试
### 运行测试
```bash
cd plugins/builtin/simple_ip_blocker
go test -v
```
### 测试覆盖率
```bash
go test -cover
```
### 性能测试
```bash
go test -bench=. -benchmem
```
### 测试用例
```bash
# 测试屏蔽IP
curl http://localhost:8080 -H "X-Forwarded-For: 8.8.8.8"
# 预期: 403 Forbidden
# 测试正常IP
curl http://localhost:8080 -H "X-Forwarded-For: 192.168.1.1"
# 预期: 正常响应
```
---
## 📊 插件接口
### 实现的接口
#### 1. Plugin 基础接口
```go
type Plugin interface {
Name() string // 返回 "Simple IP Blocker"
Version() string // 返回 "1.0.0"
Type() string // 返回 "ip_filter"
Init(config map[string]interface{}) error // 初始化
Shutdown() error // 关闭
HealthCheck(ctx context.Context) error // 健康检查
}
```
#### 2. IPFilterPlugin 接口
```go
type IPFilterPlugin interface {
Plugin
Filter(ctx context.Context, req *IPFilterRequest) (*IPFilterResponse, error)
}
// 请求
type IPFilterRequest struct {
IP string
RequestPath string
UserAgent string
}
// 响应
type IPFilterResponse struct {
Allowed bool // 是否允许
Reason string // 原因
RiskLevel int // 风险等级 0-10
}
```
#### 3. WafCheckPlugin 接口
```go
type WafCheckPlugin interface {
Plugin
Check(ctx context.Context, req *WafCheckRequest) (*WafCheckResponse, error)
}
// 请求
type WafCheckRequest struct {
RequestID string
IP string
Method string
URL string
Headers map[string]string
}
// 响应
type WafCheckResponse struct {
Allowed bool
Reason string
RiskLevel int
Action string // "allow" / "block" / "captcha"
}
```
---
## 🔍 日志示例
```
[Simple IP Blocker] 插件初始化中...
[Simple IP Blocker] 添加屏蔽IP: 8.8.8.8
[Simple IP Blocker] 添加屏蔽IP: 8.8.4.4
[Simple IP Blocker] 初始化完成,当前屏蔽 2 个IP
[Simple IP Blocker] 检查IP: 192.168.1.1
[Simple IP Blocker] ✅ 允许IP: 192.168.1.1
[Simple IP Blocker] 检查IP: 8.8.8.8
[Simple IP Blocker] ⛔ 屏蔽IP: 8.8.8.8, 原因: DNS服务器 - 禁止访问
```
---
## 🎯 使用场景
### 1. 屏蔽公共DNS服务器
```yaml
params:
blocked_ips:
- "8.8.8.8" # Google DNS
- "8.8.4.4" # Google DNS
- "1.1.1.1" # Cloudflare DNS
- "1.0.0.1" # Cloudflare DNS
block_reason: "禁止DNS服务器直接访问"
```
### 2. 屏蔽已知的恶意IP
```yaml
params:
blocked_ips:
- "1.2.3.4"
- "5.6.7.8"
block_reason: "已知恶意IP"
```
### 3. 临时屏蔽某个IP
```yaml
params:
blocked_ips:
- "10.20.30.40"
block_reason: "临时屏蔽 - 异常行为"
```
---
## 🚧 限制与注意事项
### 当前限制
1. **未集成 go-plugin 框架**: 当前版本是示例实现,等待 go-plugin 集成
2. **静态IP列表**: 屏蔽IP列表在启动时加载,修改后需要重启插件
3. **内存存储**: IP列表存储在内存中,重启后需要重新加载
### 性能考虑
- **查找复杂度**: O(1) - 使用 map 存储,查找非常快
- **内存占用**: 每个IP约占 50-100 字节
- **并发安全**: 当前版本不支持并发修改(读取是安全的)
---
## 💬 问题反馈
如有问题或建议,请:
1. 查看插件日志
2. 运行测试用例
3. 提交 Issue
---
**插件版本**: v1.0.0
**创建日期**: 2025-12-26
**作者**: SamWaf Team
**许可**: Apache
@@ -0,0 +1,46 @@
@echo off
REM Simple IP Blocker 插件编译脚本 (Windows)
SET PLUGIN_NAME=simple_ip_blocker.exe
SET OUTPUT_DIR=..\..\..\data\plugins\binaries
echo ========================================
echo 编译 Simple IP Blocker 插件
echo ========================================
REM 创建输出目录
if not exist %OUTPUT_DIR% mkdir %OUTPUT_DIR%
REM 编译插件
echo 正在编译...
go build -o %PLUGIN_NAME%
if %ERRORLEVEL% EQU 0 (
echo ✅ 编译成功
REM 复制到运行时目录
echo 正在复制到运行时目录...
copy /Y %PLUGIN_NAME% %OUTPUT_DIR%\
if %ERRORLEVEL% EQU 0 (
echo ✅ 复制成功: %OUTPUT_DIR%\%PLUGIN_NAME%
echo.
echo 插件编译完成!
echo 二进制位置: %OUTPUT_DIR%\%PLUGIN_NAME%
echo.
echo 下一步:
echo 1. 配置插件(在 conf/plugins.yml 或通过API
echo 2. 启动 SamWaf
echo 3. 插件将自动加载并运行
) else (
echo ❌ 复制失败
exit /b 1
)
) else (
echo ❌ 编译失败
exit /b 1
)
echo ========================================
pause
@@ -0,0 +1,50 @@
#!/bin/bash
# Simple IP Blocker 插件编译脚本
PLUGIN_NAME="simple_ip_blocker"
OUTPUT_DIR="../../../data/plugins/binaries"
echo "========================================"
echo "编译 Simple IP Blocker 插件"
echo "========================================"
# 创建输出目录
mkdir -p $OUTPUT_DIR
# 编译插件
echo "正在编译..."
go build -o $PLUGIN_NAME
if [ $? -eq 0 ]; then
echo "✅ 编译成功"
# 复制到运行时目录
echo "正在复制到运行时目录..."
cp $PLUGIN_NAME $OUTPUT_DIR/
if [ $? -eq 0 ]; then
echo "✅ 复制成功: $OUTPUT_DIR/$PLUGIN_NAME"
# 设置执行权限
chmod +x $OUTPUT_DIR/$PLUGIN_NAME
echo ""
echo "插件编译完成!"
echo "二进制位置: $OUTPUT_DIR/$PLUGIN_NAME"
echo ""
echo "下一步:"
echo "1. 配置插件(在 conf/plugins.yml 或通过API"
echo "2. 启动 SamWaf"
echo "3. 插件将自动加载并运行"
else
echo "❌ 复制失败"
exit 1
fi
else
echo "❌ 编译失败"
exit 1
fi
echo "========================================"
+29
View File
@@ -0,0 +1,29 @@
package main
import (
"SamWaf/plugin/proto"
"SamWaf/plugin/shared"
"github.com/hashicorp/go-plugin"
)
// main 插件入口
// 使用 hashicorp/go-plugin 框架
func main() {
// 创建插件实例
pluginImpl := NewSimpleIPBlockerPlugin()
// 使用 go-plugin 提供服务
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: shared.Handshake,
Plugins: map[string]plugin.Plugin{
"ip_filter": &shared.IPFilterGRPCPlugin{
Impl: &proto.GRPCServer{
Impl: pluginImpl,
},
},
},
// 使用 gRPC 协议
GRPCServer: plugin.DefaultGRPCServer,
})
}
+279
View File
@@ -0,0 +1,279 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
plugininterface "SamWaf/plugin/interface"
"SamWaf/plugins/common/logger"
)
// SimpleIPBlockerPlugin 简单的IP屏蔽插件
// 功能:屏蔽指定的IP地址
type SimpleIPBlockerPlugin struct {
name string
version string
description string
blockedIPs map[string]string // IP -> 屏蔽原因
logger *logger.Logger // 日志记录器
}
// NewSimpleIPBlockerPlugin 创建插件实例
func NewSimpleIPBlockerPlugin() *SimpleIPBlockerPlugin {
plugin := &SimpleIPBlockerPlugin{
name: "Simple IP Blocker",
version: "1.0.0",
description: "屏蔽指定的IP地址",
blockedIPs: map[string]string{
"8.8.8.8": "Google DNS - 示例屏蔽",
},
}
return plugin
}
// ============ 实现 Plugin 基础接口 ============
// Name 返回插件名称
func (p *SimpleIPBlockerPlugin) Name() string {
return p.name
}
// Version 返回插件版本
func (p *SimpleIPBlockerPlugin) Version() string {
return p.version
}
// Type 返回插件类型
func (p *SimpleIPBlockerPlugin) Type() string {
return "ip_filter"
}
// Init 初始化插件
func (p *SimpleIPBlockerPlugin) Init(config map[string]interface{}) error {
fmt.Printf("[%s] 插件初始化中,配置项数量: %d\n", p.name, len(config))
// 初始化日志系统
// 获取当前工作目录(主程序的工作目录)
workDir, err := os.Getwd()
if err != nil {
fmt.Printf("[%s] 警告: 获取工作目录失败: %v,使用默认路径\n", p.name, err)
workDir = "."
}
// 构建日志目录的绝对路径
logDir := filepath.Join(workDir, "data", "plugins", "logs")
// 创建日志记录器
pluginLogger, err := logger.NewLogger(
p.name, // 插件名称
logDir, // 日志目录
"simple_ip_blocker_001", // 插件ID
)
if err != nil {
fmt.Printf("[%s] 警告: 日志初始化失败: %v,将使用 fmt 输出\n", p.name, err)
} else {
p.logger = pluginLogger
p.logger.Info("插件初始化开始", "config_keys", len(config))
}
// 从配置中读取要屏蔽的IP列表
if blockedIPs, ok := config["blocked_ips"].([]interface{}); ok {
if p.logger != nil {
p.logger.Info("从配置读取屏蔽IP列表", "count", len(blockedIPs))
} else {
fmt.Printf("[%s] 从配置读取屏蔽IP列表,数量: %d\n", p.name, len(blockedIPs))
}
for _, ip := range blockedIPs {
if ipStr, ok := ip.(string); ok {
p.blockedIPs[ipStr] = "配置中指定的屏蔽IP"
if p.logger != nil {
p.logger.Info("添加屏蔽IP", "ip", ipStr)
} else {
fmt.Printf("[%s] 添加屏蔽IP: %s\n", p.name, ipStr)
}
}
}
}
// 从配置中读取自定义屏蔽原因
if reason, ok := config["block_reason"].(string); ok && reason != "" {
// 更新所有IP的屏蔽原因
for ip := range p.blockedIPs {
p.blockedIPs[ip] = reason
}
if p.logger != nil {
p.logger.Info("更新屏蔽原因", "reason", reason)
} else {
fmt.Printf("[%s] 更新屏蔽原因: %s\n", p.name, reason)
}
}
if p.logger != nil {
p.logger.Info("插件初始化完成", "blocked_ip_count", len(p.blockedIPs))
} else {
fmt.Printf("[%s] 初始化完成,当前屏蔽 %d 个IP\n", p.name, len(p.blockedIPs))
}
return nil
}
// Shutdown 关闭插件
func (p *SimpleIPBlockerPlugin) Shutdown() error {
if p.logger != nil {
p.logger.Info("插件关闭中...")
} else {
fmt.Printf("[%s] 插件关闭中...\n", p.name)
}
p.blockedIPs = make(map[string]string)
if p.logger != nil {
p.logger.Info("插件已关闭")
if err := p.logger.Close(); err != nil {
return fmt.Errorf("关闭日志失败: %w", err)
}
} else {
fmt.Printf("[%s] 插件已关闭\n", p.name)
}
return nil
}
// HealthCheck 健康检查
func (p *SimpleIPBlockerPlugin) HealthCheck(ctx context.Context) error {
// 简单的健康检查,确认插件运行正常
if p.blockedIPs == nil {
return fmt.Errorf("插件未初始化")
}
return nil
}
// ============ 实现 IPFilterPlugin 接口 ============
// Filter 执行IP过滤
func (p *SimpleIPBlockerPlugin) Filter(ctx context.Context, req *plugininterface.IPFilterRequest) (*plugininterface.IPFilterResponse, error) {
ip := req.IP
if p.logger != nil {
p.logger.Debug("开始IP过滤检查", "ip", ip, "path", req.RequestPath, "user_agent", req.UserAgent)
} else {
fmt.Printf("[%s] 检查IP: %s, 路径: %s\n", p.name, ip, req.RequestPath)
}
// 检查IP是否在屏蔽列表中
if reason, blocked := p.blockedIPs[ip]; blocked {
if p.logger != nil {
p.logger.Warn("⛔ IP被屏蔽", "ip", ip, "reason", reason, "path", req.RequestPath)
} else {
fmt.Printf("[%s] ⛔ 屏蔽IP: %s, 原因: %s\n", p.name, ip, reason)
}
return &plugininterface.IPFilterResponse{
Allowed: false,
Reason: fmt.Sprintf("IP已被屏蔽: %s", reason),
RiskLevel: 8, // 风险等级 0-108表示高风险
}, nil
}
// IP不在屏蔽列表,允许通过
if p.logger != nil {
p.logger.Debug("✅ IP检查通过", "ip", ip)
} else {
fmt.Printf("[%s] ✅ 允许IP: %s\n", p.name, ip)
}
return &plugininterface.IPFilterResponse{
Allowed: true,
Reason: "IP检查通过",
RiskLevel: 0,
}, nil
}
// ============ 实现 WafCheckPlugin 接口(支持WAF检查) ============
// Check 执行WAF检查
func (p *SimpleIPBlockerPlugin) Check(ctx context.Context, req *plugininterface.WafCheckRequest) (*plugininterface.WafCheckResponse, error) {
ip := req.IP
if p.logger != nil {
p.logger.Debug("开始WAF检查", "ip", ip, "url", req.URL, "method", req.Method)
} else {
fmt.Printf("[%s] WAF检查 - IP: %s, URL: %s\n", p.name, ip, req.URL)
}
// 检查IP是否在屏蔽列表中
if reason, blocked := p.blockedIPs[ip]; blocked {
if p.logger != nil {
p.logger.Warn("⛔ WAF拦截", "ip", ip, "url", req.URL, "reason", reason)
} else {
fmt.Printf("[%s] ⛔ WAF拦截 - IP: %s, 原因: %s\n", p.name, ip, reason)
}
return &plugininterface.WafCheckResponse{
Allowed: false,
Reason: fmt.Sprintf("IP已被屏蔽: %s", reason),
RiskLevel: 8,
Action: "block", // 建议动作:拦截
Extra: map[string]interface{}{
"blocked_ip": ip,
"plugin": p.name,
},
}, nil
}
// IP允许通过
if p.logger != nil {
p.logger.Debug("✅ WAF检查通过", "ip", ip, "url", req.URL)
} else {
fmt.Printf("[%s] ✅ WAF通过 - IP: %s\n", p.name, ip)
}
return &plugininterface.WafCheckResponse{
Allowed: true,
Reason: "IP检查通过",
RiskLevel: 0,
Action: "allow",
Extra: map[string]interface{}{
"checked_ip": ip,
"plugin": p.name,
},
}, nil
}
// ============ 辅助方法 ============
// AddBlockedIP 动态添加要屏蔽的IP
func (p *SimpleIPBlockerPlugin) AddBlockedIP(ip string, reason string) {
p.blockedIPs[ip] = reason
if p.logger != nil {
p.logger.Info("添加屏蔽IP", "ip", ip, "reason", reason)
} else {
fmt.Printf("[%s] 添加屏蔽IP: %s, 原因: %s\n", p.name, ip, reason)
}
}
// RemoveBlockedIP 移除屏蔽的IP
func (p *SimpleIPBlockerPlugin) RemoveBlockedIP(ip string) {
if _, exists := p.blockedIPs[ip]; exists {
delete(p.blockedIPs, ip)
if p.logger != nil {
p.logger.Info("移除屏蔽IP", "ip", ip)
} else {
fmt.Printf("[%s] 移除屏蔽IP: %s\n", p.name, ip)
}
}
}
// GetBlockedIPs 获取所有屏蔽的IP列表
func (p *SimpleIPBlockerPlugin) GetBlockedIPs() map[string]string {
return p.blockedIPs
}
// IsBlocked 检查IP是否被屏蔽
func (p *SimpleIPBlockerPlugin) IsBlocked(ip string) (bool, string) {
reason, blocked := p.blockedIPs[ip]
return blocked, reason
}
@@ -0,0 +1,218 @@
package main
import (
"context"
"testing"
plugininterface "SamWaf/plugin/interface"
)
// TestPluginBasics 测试插件基础功能
func TestPluginBasics(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
// 测试基础信息
if plugin.Name() != "Simple IP Blocker" {
t.Errorf("期望插件名称为 'Simple IP Blocker', 实际得到 '%s'", plugin.Name())
}
if plugin.Version() != "1.0.0" {
t.Errorf("期望版本为 '1.0.0', 实际得到 '%s'", plugin.Version())
}
if plugin.Type() != "ip_filter" {
t.Errorf("期望类型为 'ip_filter', 实际得到 '%s'", plugin.Type())
}
}
// TestPluginInit 测试插件初始化
func TestPluginInit(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
config := map[string]interface{}{
"blocked_ips": []interface{}{
"1.1.1.1",
"2.2.2.2",
},
"block_reason": "测试屏蔽",
}
err := plugin.Init(config)
if err != nil {
t.Fatalf("插件初始化失败: %v", err)
}
// 验证屏蔽IP已加载
blockedIPs := plugin.GetBlockedIPs()
if len(blockedIPs) < 2 {
t.Errorf("期望至少有2个屏蔽IP,实际有 %d 个", len(blockedIPs))
}
}
// TestIPFilter 测试IP过滤功能
func TestIPFilter(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
plugin.Init(map[string]interface{}{})
ctx := context.Background()
// 测试用例
testCases := []struct {
name string
ip string
expectAllowed bool
expectRisk int
}{
{
name: "屏蔽IP - 8.8.8.8",
ip: "8.8.8.8",
expectAllowed: false,
expectRisk: 8,
},
{
name: "正常IP - 192.168.1.1",
ip: "192.168.1.1",
expectAllowed: true,
expectRisk: 0,
},
{
name: "正常IP - 10.0.0.1",
ip: "10.0.0.1",
expectAllowed: true,
expectRisk: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := &plugininterface.IPFilterRequest{
IP: tc.ip,
RequestPath: "/test",
UserAgent: "test-agent",
}
resp, err := plugin.Filter(ctx, req)
if err != nil {
t.Fatalf("过滤失败: %v", err)
}
if resp.Allowed != tc.expectAllowed {
t.Errorf("IP %s: 期望 Allowed=%v, 实际得到 %v",
tc.ip, tc.expectAllowed, resp.Allowed)
}
if resp.RiskLevel != tc.expectRisk {
t.Errorf("IP %s: 期望 RiskLevel=%d, 实际得到 %d",
tc.ip, tc.expectRisk, resp.RiskLevel)
}
})
}
}
// TestWafCheck 测试WAF检查功能
func TestWafCheck(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
plugin.Init(map[string]interface{}{})
ctx := context.Background()
// 测试屏蔽IP
req := &plugininterface.WafCheckRequest{
RequestID: "test-001",
IP: "8.8.8.8",
Method: "GET",
URL: "/api/test",
Headers: map[string]string{
"User-Agent": "test-agent",
},
}
resp, err := plugin.Check(ctx, req)
if err != nil {
t.Fatalf("WAF检查失败: %v", err)
}
if resp.Allowed {
t.Error("期望屏蔽 8.8.8.8,但实际允许通过")
}
if resp.RiskLevel != 8 {
t.Errorf("期望风险等级为 8,实际得到 %d", resp.RiskLevel)
}
if resp.Action != "block" {
t.Errorf("期望动作为 'block',实际得到 '%s'", resp.Action)
}
}
// TestDynamicIPManagement 测试动态IP管理
func TestDynamicIPManagement(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
plugin.Init(map[string]interface{}{})
// 添加新的屏蔽IP
plugin.AddBlockedIP("3.3.3.3", "动态添加的屏蔽IP")
// 验证已添加
blocked, reason := plugin.IsBlocked("3.3.3.3")
if !blocked {
t.Error("期望 3.3.3.3 被屏蔽,但实际未屏蔽")
}
if reason != "动态添加的屏蔽IP" {
t.Errorf("期望原因为 '动态添加的屏蔽IP',实际得到 '%s'", reason)
}
// 移除屏蔽IP
plugin.RemoveBlockedIP("3.3.3.3")
// 验证已移除
blocked, _ = plugin.IsBlocked("3.3.3.3")
if blocked {
t.Error("期望 3.3.3.3 已移除屏蔽,但实际仍被屏蔽")
}
}
// TestHealthCheck 测试健康检查
func TestHealthCheck(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
plugin.Init(map[string]interface{}{})
ctx := context.Background()
err := plugin.HealthCheck(ctx)
if err != nil {
t.Errorf("健康检查失败: %v", err)
}
}
// TestShutdown 测试插件关闭
func TestShutdown(t *testing.T) {
plugin := NewSimpleIPBlockerPlugin()
plugin.Init(map[string]interface{}{})
err := plugin.Shutdown()
if err != nil {
t.Errorf("插件关闭失败: %v", err)
}
// 验证资源已清理
if len(plugin.blockedIPs) != 0 {
t.Error("期望屏蔽IP列表已清空,但实际还有数据")
}
}
// BenchmarkIPFilter 性能基准测试
func BenchmarkIPFilter(b *testing.B) {
plugin := NewSimpleIPBlockerPlugin()
plugin.Init(map[string]interface{}{})
ctx := context.Background()
req := &plugininterface.IPFilterRequest{
IP: "192.168.1.1",
RequestPath: "/test",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
plugin.Filter(ctx, req)
}
}
+184
View File
@@ -0,0 +1,184 @@
package logger
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
"gopkg.in/natefinch/lumberjack.v2"
)
// Logger 插件日志记录器
type Logger struct {
logger *log.Logger
logFile *lumberjack.Logger
pluginName string
mu sync.Mutex
}
var (
globalLoggers = make(map[string]*Logger)
loggerMu sync.RWMutex
)
// NewLogger 创建新的日志记录器
// pluginName: 插件名称(用于日志标识)
// logDir: 日志目录
// pluginID: 插件ID(用于日志文件名)
func NewLogger(pluginName string, logDir string, pluginID string) (*Logger, error) {
// 检查是否已存在
loggerMu.RLock()
if existingLogger, exists := globalLoggers[pluginID]; exists {
loggerMu.RUnlock()
return existingLogger, nil
}
loggerMu.RUnlock()
// 确保日志目录存在
if err := os.MkdirAll(logDir, 0755); err != nil {
return nil, fmt.Errorf("创建日志目录失败: %w", err)
}
// 日志文件名:pluginID.log
logFileName := fmt.Sprintf("%s.log", pluginID)
logFilePath := filepath.Join(logDir, logFileName)
// 配置 lumberjack 日志轮转
lumberJackLogger := &lumberjack.Logger{
Filename: logFilePath,
MaxSize: 50, // 每个日志文件最大 50MB
MaxBackups: 10, // 保留 10 个备份
MaxAge: 7, // 保留 7 天
Compress: true, // 压缩旧日志
}
// 创建多写入器:同时写入文件和标准输出
multiWriter := io.MultiWriter(os.Stdout, lumberJackLogger)
// 创建 logger
logger := log.New(multiWriter, "", 0)
pluginLogger := &Logger{
logger: logger,
logFile: lumberJackLogger,
pluginName: pluginName,
}
// 缓存日志实例
loggerMu.Lock()
globalLoggers[pluginID] = pluginLogger
loggerMu.Unlock()
pluginLogger.Info("插件日志系统初始化成功", "log_file", logFilePath)
return pluginLogger, nil
}
// GetLogger 获取已存在的日志实例
func GetLogger(pluginID string) (*Logger, bool) {
loggerMu.RLock()
defer loggerMu.RUnlock()
logger, exists := globalLoggers[pluginID]
return logger, exists
}
// formatMessage 格式化日志消息
func (l *Logger) formatMessage(level string, msg string, keysAndValues ...interface{}) string {
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
message := fmt.Sprintf("[%s] [%s] [%s] %s", timestamp, level, l.pluginName, msg)
// 添加额外的字段
if len(keysAndValues) > 0 {
for i := 0; i < len(keysAndValues); i += 2 {
if i+1 < len(keysAndValues) {
message += fmt.Sprintf(" %v=%v", keysAndValues[i], keysAndValues[i+1])
}
}
}
return message
}
// Debug 调试日志
func (l *Logger) Debug(msg string, keysAndValues ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
message := l.formatMessage("DEBUG", msg, keysAndValues...)
l.logger.Println(message)
}
// Info 信息日志
func (l *Logger) Info(msg string, keysAndValues ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
message := l.formatMessage("INFO", msg, keysAndValues...)
l.logger.Println(message)
}
// Warn 警告日志
func (l *Logger) Warn(msg string, keysAndValues ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
message := l.formatMessage("WARN", msg, keysAndValues...)
l.logger.Println(message)
}
// Error 错误日志
func (l *Logger) Error(msg string, keysAndValues ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
message := l.formatMessage("ERROR", msg, keysAndValues...)
l.logger.Println(message)
}
// Fatal 致命错误日志
func (l *Logger) Fatal(msg string, keysAndValues ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
message := l.formatMessage("FATAL", msg, keysAndValues...)
l.logger.Println(message)
}
// Close 关闭日志
func (l *Logger) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.logFile != nil {
return l.logFile.Close()
}
return nil
}
// CloseAll 关闭所有日志实例
func CloseAll() error {
loggerMu.Lock()
defer loggerMu.Unlock()
var lastErr error
for id, logger := range globalLoggers {
if err := logger.Close(); err != nil {
lastErr = err
}
delete(globalLoggers, id)
}
return lastErr
}
// GetAllLoggers 获取所有日志实例(用于调试)
func GetAllLoggers() map[string]*Logger {
loggerMu.RLock()
defer loggerMu.RUnlock()
result := make(map[string]*Logger, len(globalLoggers))
for k, v := range globalLoggers {
result[k] = v
}
return result
}
+2
View File
@@ -0,0 +1,2 @@
# 外部插件目录
# 用户自定义插件放在这里
+1
View File
@@ -49,6 +49,7 @@ type ApiGroup struct {
NotifySubscriptionRouter
NotifyLogRouter
FirewallIPBlockRouter
PluginRouter
}
type PublicApiGroup struct {
LoginRouter
+29
View File
@@ -0,0 +1,29 @@
package router
import (
"SamWaf/api"
"github.com/gin-gonic/gin"
)
type PluginRouter struct {
}
func (receiver *PluginRouter) InitPluginRouter(group *gin.RouterGroup) {
apiInstance := api.APIGroupAPP.WafPluginApi
router := group.Group("")
// 插件管理
router.POST("/api/v1/wafplugin/list", apiInstance.GetListApi)
router.GET("/api/v1/wafplugin/detail", apiInstance.GetDetailApi)
router.POST("/api/v1/wafplugin/add", apiInstance.AddApi)
router.POST("/api/v1/wafplugin/modify", apiInstance.ModifyApi)
router.GET("/api/v1/wafplugin/del", apiInstance.DeleteApi)
router.POST("/api/v1/wafplugin/toggle", apiInstance.ToggleApi)
// 系统配置
router.GET("/api/v1/wafplugin/systemconfig/get", apiInstance.GetSystemConfigApi)
router.POST("/api/v1/wafplugin/systemconfig/update", apiInstance.UpdateSystemConfigApi)
// 插件日志
router.POST("/api/v1/wafplugin/logs", apiInstance.GetPluginLogsApi)
}
+66
View File
@@ -67,6 +67,9 @@ type WafEngine struct {
TransportPool map[string]*http.Transport // 添加Transport缓存池
TransportMux sync.RWMutex // 保护Transport池的读写锁
// 插件管理器(避免循环导入)
PluginManager interface{} // 使用 interface{} 避免导入 plugin/manager
}
func (waf *WafEngine) Error() string {
@@ -402,6 +405,15 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return false
}
globalHostSafe := waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]]
// 插件预检查(在所有检测之前)
if handleBlock(func(r *http.Request, weblogbean *innerbean.WebLog, formValues url.Values, hostTarget *wafenginmodel.HostSafe, globalHost *wafenginmodel.HostSafe) detection.Result {
return waf.checkWithPlugins(r, weblogbean, hostTarget, "pre_check")
}) {
// 插件拦截,记录日志并返回
return
}
// 检测白名单开始
detectionWhiteResult := detection.Result{JumpGuardResult: false}
checkFunctions := []func(*http.Request, *innerbean.WebLog, url.Values, *wafenginmodel.HostSafe, *wafenginmodel.HostSafe) detection.Result{
@@ -1658,3 +1670,57 @@ func (waf *WafEngine) ApplyAntiCCConfig(hostCode string, antiCC model.AntiCC) {
zlog.Debug("远程配置", zap.Any("Anticc", antiCC))
// ... existing code ...
}
// checkWithPlugins 使用插件系统检查请求
// 返回检测结果
// 注意:此方法预留用于未来的插件集成
// 当前版本中,插件管理器的实际调用逻辑需要在 go-plugin 集成后完善
func (waf *WafEngine) checkWithPlugins(r *http.Request, weblogbean *innerbean.WebLog, hostTarget *wafenginmodel.HostSafe, group string) detection.Result {
// 检查插件管理器是否存在
if waf.PluginManager == nil {
return detection.Result{IsBlock: false}
}
// 类型断言获取插件管理器(定义需要的方法)
pluginManager, ok := waf.PluginManager.(interface {
CheckRequest(ctx context.Context, group string, ip string, requestPath string, userAgent string, method string, host string) (bool, string)
})
// 如果类型断言失败,直接返回
if !ok {
return detection.Result{IsBlock: false}
}
// 提取请求信息 - 使用已有的 getClientIP 方法
_, clientIP, _ := waf.getClientIP(r, strings.Split(global.GCONFIG_RECORD_PROXY_HEADER, ",")...)
if clientIP == "" {
// 如果获取 IP 失败,从 RemoteAddr 获取
clientIP = strings.Split(r.RemoteAddr, ":")[0]
}
requestPath := r.URL.Path
userAgent := r.UserAgent()
// 调用插件管理器检查请求
ctx := context.Background()
isBlock, reason := pluginManager.CheckRequest(ctx, group, clientIP, requestPath, userAgent, r.Method, r.Host)
// 如果插件要求拦截
if isBlock {
// 记录到日志
if weblogbean != nil && weblogbean.REQ_UUID == "" {
weblogbean.REQ_UUID = uuid.GenUUID()
}
if weblogbean != nil {
weblogbean.RULE = "插件拦截"
}
return detection.Result{
IsBlock: true,
Title: "插件拦截",
Content: reason,
}
}
// 插件检查通过,不拦截
return detection.Result{IsBlock: false}
}