update: 2023-03-15
1. 添加URL重写器 2. 添加插件系统
This commit is contained in:
460
examples/plugin/admin/web_admin.go
Normal file
460
examples/plugin/admin/web_admin.go
Normal file
@@ -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 := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>插件管理系统</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #333; }
|
||||
.plugin-card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.plugin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.plugin-title { margin: 0; color: #333; }
|
||||
.plugin-version { color: #888; font-size: 0.9em; }
|
||||
.plugin-author { color: #888; font-size: 0.9em; }
|
||||
.plugin-desc { margin: 10px 0; }
|
||||
.plugin-status {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.status-enabled { background-color: #dff0d8; color: #3c763d; }
|
||||
.status-disabled { background-color: #f2dede; color: #a94442; }
|
||||
.plugin-actions { margin-top: 15px; }
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
margin-right: 5px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.btn-enable { background-color: #5cb85c; color: white; }
|
||||
.btn-disable { background-color: #d9534f; color: white; }
|
||||
.btn-config { background-color: #5bc0de; color: white; }
|
||||
.config-section {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.config-item {
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
}
|
||||
.config-key {
|
||||
font-weight: bold;
|
||||
width: 150px;
|
||||
}
|
||||
.system-info {
|
||||
background-color: #e9ecef;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>插件管理系统</h1>
|
||||
|
||||
<div class="system-info">
|
||||
<h3>系统信息</h3>
|
||||
<p>操作系统: {{.OS}}</p>
|
||||
<p>动态加载支持: {{if .DynamicLoadingSupported}}是{{else}}否{{end}}</p>
|
||||
<p>插件目录: {{.PluginsDir}}</p>
|
||||
<p>已加载插件数量: {{len .Plugins}}</p>
|
||||
</div>
|
||||
|
||||
<h2>已安装插件</h2>
|
||||
|
||||
{{range .Plugins}}
|
||||
<div class="plugin-card">
|
||||
<div class="plugin-header">
|
||||
<div>
|
||||
<h3 class="plugin-title">{{.Name}}</h3>
|
||||
<span class="plugin-version">版本: {{.Version}}</span>
|
||||
<span class="plugin-author">作者: {{.Author}}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{if .Enabled}}
|
||||
<span class="plugin-status status-enabled">已启用</span>
|
||||
{{else}}
|
||||
<span class="plugin-status status-disabled">已禁用</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-desc">{{.Description}}</div>
|
||||
|
||||
{{if .Config}}
|
||||
<div class="config-section">
|
||||
<h4>配置:</h4>
|
||||
{{range $key, $value := .Config}}
|
||||
<div class="config-item">
|
||||
<div class="config-key">{{$key}}:</div>
|
||||
<div class="config-value">{{$value}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="plugin-actions">
|
||||
{{if .Enabled}}
|
||||
<a href="/disable/{{.Name}}"><button class="btn-disable">禁用</button></a>
|
||||
{{else}}
|
||||
<a href="/enable/{{.Name}}"><button class="btn-enable">启用</button></a>
|
||||
{{end}}
|
||||
<a href="/config/{{.Name}}"><button class="btn-config">配置</button></a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .Plugins}}
|
||||
<p>没有已安装的插件。</p>
|
||||
{{end}}
|
||||
|
||||
<h2>操作</h2>
|
||||
<form action="/reload" method="post">
|
||||
<button type="submit">重新加载插件</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 解析模板
|
||||
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 := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>配置 {{.Info.Name}}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #333; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
input[type="text"], input[type="number"] { width: 100%; padding: 8px; box-sizing: border-box; }
|
||||
input[type="checkbox"] { margin-right: 5px; }
|
||||
button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background-color: #45a049; }
|
||||
.back-link { margin-top: 20px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>配置 {{.Info.Name}}</h1>
|
||||
|
||||
<form action="/config/{{.Info.Name}}" method="post">
|
||||
<input type="hidden" name="pluginName" value="{{.Info.Name}}">
|
||||
|
||||
{{range $key, $value := .Config}}
|
||||
<div class="form-group">
|
||||
<label for="{{$key}}">{{$key}}:</label>
|
||||
{{if eq (printf "%T" $value) "bool"}}
|
||||
<input type="checkbox" id="{{$key}}" name="{{$key}}" {{if $value}}checked{{end}}>
|
||||
{{else if eq (printf "%T" $value) "float64"}}
|
||||
<input type="number" id="{{$key}}" name="{{$key}}" value="{{$value}}" step="0.01">
|
||||
{{else if eq (printf "%T" $value) "int"}}
|
||||
<input type="number" id="{{$key}}" name="{{$key}}" value="{{$value}}">
|
||||
{{else}}
|
||||
<input type="text" id="{{$key}}" name="{{$key}}" value="{{$value}}">
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<button type="submit">保存配置</button>
|
||||
</form>
|
||||
|
||||
<a href="/" class="back-link">返回插件列表</a>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 解析模板
|
||||
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))
|
||||
}
|
91
examples/plugin/interface.go
Normal file
91
examples/plugin/interface.go
Normal file
@@ -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
|
||||
}
|
169
examples/plugin/manager/plugin_manager.go
Normal file
169
examples/plugin/manager/plugin_manager.go
Normal file
@@ -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("示例结束")
|
||||
}
|
410
examples/plugin/plugin.go
Normal file
410
examples/plugin/plugin.go
Normal file
@@ -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
|
||||
}
|
22
examples/plugin/plugins/build.sh
Normal file
22
examples/plugin/plugins/build.sh
Normal file
@@ -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
|
126
examples/plugin/plugins/logger/logger_plugin.go
Normal file
126
examples/plugin/plugins/logger/logger_plugin.go
Normal file
@@ -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() {
|
||||
// 不会被执行,仅用于编译插件
|
||||
}
|
170
examples/plugin/plugins/stats/stats_plugin.go
Normal file
170
examples/plugin/plugins/stats/stats_plugin.go
Normal file
@@ -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() {
|
||||
// 不会被执行,仅用于编译插件
|
||||
}
|
121
examples/rewriter/http_server.go
Normal file
121
examples/rewriter/http_server.go
Normal file
@@ -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))
|
||||
}
|
88
examples/rewriter/main.go
Normal file
88
examples/rewriter/main.go
Normal file
@@ -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("规则已保存")
|
||||
}
|
||||
}
|
285
examples/rewriter/rewriter/rewriter.go
Normal file
285
examples/rewriter/rewriter/rewriter.go
Normal file
@@ -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()
|
||||
}
|
||||
}
|
30
examples/rewriter/rules.json
Normal file
30
examples/rewriter/rules.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
7
examples/rewriter/rules.txt
Normal file
7
examples/rewriter/rules.txt
Normal file
@@ -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 # 将语言路径转换为查询参数
|
259
examples/rewriter/web_admin.go
Normal file
259
examples/rewriter/web_admin.go
Normal file
@@ -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 := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>URL重写规则管理</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #333; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
th { background-color: #f2f2f2; }
|
||||
.enabled { color: green; }
|
||||
.disabled { color: red; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; }
|
||||
input[type="text"], textarea { width: 100%; padding: 8px; box-sizing: border-box; }
|
||||
input[type="checkbox"] { margin-right: 5px; }
|
||||
button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background-color: #45a049; }
|
||||
.action-btn { padding: 5px 10px; margin-right: 5px; }
|
||||
.delete-btn { background-color: #f44336; }
|
||||
.edit-btn { background-color: #2196F3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>URL重写规则管理</h1>
|
||||
|
||||
<h2>当前规则</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>索引</th>
|
||||
<th>匹配模式</th>
|
||||
<th>替换模式</th>
|
||||
<th>类型</th>
|
||||
<th>描述</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
{{range $i, $rule := .Rules}}
|
||||
<tr>
|
||||
<td>{{$i}}</td>
|
||||
<td>{{$rule.Pattern}}</td>
|
||||
<td>{{$rule.Replacement}}</td>
|
||||
<td>{{if $rule.UseRegex}}正则表达式{{else}}前缀匹配{{end}}</td>
|
||||
<td>{{$rule.Description}}</td>
|
||||
<td class="{{if $rule.Enabled}}enabled{{else}}disabled{{end}}">
|
||||
{{if $rule.Enabled}}启用{{else}}禁用{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if $rule.Enabled}}
|
||||
<a href="/disable/{{$i}}"><button class="action-btn">禁用</button></a>
|
||||
{{else}}
|
||||
<a href="/enable/{{$i}}"><button class="action-btn">启用</button></a>
|
||||
{{end}}
|
||||
<a href="/remove/{{$i}}"><button class="action-btn delete-btn">删除</button></a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
<h2>添加新规则</h2>
|
||||
<form action="/add" method="post">
|
||||
<div class="form-group">
|
||||
<label for="pattern">匹配模式:</label>
|
||||
<input type="text" id="pattern" name="pattern" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="replacement">替换模式:</label>
|
||||
<input type="text" id="replacement" name="replacement" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="regex">
|
||||
<input type="checkbox" id="regex" name="regex">
|
||||
使用正则表达式
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">描述:</label>
|
||||
<textarea id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<button type="submit">添加规则</button>
|
||||
</form>
|
||||
|
||||
<h2>保存配置</h2>
|
||||
<form action="/save" method="post">
|
||||
<button type="submit">保存规则配置</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 解析模板
|
||||
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))
|
||||
}
|
Reference in New Issue
Block a user