diff --git a/examples/plugin/admin/web_admin.go b/examples/plugin/admin/web_admin.go new file mode 100644 index 0000000..28bb926 --- /dev/null +++ b/examples/plugin/admin/web_admin.go @@ -0,0 +1,460 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "runtime" + "strconv" + "strings" + + "github.com/darkit/goproxy/examples/plugin" +) + +// 全局插件管理器 +var pm *plugin.PluginManager + +// AdminHandler 处理管理界面主页 +func AdminHandler(w http.ResponseWriter, r *http.Request) { + // HTML模板 + tmpl := ` + + + + + 插件管理系统 + + + +

插件管理系统

+ +
+

系统信息

+

操作系统: {{.OS}}

+

动态加载支持: {{if .DynamicLoadingSupported}}是{{else}}否{{end}}

+

插件目录: {{.PluginsDir}}

+

已加载插件数量: {{len .Plugins}}

+
+ +

已安装插件

+ + {{range .Plugins}} +
+
+
+

{{.Name}}

+ 版本: {{.Version}} + 作者: {{.Author}} +
+
+ {{if .Enabled}} + 已启用 + {{else}} + 已禁用 + {{end}} +
+
+ +
{{.Description}}
+ + {{if .Config}} +
+

配置:

+ {{range $key, $value := .Config}} +
+
{{$key}}:
+
{{$value}}
+
+ {{end}} +
+ {{end}} + +
+ {{if .Enabled}} + + {{else}} + + {{end}} + +
+
+ {{end}} + + {{if not .Plugins}} +

没有已安装的插件。

+ {{end}} + +

操作

+
+ +
+ + +` + + // 解析模板 + t, err := template.New("admin").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 准备数据 + data := struct { + Plugins []plugin.PluginInfo + OS string + DynamicLoadingSupported bool + PluginsDir string + }{ + Plugins: pm.GetPluginInfos(), + OS: strings.Title(strings.ToLower(fmt.Sprintf("%s", strings.Split(runtime.GOOS, "/")[0]))), + DynamicLoadingSupported: pm.IsDynamicLoadingSupported(), + PluginsDir: pluginsDir, + } + + // 渲染模板 + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// EnablePluginHandler 启用插件 +func EnablePluginHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/enable/"):] + + if err := pm.EnablePlugin(pluginName); err != nil { + http.Error(w, fmt.Sprintf("启用插件失败: %v", err), http.StatusInternalServerError) + return + } + + // 尝试初始化和启动插件 + ctx := context.Background() + plugin, exists := pm.GetPlugin(pluginName) + if exists && plugin.IsEnabled() { + config, _ := pm.GetPluginConfig(pluginName) + if err := plugin.Init(ctx, config); err != nil { + http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError) + return + } + + if err := plugin.Start(ctx); err != nil { + http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError) + return + } + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// DisablePluginHandler 禁用插件 +func DisablePluginHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/disable/"):] + + // 先停止插件 + ctx := context.Background() + plugin, exists := pm.GetPlugin(pluginName) + if exists && plugin.IsEnabled() { + if err := plugin.Stop(ctx); err != nil { + http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError) + return + } + } + + // 禁用插件 + if err := pm.DisablePlugin(pluginName); err != nil { + http.Error(w, fmt.Sprintf("禁用插件失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// ConfigPluginHandler 配置插件 +func ConfigPluginHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/config/"):] + + if r.Method == http.MethodPost { + // 处理配置表单提交 + if err := r.ParseForm(); err != nil { + http.Error(w, fmt.Sprintf("解析表单失败: %v", err), http.StatusBadRequest) + return + } + + // 收集配置项 + config := make(map[string]interface{}) + for key, values := range r.PostForm { + if key == "pluginName" { + continue // 跳过插件名称字段 + } + + if len(values) > 0 { + // 尝试将值转换为适当的类型 + value := values[0] + + // 布尔值处理 + if value == "true" { + config[key] = true + } else if value == "false" { + config[key] = false + } else if strings.Contains(value, ".") { + // 尝试解析为浮点数 + if f, err := strconv.ParseFloat(value, 64); err == nil { + config[key] = f + } else { + config[key] = value + } + } else if i, err := strconv.Atoi(value); err == nil { + // 尝试解析为整数 + config[key] = i + } else { + // 默认为字符串 + config[key] = value + } + } + } + + // 保存配置 + if err := pm.SetPluginConfig(pluginName, config); err != nil { + http.Error(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError) + return + } + + // 重定向回主页 + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // 获取插件信息和配置 + info, config := getPluginInfo(pluginName) + if info == nil { + http.Error(w, "插件不存在", http.StatusNotFound) + return + } + + // 配置表单模板 + tmpl := ` + + + + + 配置 {{.Info.Name}} + + + +

配置 {{.Info.Name}}

+ +
+ + + {{range $key, $value := .Config}} +
+ + {{if eq (printf "%T" $value) "bool"}} + + {{else if eq (printf "%T" $value) "float64"}} + + {{else if eq (printf "%T" $value) "int"}} + + {{else}} + + {{end}} +
+ {{end}} + + +
+ + 返回插件列表 + + +` + + // 解析模板 + t, err := template.New("config").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 准备数据 + data := struct { + Info *plugin.PluginInfo + Config map[string]interface{} + }{ + Info: info, + Config: config, + } + + // 渲染模板 + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// ReloadPluginsHandler 重新加载插件 +func ReloadPluginsHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // 停止所有插件 + ctx := context.Background() + if err := pm.StopPlugins(ctx); err != nil { + http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError) + return + } + + // 重新加载插件 + if err := pm.LoadPlugins(); err != nil { + http.Error(w, fmt.Sprintf("加载插件失败: %v", err), http.StatusInternalServerError) + return + } + + // 初始化和启动插件 + if err := pm.InitPlugins(ctx); err != nil { + http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError) + return + } + + if err := pm.StartPlugins(ctx); err != nil { + http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// API接口处理器 - 返回插件列表 +func APIPluginsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + plugins := pm.GetPluginInfos() + if err := json.NewEncoder(w).Encode(plugins); err != nil { + http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError) + } +} + +// getPluginInfo 获取插件信息和配置 +func getPluginInfo(name string) (*plugin.PluginInfo, map[string]interface{}) { + plugins := pm.GetPluginInfos() + + for _, info := range plugins { + if info.Name == name { + config, _ := pm.GetPluginConfig(name) + return &info, config + } + } + + return nil, nil +} + +// 插件目录 +var pluginsDir = "./plugins" + +func main3() { + // 创建插件管理器 + pm = plugin.NewPluginManager(pluginsDir) + + // 加载插件 + if err := pm.LoadPlugins(); err != nil { + log.Fatalf("加载插件失败: %v", err) + } + + // 初始化并启动插件 + ctx := context.Background() + if err := pm.InitPlugins(ctx); err != nil { + log.Printf("初始化插件失败: %v", err) + } + + if err := pm.StartPlugins(ctx); err != nil { + log.Printf("启动插件失败: %v", err) + } + + // 注册处理器 + http.HandleFunc("/", AdminHandler) + http.HandleFunc("/enable/", EnablePluginHandler) + http.HandleFunc("/disable/", DisablePluginHandler) + http.HandleFunc("/config/", ConfigPluginHandler) + http.HandleFunc("/reload", ReloadPluginsHandler) + http.HandleFunc("/api/plugins", APIPluginsHandler) + + // 启动Web服务器 + log.Println("管理界面启动在 :8080...") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/examples/plugin/interface.go b/examples/plugin/interface.go new file mode 100644 index 0000000..1fa5079 --- /dev/null +++ b/examples/plugin/interface.go @@ -0,0 +1,91 @@ +package plugin + +// IPlugin 插件接口 +// 这个文件定义了所有插件必须实现的接口 +// 注意:这个文件应该与实际插件代码一起编译 +type IPlugin interface { + // Name 插件名称 + Name() string + // Version 插件版本 + Version() string + // Description 插件描述 + Description() string + // Author 插件作者 + Author() string + // Init 初始化插件 + Init(config map[string]interface{}) error + // Start 启动插件 + Start() error + // Stop 停止插件 + Stop() error + // IsEnabled 插件是否启用 + IsEnabled() bool + // SetEnabled 设置插件启用状态 + SetEnabled(enabled bool) +} + +// BasePlugin 提供插件接口的基本实现 +// 插件开发者可以嵌入此结构体,以减少需要实现的方法数量 +type BasePlugin struct { + name string + version string + description string + author string + enabled bool +} + +// NewBasePlugin 创建一个基本插件 +func NewBasePlugin(name, version, description, author string) *BasePlugin { + return &BasePlugin{ + name: name, + version: version, + description: description, + author: author, + enabled: true, + } +} + +// Name 获取插件名称 +func (p *BasePlugin) Name() string { + return p.name +} + +// Version 获取插件版本 +func (p *BasePlugin) Version() string { + return p.version +} + +// Description 获取插件描述 +func (p *BasePlugin) Description() string { + return p.description +} + +// Author 获取插件作者 +func (p *BasePlugin) Author() string { + return p.author +} + +// IsEnabled 插件是否启用 +func (p *BasePlugin) IsEnabled() bool { + return p.enabled +} + +// SetEnabled 设置插件启用状态 +func (p *BasePlugin) SetEnabled(enabled bool) { + p.enabled = enabled +} + +// Init 初始化插件,子类需要重写此方法 +func (p *BasePlugin) Init(config map[string]interface{}) error { + return nil +} + +// Start 启动插件,子类需要重写此方法 +func (p *BasePlugin) Start() error { + return nil +} + +// Stop 停止插件,子类需要重写此方法 +func (p *BasePlugin) Stop() error { + return nil +} diff --git a/examples/plugin/manager/plugin_manager.go b/examples/plugin/manager/plugin_manager.go new file mode 100644 index 0000000..82f983d --- /dev/null +++ b/examples/plugin/manager/plugin_manager.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "fmt" + "runtime" + + "github.com/darkit/goproxy/examples/plugin" +) + +// 这是内置插件的示例实现 +// LogPlugin 日志插件 +type LogPlugin struct { + name string + version string + description string + author string + enabled bool + level string +} + +// 创建内置日志插件 +func NewLogPlugin() *LogPlugin { + return &LogPlugin{ + name: "InternalLogPlugin", + version: "1.0.0", + description: "内置日志插件,在不支持动态加载插件的平台上使用", + author: "开发者", + enabled: true, + level: "info", + } +} + +// 实现Plugin接口 +func (p *LogPlugin) Name() string { + return p.name +} + +func (p *LogPlugin) Version() string { + return p.version +} + +func (p *LogPlugin) Description() string { + return p.description +} + +func (p *LogPlugin) Author() string { + return p.author +} + +func (p *LogPlugin) IsEnabled() bool { + return p.enabled +} + +func (p *LogPlugin) SetEnabled(enabled bool) { + p.enabled = enabled +} + +func (p *LogPlugin) Init(ctx context.Context, config map[string]interface{}) error { + if level, ok := config["level"].(string); ok { + p.level = level + } + fmt.Println("内置日志插件已初始化,日志级别:", p.level) + return nil +} + +func (p *LogPlugin) Start(ctx context.Context) error { + fmt.Println("内置日志插件已启动") + return nil +} + +func (p *LogPlugin) Stop(ctx context.Context) error { + fmt.Println("内置日志插件已停止") + return nil +} + +// Log 记录日志 +func (p *LogPlugin) Log(message string) { + fmt.Printf("[%s] %s\n", p.level, message) +} + +func main() { + // 创建插件管理器 + pluginsDir := "./plugins" + pm := plugin.NewPluginManager(pluginsDir) + + // 检查当前系统是否支持动态加载插件 + if pm.IsDynamicLoadingSupported() { + fmt.Printf("当前系统(%s)支持动态加载插件\n", runtime.GOOS) + } else { + fmt.Printf("当前系统(%s)不支持动态加载插件\n", runtime.GOOS) + + // 在不支持动态加载的系统上使用内置插件 + logPlugin := NewLogPlugin() + pm.RegisterPlugin(logPlugin) + } + + // 加载插件 + fmt.Println("正在加载插件...") + if err := pm.LoadPlugins(); err != nil { + fmt.Printf("加载插件失败: %v\n", err) + } + + // 配置插件 + logPluginConfig := map[string]interface{}{ + "level": "debug", + } + if err := pm.SetPluginConfig("InternalLogPlugin", logPluginConfig); err != nil { + fmt.Printf("配置插件失败: %v\n", err) + } + + // 初始化插件 + ctx := context.Background() + fmt.Println("正在初始化插件...") + if err := pm.InitPlugins(ctx); err != nil { + fmt.Printf("初始化插件失败: %v\n", err) + } + + // 启动插件 + fmt.Println("正在启动插件...") + if err := pm.StartPlugins(ctx); err != nil { + fmt.Printf("启动插件失败: %v\n", err) + } + + // 获取并打印所有插件信息 + fmt.Println("\n已加载的插件列表:") + plugins := pm.GetPluginInfos() + for i, info := range plugins { + status := "启用" + if !info.Enabled { + status = "禁用" + } + fmt.Printf("[%d] %s (v%s) - %s [%s]\n作者: %s\n", + i, info.Name, info.Version, info.Description, status, info.Author) + + // 打印插件配置 + if len(info.Config) > 0 { + fmt.Println("配置:") + for k, v := range info.Config { + fmt.Printf(" %s: %v\n", k, v) + } + } + fmt.Println() + } + + // 使用第一个启用的插件(这里只是示例) + if len(plugins) > 0 { + for _, info := range plugins { + if info.Enabled { + fmt.Printf("正在使用插件: %s\n", info.Name) + if p, ok := pm.GetPlugin(info.Name); ok { + // 这里可以根据插件类型执行特定操作 + if logPlugin, ok := p.(*LogPlugin); ok { + logPlugin.Log("这是一条测试日志消息") + } + } + break + } + } + } + + // 停止插件 + fmt.Println("\n正在停止插件...") + if err := pm.StopPlugins(ctx); err != nil { + fmt.Printf("停止插件失败: %v\n", err) + } + + fmt.Println("示例结束") +} diff --git a/examples/plugin/plugin.go b/examples/plugin/plugin.go new file mode 100644 index 0000000..6c366bf --- /dev/null +++ b/examples/plugin/plugin.go @@ -0,0 +1,410 @@ +package plugin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "plugin" + "runtime" + "sync" +) + +// Plugin 插件接口 +type Plugin interface { + // Name 插件名称 + Name() string + // Version 插件版本 + Version() string + // Description 插件描述 + Description() string + // Author 插件作者 + Author() string + // Init 初始化插件 + Init(ctx context.Context, config map[string]interface{}) error + // Start 启动插件 + Start(ctx context.Context) error + // Stop 停止插件 + Stop(ctx context.Context) error + // IsEnabled 插件是否启用 + IsEnabled() bool + // SetEnabled 设置插件启用状态 + SetEnabled(enabled bool) +} + +// PluginInfo 插件信息 +type PluginInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Author string `json:"author"` + Enabled bool `json:"enabled"` + Config map[string]interface{} `json:"config,omitempty"` +} + +// PluginManager 插件管理器 +type PluginManager struct { + pluginsDir string + plugins map[string]Plugin + configs map[string]map[string]interface{} + mu sync.RWMutex + dynamicLoadingSupported bool // 是否支持动态加载插件 +} + +// NewPluginManager 创建插件管理器 +func NewPluginManager(pluginsDir string) *PluginManager { + // 检查当前系统是否支持动态加载插件 + dynamicLoadingSupported := runtime.GOOS == "linux" || runtime.GOOS == "darwin" + + return &PluginManager{ + pluginsDir: pluginsDir, + plugins: make(map[string]Plugin), + configs: make(map[string]map[string]interface{}), + dynamicLoadingSupported: dynamicLoadingSupported, + } +} + +// IsDynamicLoadingSupported 检查是否支持动态加载插件 +func (pm *PluginManager) IsDynamicLoadingSupported() bool { + return pm.dynamicLoadingSupported +} + +// LoadPlugins 加载插件 +func (pm *PluginManager) LoadPlugins() error { + // 确保插件目录存在 + if err := os.MkdirAll(pm.pluginsDir, 0o755); err != nil { + return fmt.Errorf("创建插件目录失败: %v", err) + } + + // 加载插件配置 + if err := pm.loadPluginConfigs(); err != nil { + return fmt.Errorf("加载插件配置失败: %v", err) + } + + // 如果不支持动态加载,则返回 + if !pm.dynamicLoadingSupported { + fmt.Printf("警告: 当前系统(%s)不支持动态加载插件,跳过加载\n", runtime.GOOS) + return nil + } + + // 遍历插件目录 + err := filepath.Walk(pm.pluginsDir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + // 跳过目录和非.so文件 + if info.IsDir() || filepath.Ext(path) != ".so" { + return nil + } + + // 加载插件 + if err := pm.loadPlugin(path); err != nil { + fmt.Printf("加载插件 %s 失败: %v\n", path, err) + } + + return nil + }) + + return err +} + +// loadPluginConfigs 加载插件配置 +func (pm *PluginManager) loadPluginConfigs() error { + configPath := filepath.Join(pm.pluginsDir, "plugins.json") + + // 如果配置文件不存在,创建一个空的 + if _, err := os.Stat(configPath); os.IsNotExist(err) { + file, err := os.Create(configPath) + if err != nil { + return fmt.Errorf("创建插件配置文件失败: %v", err) + } + file.Write([]byte("{}")) + file.Close() + return nil + } + + // 读取配置文件 + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("读取插件配置文件失败: %v", err) + } + + // 解析配置 + var configs map[string]map[string]interface{} + if err := json.Unmarshal(data, &configs); err != nil { + return fmt.Errorf("解析插件配置文件失败: %v", err) + } + + pm.configs = configs + return nil +} + +// savePluginConfigs 保存插件配置 +func (pm *PluginManager) savePluginConfigs() error { + configPath := filepath.Join(pm.pluginsDir, "plugins.json") + + // 创建配置数据 + configs := make(map[string]map[string]interface{}) + pm.mu.RLock() + for name, plugin := range pm.plugins { + config := pm.configs[name] + if config == nil { + config = make(map[string]interface{}) + } + config["enabled"] = plugin.IsEnabled() + configs[name] = config + } + pm.mu.RUnlock() + + // 序列化配置 + data, err := json.MarshalIndent(configs, "", " ") + if err != nil { + return fmt.Errorf("序列化插件配置失败: %v", err) + } + + // 写入文件 + if err := os.WriteFile(configPath, data, 0o644); err != nil { + return fmt.Errorf("写入插件配置文件失败: %v", err) + } + + return nil +} + +// loadPlugin 加载单个插件 +func (pm *PluginManager) loadPlugin(path string) error { + // 打开插件文件 + p, err := plugin.Open(path) + if err != nil { + return fmt.Errorf("打开插件失败: %v", err) + } + + // 查找Plugin变量 + symPlugin, err := p.Lookup("Plugin") + if err != nil { + return fmt.Errorf("查找Plugin变量失败: %v", err) + } + + // 类型断言 + plugin, ok := symPlugin.(Plugin) + if !ok { + return errors.New("插件类型错误") + } + + // 检查插件名称是否已存在 + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.plugins[plugin.Name()]; exists { + return fmt.Errorf("插件 %s 已存在", plugin.Name()) + } + + // 设置插件启用状态 + if config, exists := pm.configs[plugin.Name()]; exists { + if enabled, ok := config["enabled"].(bool); ok { + plugin.SetEnabled(enabled) + } else { + plugin.SetEnabled(true) // 默认启用 + } + } else { + plugin.SetEnabled(true) // 默认启用 + } + + pm.plugins[plugin.Name()] = plugin + return nil +} + +// RegisterPlugin 注册内置插件 +// 用于在不支持动态加载的平台上注册插件 +func (pm *PluginManager) RegisterPlugin(plugin Plugin) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.plugins[plugin.Name()]; exists { + return fmt.Errorf("插件 %s 已存在", plugin.Name()) + } + + // 设置插件启用状态 + if config, exists := pm.configs[plugin.Name()]; exists { + if enabled, ok := config["enabled"].(bool); ok { + plugin.SetEnabled(enabled) + } else { + plugin.SetEnabled(true) // 默认启用 + } + } else { + plugin.SetEnabled(true) // 默认启用 + } + + pm.plugins[plugin.Name()] = plugin + return nil +} + +// GetPlugin 获取插件 +func (pm *PluginManager) GetPlugin(name string) (Plugin, bool) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugin, exists := pm.plugins[name] + return plugin, exists +} + +// GetAllPlugins 获取所有插件 +func (pm *PluginManager) GetAllPlugins() []Plugin { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugins := make([]Plugin, 0, len(pm.plugins)) + for _, plugin := range pm.plugins { + plugins = append(plugins, plugin) + } + return plugins +} + +// GetPluginInfos 获取所有插件信息 +func (pm *PluginManager) GetPluginInfos() []PluginInfo { + pm.mu.RLock() + defer pm.mu.RUnlock() + + infos := make([]PluginInfo, 0, len(pm.plugins)) + for name, plugin := range pm.plugins { + info := PluginInfo{ + Name: plugin.Name(), + Version: plugin.Version(), + Description: plugin.Description(), + Author: plugin.Author(), + Enabled: plugin.IsEnabled(), + Config: pm.configs[name], + } + infos = append(infos, info) + } + return infos +} + +// EnablePlugin 启用插件 +func (pm *PluginManager) EnablePlugin(name string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + plugin, exists := pm.plugins[name] + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + plugin.SetEnabled(true) + + // 更新配置 + if pm.configs[name] == nil { + pm.configs[name] = make(map[string]interface{}) + } + pm.configs[name]["enabled"] = true + + // 保存配置 + return pm.savePluginConfigs() +} + +// DisablePlugin 禁用插件 +func (pm *PluginManager) DisablePlugin(name string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + plugin, exists := pm.plugins[name] + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + plugin.SetEnabled(false) + + // 更新配置 + if pm.configs[name] == nil { + pm.configs[name] = make(map[string]interface{}) + } + pm.configs[name]["enabled"] = false + + // 保存配置 + return pm.savePluginConfigs() +} + +// InitPlugins 初始化所有插件 +func (pm *PluginManager) InitPlugins(ctx context.Context) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + config := pm.configs[name] + if err := plugin.Init(ctx, config); err != nil { + return fmt.Errorf("初始化插件 %s 失败: %v", name, err) + } + } + return nil +} + +// StartPlugins 启动所有插件 +func (pm *PluginManager) StartPlugins(ctx context.Context) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + if err := plugin.Start(ctx); err != nil { + return fmt.Errorf("启动插件 %s 失败: %v", name, err) + } + } + return nil +} + +// StopPlugins 停止所有插件 +func (pm *PluginManager) StopPlugins(ctx context.Context) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + if err := plugin.Stop(ctx); err != nil { + return fmt.Errorf("停止插件 %s 失败: %v", name, err) + } + } + return nil +} + +// SetPluginConfig 设置插件配置 +func (pm *PluginManager) SetPluginConfig(name string, config map[string]interface{}) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.plugins[name]; !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + pm.configs[name] = config + return pm.savePluginConfigs() +} + +// GetPluginConfig 获取插件配置 +func (pm *PluginManager) GetPluginConfig(name string) (map[string]interface{}, error) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + if _, exists := pm.plugins[name]; !exists { + return nil, fmt.Errorf("插件 %s 不存在", name) + } + + config := pm.configs[name] + if config == nil { + config = make(map[string]interface{}) + } + + return config, nil +} diff --git a/examples/plugin/plugins/build.sh b/examples/plugin/plugins/build.sh new file mode 100644 index 0000000..d58aa12 --- /dev/null +++ b/examples/plugin/plugins/build.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# 编译插件 +go build -buildmode=plugin -o stats.so ./stats/stats.go + +# 检查编译结果 +if [ $? -eq 0 ]; then + echo "插件编译成功: stats.so" +else + echo "插件编译失败" + exit 1 +fi + + +go build -buildmode=plugin -o logger.so ./logger/logger.go +# 检查编译结果 +if [ $? -eq 0 ]; then + echo "插件编译成功: logger.so" +else + echo "插件编译失败" + exit 1 +fi \ No newline at end of file diff --git a/examples/plugin/plugins/logger/logger_plugin.go b/examples/plugin/plugins/logger/logger_plugin.go new file mode 100644 index 0000000..99af421 --- /dev/null +++ b/examples/plugin/plugins/logger/logger_plugin.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/darkit/goproxy/examples/plugin" +) + +// LoggerPlugin 日志插件 +// 提供文件日志和控制台日志功能 +type LoggerPlugin struct { + *plugin.BasePlugin // 嵌入基本插件结构 + logFile *os.File // 日志文件 + logger *log.Logger // 日志记录器 + config map[string]interface{} // 配置 +} + +// Plugin 导出的插件变量 +// 注意:变量名必须是Plugin,大小写敏感 +var Plugin = &LoggerPlugin{ + BasePlugin: plugin.NewBasePlugin( + "LoggerPlugin", + "1.0.0", + "简单的日志记录插件", + "开发者", + ), +} + +// Init 初始化插件 +func (p *LoggerPlugin) Init(config map[string]interface{}) error { + p.config = config + + // 获取日志文件路径 + logPath, ok := config["log_path"].(string) + if !ok { + // 使用默认路径 + logPath = "logs" + } + + // 确保日志目录存在 + if err := os.MkdirAll(logPath, 0o755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建日志文件 + logFilePath := filepath.Join(logPath, fmt.Sprintf("app_%s.log", time.Now().Format("2006-01-02"))) + logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return fmt.Errorf("打开日志文件失败: %v", err) + } + + p.logFile = logFile + p.logger = log.New(logFile, "[LoggerPlugin] ", log.LstdFlags) + + p.logger.Println("日志插件初始化完成") + fmt.Println("日志插件初始化完成,日志文件:", logFilePath) + + return nil +} + +// Start 启动插件 +func (p *LoggerPlugin) Start() error { + if p.logger == nil { + return fmt.Errorf("插件未初始化") + } + + p.logger.Println("日志插件已启动") + fmt.Println("日志插件已启动") + return nil +} + +// Stop 停止插件 +func (p *LoggerPlugin) Stop() error { + if p.logger != nil { + p.logger.Println("日志插件正在停止") + } + + if p.logFile != nil { + if err := p.logFile.Close(); err != nil { + return fmt.Errorf("关闭日志文件失败: %v", err) + } + } + + fmt.Println("日志插件已停止") + return nil +} + +// Log 记录日志 +func (p *LoggerPlugin) Log(level, message string) { + if p.logger == nil { + fmt.Printf("[%s] %s\n", level, message) + return + } + + logMsg := fmt.Sprintf("[%s] %s", level, message) + p.logger.Println(logMsg) + + // 如果配置了同时输出到控制台 + if consoleOutput, ok := p.config["console_output"].(bool); ok && consoleOutput { + fmt.Println(logMsg) + } +} + +// Info 记录信息日志 +func (p *LoggerPlugin) Info(message string) { + p.Log("INFO", message) +} + +// Warn 记录警告日志 +func (p *LoggerPlugin) Warn(message string) { + p.Log("WARN", message) +} + +// Error 记录错误日志 +func (p *LoggerPlugin) Error(message string) { + p.Log("ERROR", message) +} + +// main 函数是必须的,但不会被调用 +func main() { + // 不会被执行,仅用于编译插件 +} diff --git a/examples/plugin/plugins/stats/stats_plugin.go b/examples/plugin/plugins/stats/stats_plugin.go new file mode 100644 index 0000000..3508eed --- /dev/null +++ b/examples/plugin/plugins/stats/stats_plugin.go @@ -0,0 +1,170 @@ +package main + +import ( + "fmt" + "sync" + "time" + + "github.com/darkit/goproxy/examples/plugin" +) + +// StatsPlugin 统计插件 +// 用于收集和记录系统运行时统计数据 +type StatsPlugin struct { + *plugin.BasePlugin + stats map[string]int64 + startTime time.Time + mu sync.RWMutex + tickerStop chan bool + ticker *time.Ticker + config map[string]interface{} +} + +// Plugin 导出的插件变量 +var Plugin = &StatsPlugin{ + BasePlugin: plugin.NewBasePlugin( + "StatsPlugin", + "1.0.0", + "系统运行时统计插件", + "开发者", + ), + stats: make(map[string]int64), + tickerStop: make(chan bool), +} + +// Init 初始化插件 +func (p *StatsPlugin) Init(config map[string]interface{}) error { + p.config = config + + // 初始化统计指标 + p.mu.Lock() + p.stats["requests"] = 0 + p.stats["errors"] = 0 + p.stats["bytes_sent"] = 0 + p.stats["bytes_received"] = 0 + p.mu.Unlock() + + fmt.Println("统计插件初始化完成") + return nil +} + +// Start 启动插件 +func (p *StatsPlugin) Start() error { + p.startTime = time.Now() + + // 启动定时统计任务 + interval := 60 * time.Second // 默认60秒 + + // 从配置中获取统计间隔 + if intervalSec, ok := p.config["interval_seconds"].(float64); ok { + interval = time.Duration(intervalSec) * time.Second + } + + p.ticker = time.NewTicker(interval) + + go func() { + for { + select { + case <-p.ticker.C: + p.logStats() + case <-p.tickerStop: + p.ticker.Stop() + return + } + } + }() + + fmt.Println("统计插件已启动") + return nil +} + +// Stop 停止插件 +func (p *StatsPlugin) Stop() error { + if p.ticker != nil { + p.tickerStop <- true + } + + // 输出最终统计信息 + p.logStats() + + fmt.Println("统计插件已停止") + return nil +} + +// logStats 记录当前统计信息 +func (p *StatsPlugin) logStats() { + p.mu.RLock() + defer p.mu.RUnlock() + + uptime := time.Since(p.startTime).Seconds() + + fmt.Printf("===== 系统统计信息 =====\n") + fmt.Printf("运行时间: %.2f 秒\n", uptime) + fmt.Printf("总请求数: %d\n", p.stats["requests"]) + fmt.Printf("错误数: %d\n", p.stats["errors"]) + fmt.Printf("发送字节: %d\n", p.stats["bytes_sent"]) + fmt.Printf("接收字节: %d\n", p.stats["bytes_received"]) + + if uptime > 0 && p.stats["requests"] > 0 { + fmt.Printf("平均请求/秒: %.2f\n", float64(p.stats["requests"])/uptime) + fmt.Printf("错误率: %.2f%%\n", float64(p.stats["errors"])*100/float64(p.stats["requests"])) + } + + fmt.Printf("=======================\n") +} + +// IncrementStat 增加统计值 +func (p *StatsPlugin) IncrementStat(name string, value int64) { + p.mu.Lock() + defer p.mu.Unlock() + + if _, exists := p.stats[name]; exists { + p.stats[name] += value + } else { + p.stats[name] = value + } +} + +// GetStat 获取统计值 +func (p *StatsPlugin) GetStat(name string) int64 { + p.mu.RLock() + defer p.mu.RUnlock() + + if value, exists := p.stats[name]; exists { + return value + } + return 0 +} + +// RecordRequest 记录请求 +func (p *StatsPlugin) RecordRequest(bytesReceived, bytesSent int64, isError bool) { + p.IncrementStat("requests", 1) + p.IncrementStat("bytes_received", bytesReceived) + p.IncrementStat("bytes_sent", bytesSent) + + if isError { + p.IncrementStat("errors", 1) + } +} + +// GetAllStats 获取所有统计数据 +func (p *StatsPlugin) GetAllStats() map[string]int64 { + p.mu.RLock() + defer p.mu.RUnlock() + + // 创建一个副本 + statsCopy := make(map[string]int64, len(p.stats)) + for k, v := range p.stats { + statsCopy[k] = v + } + + // 添加运行时间 + statsCopy["uptime_seconds"] = int64(time.Since(p.startTime).Seconds()) + + return statsCopy +} + +// main 函数是必须的,但不会被调用 +func main() { + // 不会被执行,仅用于编译插件 +} diff --git a/examples/rewriter/http_server.go b/examples/rewriter/http_server.go new file mode 100644 index 0000000..e8f1e96 --- /dev/null +++ b/examples/rewriter/http_server.go @@ -0,0 +1,121 @@ +package main + +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/darkit/goproxy/examples/rewriter/rewriter" +) + +// RewriteReverseProxy 重写反向代理 +// 在请求发送到后端服务器前重写URL +type RewriteReverseProxy struct { + // 后端服务器地址 + Target *url.URL + // URL重写器 + Rewriter *rewriter.Rewriter + // 反向代理 + Proxy *httputil.ReverseProxy +} + +// NewRewriteReverseProxy 创建重写反向代理 +func NewRewriteReverseProxy(target string, rewriteRulesFile string) (*RewriteReverseProxy, error) { + // 解析目标URL + targetURL, err := url.Parse(target) + if err != nil { + return nil, err + } + + // 创建重写器 + rw := rewriter.NewRewriter() + + // 加载重写规则 + if rewriteRulesFile != "" { + if err := rw.LoadRulesFromFile(rewriteRulesFile); err != nil { + return nil, err + } + } + + // 创建反向代理 + proxy := httputil.NewSingleHostReverseProxy(targetURL) + + // 修改默认的Director函数,添加URL重写逻辑 + defaultDirector := proxy.Director + proxy.Director = func(req *http.Request) { + // 先执行默认的Director函数 + defaultDirector(req) + + // 然后执行URL重写 + rw.Rewrite(req) + + // 记录重写后的URL + log.Printf("请求重写: %s -> %s", req.URL.Path, req.URL.String()) + } + + // 修改响应处理器,重写响应头 + proxy.ModifyResponse = func(resp *http.Response) error { + // 重写响应 + rw.RewriteResponse(resp, targetURL.Host) + return nil + } + + return &RewriteReverseProxy{ + Target: targetURL, + Rewriter: rw, + Proxy: proxy, + }, nil +} + +// ServeHTTP 实现http.Handler接口 +func (rrp *RewriteReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Printf("收到请求: %s %s", r.Method, r.URL.Path) + rrp.Proxy.ServeHTTP(w, r) +} + +// 中间件:将重写器应用到处理链中 +func RewriteMiddleware(rw *rewriter.Rewriter, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 重写URL + originalPath := r.URL.Path + rw.Rewrite(r) + + if originalPath != r.URL.Path { + log.Printf("URL重写: %s -> %s", originalPath, r.URL.Path) + } + + // 继续处理链 + next.ServeHTTP(w, r) + }) +} + +func main1() { + // 创建重写器 + rw := rewriter.NewRewriter() + + // 加载重写规则 + if err := rw.LoadRulesFromFile("rules.json"); err != nil { + log.Fatalf("加载重写规则失败: %v", err) + } + + // 创建反向代理 + proxy, err := NewRewriteReverseProxy("http://localhost:8081", "rules.json") + if err != nil { + log.Fatalf("创建反向代理失败: %v", err) + } + + // 创建静态文件服务器(模拟普通Web服务器) + fileServer := http.FileServer(http.Dir("./static")) + + // 使用中间件包装文件服务器 + rewrittenFileServer := RewriteMiddleware(rw, fileServer) + + // 设置处理函数 + http.Handle("/proxy/", http.StripPrefix("/proxy", proxy)) + http.Handle("/", rewrittenFileServer) + + // 启动服务器 + log.Println("服务器启动在 :8080...") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/examples/rewriter/main.go b/examples/rewriter/main.go new file mode 100644 index 0000000..b32f109 --- /dev/null +++ b/examples/rewriter/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + "github.com/darkit/goproxy/examples/rewriter/rewriter" +) + +func main2() { + // 创建URL重写器 + rw := rewriter.NewRewriter() + + // 添加一些规则 + rw.AddRule("/api/v1/", "/api/v2/", false) + rw.AddRuleWithDescription("/old/(.*)/page", "/new/$1/page", true, "旧页面重定向") + + // 从文件加载规则 + exampleDir, _ := os.Getwd() + jsonRulesPath := filepath.Join(exampleDir, "rules.json") + textRulesPath := filepath.Join(exampleDir, "rules.txt") + + // 先加载JSON格式规则 + fmt.Println("从JSON文件加载规则...") + if err := rw.LoadRulesFromFile(jsonRulesPath); err != nil { + fmt.Printf("从JSON文件加载规则失败: %v\n", err) + } + + // 然后加载文本格式规则 + fmt.Println("从文本文件加载规则...") + if err := rw.LoadRulesFromFile(textRulesPath); err != nil { + fmt.Printf("从文本文件加载规则失败: %v\n", err) + } + + // 打印所有规则 + fmt.Println("\n当前规则列表:") + for i, rule := range rw.GetRules() { + status := "启用" + if !rule.Enabled { + status = "禁用" + } + fmt.Printf("[%d] %s -> %s [%s] (%s)\n", + i, rule.Pattern, rule.Replacement, + map[bool]string{true: "正则", false: "前缀"}[rule.UseRegex], + status) + } + + // 测试重写 + fmt.Println("\n测试URL重写:") + testURLs := []string{ + "/api/v1/users", + "/old/profile/page", + "/legacy-files/document.pdf", + "/en/about/company", + } + + for _, url := range testURLs { + // 创建测试请求 + req := httptest.NewRequest("GET", "http://example.com"+url, nil) + + // 重写URL + fmt.Printf("原始URL: %s\n", req.URL.Path) + rw.Rewrite(req) + fmt.Printf("重写后: %s\n\n", req.URL.Path) + } + + // 测试响应重写 + fmt.Println("测试响应重写:") + resp := &http.Response{ + Header: http.Header{}, + } + resp.Header.Set("Location", "http://backend.example.com/old/profile/page") + fmt.Printf("原始Location: %s\n", resp.Header.Get("Location")) + rw.RewriteResponse(resp, "backend.example.com") + fmt.Printf("重写后Location: %s\n", resp.Header.Get("Location")) + + // 保存规则到新文件 + newRulesPath := filepath.Join(exampleDir, "new_rules.json") + fmt.Printf("\n保存规则到文件: %s\n", newRulesPath) + if err := rw.SaveRulesToFile(newRulesPath); err != nil { + fmt.Printf("保存规则失败: %v\n", err) + } else { + fmt.Println("规则已保存") + } +} diff --git a/examples/rewriter/rewriter/rewriter.go b/examples/rewriter/rewriter/rewriter.go new file mode 100644 index 0000000..779d3e3 --- /dev/null +++ b/examples/rewriter/rewriter/rewriter.go @@ -0,0 +1,285 @@ +package rewriter + +import ( + "bufio" + "encoding/json" + "fmt" + "net/http" + "os" + "regexp" + "strings" +) + +// Rewriter URL重写器 +// 用于在反向代理中重写请求URL +type Rewriter struct { + // 重写规则列表 + rules []*RewriteRule +} + +// RewriteRule 重写规则 +type RewriteRule struct { + // 匹配模式 + Pattern string `json:"pattern"` + // 替换模式 + Replacement string `json:"replacement"` + // 是否使用正则表达式 + UseRegex bool `json:"use_regex"` + // 编译后的正则表达式 + regex *regexp.Regexp `json:"-"` + // 规则描述 + Description string `json:"description,omitempty"` + // 规则启用状态 + Enabled bool `json:"enabled,omitempty"` +} + +// NewRewriter 创建URL重写器 +func NewRewriter() *Rewriter { + return &Rewriter{ + rules: make([]*RewriteRule, 0), + } +} + +// AddRule 添加重写规则 +func (r *Rewriter) AddRule(pattern, replacement string, useRegex bool) error { + rule := &RewriteRule{ + Pattern: pattern, + Replacement: replacement, + UseRegex: useRegex, + Enabled: true, + } + + if useRegex { + regex, err := regexp.Compile(pattern) + if err != nil { + return err + } + rule.regex = regex + } + + r.rules = append(r.rules, rule) + return nil +} + +// AddRuleWithDescription 添加带描述的重写规则 +func (r *Rewriter) AddRuleWithDescription(pattern, replacement string, useRegex bool, description string) error { + rule := &RewriteRule{ + Pattern: pattern, + Replacement: replacement, + UseRegex: useRegex, + Description: description, + Enabled: true, + } + + if useRegex { + regex, err := regexp.Compile(pattern) + if err != nil { + return err + } + rule.regex = regex + } + + r.rules = append(r.rules, rule) + return nil +} + +// Rewrite 重写URL +func (r *Rewriter) Rewrite(req *http.Request) { + path := req.URL.Path + + for _, rule := range r.rules { + if !rule.Enabled { + continue + } + + if rule.UseRegex { + if rule.regex.MatchString(path) { + req.URL.Path = rule.regex.ReplaceAllString(path, rule.Replacement) + break + } + } else { + if strings.HasPrefix(path, rule.Pattern) { + req.URL.Path = strings.Replace(path, rule.Pattern, rule.Replacement, 1) + break + } + } + } +} + +// RewriteResponse 重写响应 +// 主要用于处理响应中的Location头和内容中的URL +func (r *Rewriter) RewriteResponse(resp *http.Response, originHost string) { + // 处理重定向头 + location := resp.Header.Get("Location") + if location != "" { + // 将后端服务器的域名替换成代理服务器的域名 + for _, rule := range r.rules { + if !rule.Enabled { + continue + } + + if rule.UseRegex && rule.regex != nil { + if rule.regex.MatchString(location) { + newLocation := rule.regex.ReplaceAllString(location, rule.Replacement) + resp.Header.Set("Location", newLocation) + break + } + } + } + } +} + +// LoadRulesFromFile 从文件加载重写规则 +func (r *Rewriter) LoadRulesFromFile(filename string) error { + file, err := os.Open(filename) + if err != nil { + return fmt.Errorf("打开文件失败: %v", err) + } + defer file.Close() + + // 检查文件扩展名,决定使用何种方式解析 + if strings.HasSuffix(filename, ".json") { + return r.loadRulesFromJSON(file) + } else { + return r.loadRulesFromText(file) + } +} + +// loadRulesFromJSON 从JSON文件加载规则 +func (r *Rewriter) loadRulesFromJSON(file *os.File) error { + var rules []*RewriteRule + decoder := json.NewDecoder(file) + if err := decoder.Decode(&rules); err != nil { + return fmt.Errorf("解析JSON失败: %v", err) + } + + // 编译正则表达式 + for _, rule := range rules { + if rule.UseRegex { + regex, err := regexp.Compile(rule.Pattern) + if err != nil { + return fmt.Errorf("编译正则表达式'%s'失败: %v", rule.Pattern, err) + } + rule.regex = regex + } + r.rules = append(r.rules, rule) + } + + return nil +} + +// loadRulesFromText 从文本文件加载规则 +// 格式: pattern replacement [regex] [#description] +func (r *Rewriter) loadRulesFromText(file *os.File) error { + scanner := bufio.NewScanner(file) + lineNum := 0 + + for scanner.Scan() { + lineNum++ + line := strings.TrimSpace(scanner.Text()) + + // 跳过空行和注释 + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Fields(line) + if len(parts) < 2 { + return fmt.Errorf("第%d行格式错误: %s", lineNum, line) + } + + pattern := parts[0] + replacement := parts[1] + useRegex := false + description := "" + + // 检查是否有额外选项 + for i := 2; i < len(parts); i++ { + if parts[i] == "regex" { + useRegex = true + } else if strings.HasPrefix(parts[i], "#") { + // 获取描述信息 + description = strings.Join(parts[i:], " ") + description = strings.TrimPrefix(description, "#") + description = strings.TrimSpace(description) + break + } + } + + if err := r.AddRuleWithDescription(pattern, replacement, useRegex, description); err != nil { + return fmt.Errorf("第%d行添加规则失败: %v", lineNum, err) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("读取文件失败: %v", err) + } + + return nil +} + +// GetRules 获取所有规则 +func (r *Rewriter) GetRules() []*RewriteRule { + return r.rules +} + +// EnableRule 启用规则 +func (r *Rewriter) EnableRule(index int) error { + if index < 0 || index >= len(r.rules) { + return fmt.Errorf("规则索引越界: %d", index) + } + r.rules[index].Enabled = true + return nil +} + +// DisableRule 禁用规则 +func (r *Rewriter) DisableRule(index int) error { + if index < 0 || index >= len(r.rules) { + return fmt.Errorf("规则索引越界: %d", index) + } + r.rules[index].Enabled = false + return nil +} + +// RemoveRule 删除规则 +func (r *Rewriter) RemoveRule(index int) error { + if index < 0 || index >= len(r.rules) { + return fmt.Errorf("规则索引越界: %d", index) + } + r.rules = append(r.rules[:index], r.rules[index+1:]...) + return nil +} + +// SaveRulesToFile 将规则保存到文件 +func (r *Rewriter) SaveRulesToFile(filename string) error { + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("创建文件失败: %v", err) + } + defer file.Close() + + // 根据文件扩展名决定保存格式 + if strings.HasSuffix(filename, ".json") { + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(r.rules) + } else { + writer := bufio.NewWriter(file) + for _, rule := range r.rules { + line := rule.Pattern + " " + rule.Replacement + if rule.UseRegex { + line += " regex" + } + if rule.Description != "" { + line += " # " + rule.Description + } + if !rule.Enabled { + line = "# " + line + " (disabled)" + } + if _, err := writer.WriteString(line + "\n"); err != nil { + return fmt.Errorf("写入文件失败: %v", err) + } + } + return writer.Flush() + } +} diff --git a/examples/rewriter/rules.json b/examples/rewriter/rules.json new file mode 100644 index 0000000..95a3997 --- /dev/null +++ b/examples/rewriter/rules.json @@ -0,0 +1,30 @@ +[ + { + "pattern": "/api/v1/", + "replacement": "/api/v2/", + "use_regex": false, + "description": "将API v1请求重定向到v2", + "enabled": true + }, + { + "pattern": "/old/(.*)/page", + "replacement": "/new/$1/page", + "use_regex": true, + "description": "旧页面格式重定向到新格式", + "enabled": true + }, + { + "pattern": "/legacy-files/", + "replacement": "/files/", + "use_regex": false, + "description": "旧文件路径重定向", + "enabled": false + }, + { + "pattern": "^/(en|zh|ja)/(.*)", + "replacement": "/$2?lang=$1", + "use_regex": true, + "description": "将语言路径转换为查询参数", + "enabled": true + } +] \ No newline at end of file diff --git a/examples/rewriter/rules.txt b/examples/rewriter/rules.txt new file mode 100644 index 0000000..dc11818 --- /dev/null +++ b/examples/rewriter/rules.txt @@ -0,0 +1,7 @@ +# URL重写规则示例 +# 格式: pattern replacement [regex] [#description] + +/api/v1/ /api/v2/ # 将API v1请求重定向到v2 +/old/(.*)/page /new/$1/page regex # 旧页面格式重定向到新格式 +# /legacy-files/ /files/ # 旧文件路径重定向 (已禁用) +^/(en|zh|ja)/(.*) /$2?lang=$1 regex # 将语言路径转换为查询参数 \ No newline at end of file diff --git a/examples/rewriter/web_admin.go b/examples/rewriter/web_admin.go new file mode 100644 index 0000000..0cb3218 --- /dev/null +++ b/examples/rewriter/web_admin.go @@ -0,0 +1,259 @@ +package main + +import ( + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "strconv" + + "github.com/darkit/goproxy/examples/rewriter/rewriter" +) + +// 全局重写器实例 +var rw *rewriter.Rewriter + +// 规则配置文件路径 +var rulesFile = "rules.json" + +// AdminHandler Web管理页面处理器 +func AdminHandler(w http.ResponseWriter, r *http.Request) { + // 定义模板 + tmpl := ` + + + + + URL重写规则管理 + + + +

URL重写规则管理

+ +

当前规则

+ + + + + + + + + + + {{range $i, $rule := .Rules}} + + + + + + + + + + {{end}} +
索引匹配模式替换模式类型描述状态操作
{{$i}}{{$rule.Pattern}}{{$rule.Replacement}}{{if $rule.UseRegex}}正则表达式{{else}}前缀匹配{{end}}{{$rule.Description}} + {{if $rule.Enabled}}启用{{else}}禁用{{end}} + + {{if $rule.Enabled}} + + {{else}} + + {{end}} + +
+ +

添加新规则

+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+ +

保存配置

+
+ +
+ + +` + + // 解析模板 + t, err := template.New("admin").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 渲染模板 + data := struct { + Rules []*rewriter.RewriteRule + }{ + Rules: rw.GetRules(), + } + + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// AddRuleHandler 添加规则处理器 +func AddRuleHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + r.ParseForm() + + pattern := r.Form.Get("pattern") + replacement := r.Form.Get("replacement") + useRegex := r.Form.Get("regex") == "on" + description := r.Form.Get("description") + + if pattern == "" || replacement == "" { + http.Error(w, "匹配模式和替换模式不能为空", http.StatusBadRequest) + return + } + + if err := rw.AddRuleWithDescription(pattern, replacement, useRegex, description); err != nil { + http.Error(w, fmt.Sprintf("添加规则失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// EnableRuleHandler 启用规则处理器 +func EnableRuleHandler(w http.ResponseWriter, r *http.Request) { + // 解析URL中的规则索引 + indexStr := r.URL.Path[len("/enable/"):] + index, err := strconv.Atoi(indexStr) + if err != nil { + http.Error(w, "无效的规则索引", http.StatusBadRequest) + return + } + + if err := rw.EnableRule(index); err != nil { + http.Error(w, fmt.Sprintf("启用规则失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// DisableRuleHandler 禁用规则处理器 +func DisableRuleHandler(w http.ResponseWriter, r *http.Request) { + // 解析URL中的规则索引 + indexStr := r.URL.Path[len("/disable/"):] + index, err := strconv.Atoi(indexStr) + if err != nil { + http.Error(w, "无效的规则索引", http.StatusBadRequest) + return + } + + if err := rw.DisableRule(index); err != nil { + http.Error(w, fmt.Sprintf("禁用规则失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// RemoveRuleHandler 删除规则处理器 +func RemoveRuleHandler(w http.ResponseWriter, r *http.Request) { + // 解析URL中的规则索引 + indexStr := r.URL.Path[len("/remove/"):] + index, err := strconv.Atoi(indexStr) + if err != nil { + http.Error(w, "无效的规则索引", http.StatusBadRequest) + return + } + + if err := rw.RemoveRule(index); err != nil { + http.Error(w, fmt.Sprintf("删除规则失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// SaveRulesHandler 保存规则处理器 +func SaveRulesHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + if err := rw.SaveRulesToFile(rulesFile); err != nil { + http.Error(w, fmt.Sprintf("保存规则失败: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// APIHandler API处理器,返回JSON格式的规则列表 +func APIHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + rules := rw.GetRules() + json.NewEncoder(w).Encode(rules) +} + +func main() { + // 创建重写器 + rw = rewriter.NewRewriter() + + // 尝试从文件加载规则 + if err := rw.LoadRulesFromFile(rulesFile); err != nil { + log.Printf("加载规则失败: %v, 将使用空规则集", err) + } + + // 注册处理器 + http.HandleFunc("/", AdminHandler) + http.HandleFunc("/add", AddRuleHandler) + http.HandleFunc("/enable/", EnableRuleHandler) + http.HandleFunc("/disable/", DisableRuleHandler) + http.HandleFunc("/remove/", RemoveRuleHandler) + http.HandleFunc("/save", SaveRulesHandler) + http.HandleFunc("/api/rules", APIHandler) + + // 启动服务器 + log.Println("管理界面启动在 :8080...") + log.Fatal(http.ListenAndServe(":8080", nil)) +}