From 86d5d22209a9566a6bbb21f942da949a03b057b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 4 Jul 2023 01:36:32 +0800 Subject: [PATCH] refactor: plugin --- .github/workflows/issue-auto-reply.yml | 22 +- app/http/controllers/plugin_controller.go | 246 ++++++++++++ .../plugins}/openresty_controller.go | 10 +- app/http/controllers/plugins/plugins.go | 52 +++ app/http/middleware/jwt.go | 2 +- app/jobs/process_task.go | 84 +++++ app/providers/queue_service_provider.go | 6 +- app/providers/route_service_provider.go | 1 + app/services/plugin.go | 79 ++++ bootstrap/plugins.go | 7 - config/queue.go | 7 +- main.go | 3 - plugins/openresty/openresty.go | 10 +- public/panel/adminui/src/modules/view.js | 4 +- public/panel/views/plugin.html | 184 +++++++++ .../openresty/routes.go => routes/plugin.go | 9 +- routes/web.go | 8 + scripts/plugins/openresty/install.sh | 353 ++++++++++++++++++ scripts/plugins/openresty/uninstall.sh | 19 + 19 files changed, 1072 insertions(+), 34 deletions(-) create mode 100644 app/http/controllers/plugin_controller.go rename {plugins/openresty/http/controllers => app/http/controllers/plugins}/openresty_controller.go (98%) create mode 100644 app/http/controllers/plugins/plugins.go create mode 100644 app/jobs/process_task.go create mode 100644 app/services/plugin.go delete mode 100644 bootstrap/plugins.go create mode 100644 public/panel/views/plugin.html rename plugins/openresty/routes.go => routes/plugin.go (83%) create mode 100644 scripts/plugins/openresty/install.sh create mode 100644 scripts/plugins/openresty/uninstall.sh diff --git a/.github/workflows/issue-auto-reply.yml b/.github/workflows/issue-auto-reply.yml index 4c163644..0de5a001 100644 --- a/.github/workflows/issue-auto-reply.yml +++ b/.github/workflows/issue-auto-reply.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - name: bug or enhancement + - name: ✏️ Feature if: github.event.label.name == '✏️ Feature' uses: actions-cool/issues-helper@v3 with: @@ -24,5 +24,21 @@ jobs: body: | Hi @${{ github.event.issue.user.login }} 👋 - 我们认为您的反馈非常有价值!如果有兴趣欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! - We think your feedback is very valuable! If you are interested, please submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! + 我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! + We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! + + "Talk is cheap, Show me the Code." - Linus Torvalds + - name: ☢️ Bug + if: github.event.label.name == '☢️ Bug' + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + Hi @${{ github.event.issue.user.login }} 👋 + + 我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! + We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! + + "Talk is cheap, Show me the Code." - Linus Torvalds diff --git a/app/http/controllers/plugin_controller.go b/app/http/controllers/plugin_controller.go new file mode 100644 index 00000000..83eceeec --- /dev/null +++ b/app/http/controllers/plugin_controller.go @@ -0,0 +1,246 @@ +package controllers + +import ( + "github.com/goravel/framework/contracts/queue" + "github.com/goravel/framework/facades" + "panel/app/jobs" + "sync" + + "github.com/goravel/framework/contracts/http" + + "panel/app/models" + "panel/app/services" +) + +type PluginController struct { + plugin services.Plugin +} + +func NewPluginController() *PluginController { + return &PluginController{ + plugin: services.NewPluginImpl(), + } +} + +// List 列出所有插件 +func (r *PluginController) List(ctx http.Context) { + plugins := r.plugin.All() + installedPlugins, err := r.plugin.AllInstalled() + if err != nil { + Error(ctx, http.StatusInternalServerError, "系统内部错误") + } + + var lock sync.RWMutex + installedPluginsMap := make(map[string]models.Plugin) + + for _, p := range installedPlugins { + lock.Lock() + installedPluginsMap[p.Slug] = p + lock.Unlock() + } + + type plugin struct { + Name string `json:"name"` + Author string `json:"author"` + Description string `json:"description"` + Slug string `json:"slug"` + Version string `json:"version"` + Requires []string `json:"requires"` + Excludes []string `json:"excludes"` + Installed bool `json:"installed"` + InstalledVersion string `json:"installed_version"` + Show bool `json:"show"` + } + + var p []plugin + for _, item := range plugins { + installed, installedVersion, show := false, "", false + if _, ok := installedPluginsMap[item.Slug]; ok { + installed = true + installedVersion = installedPluginsMap[item.Slug].Version + show = installedPluginsMap[item.Slug].Show + } + p = append(p, plugin{ + Name: item.Name, + Author: item.Author, + Description: item.Description, + Slug: item.Slug, + Version: item.Version, + Requires: item.Requires, + Excludes: item.Excludes, + Installed: installed, + InstalledVersion: installedVersion, + Show: show, + }) + } + + Success(ctx, p) +} + +// Install 安装插件 +func (r *PluginController) Install(ctx http.Context) { + slug := ctx.Request().Input("slug") + plugins := r.plugin.All() + + var plugin services.PanelPlugin + check := false + for _, item := range plugins { + if item.Slug == slug { + check = true + plugin = item + break + } + } + if !check { + Error(ctx, http.StatusBadRequest, "插件不存在") + return + } + + var installedPlugin models.Plugin + if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil { + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + if installedPlugin.ID != 0 { + Error(ctx, http.StatusBadRequest, "插件已安装") + } + + var task models.Task + task.Name = "安装插件 " + plugin.Name + task.Status = models.TaskStatusWaiting + task.Shell = "bash scripts/plugins/" + plugin.Slug + "/install.sh >> /tmp/" + plugin.Slug + ".log 2>&1" + task.Log = "/tmp/" + plugin.Slug + ".log" + if err := facades.Orm().Query().Create(&task); err != nil { + facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error()) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + + processTask(task.ID) + Success(ctx, "任务已提交") +} + +// Uninstall 卸载插件 +func (r *PluginController) Uninstall(ctx http.Context) { + slug := ctx.Request().Input("slug") + plugins := r.plugin.All() + + var plugin services.PanelPlugin + check := false + for _, item := range plugins { + if item.Slug == slug { + check = true + plugin = item + break + } + } + if !check { + Error(ctx, http.StatusBadRequest, "插件不存在") + return + } + + var installedPlugin models.Plugin + if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil { + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + if installedPlugin.ID == 0 { + Error(ctx, http.StatusBadRequest, "插件未安装") + } + + var task models.Task + task.Name = "卸载插件 " + plugin.Name + task.Status = models.TaskStatusWaiting + task.Shell = "bash scripts/plugins/" + plugin.Slug + "/uninstall.sh >> /tmp/" + plugin.Slug + ".log 2>&1" + task.Log = "/tmp/" + plugin.Slug + ".log" + if err := facades.Orm().Query().Create(&task); err != nil { + facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error()) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + + processTask(task.ID) + Success(ctx, "任务已提交") +} + +// Update 更新插件 +func (r *PluginController) Update(ctx http.Context) { + slug := ctx.Request().Input("slug") + plugins := r.plugin.All() + + var plugin services.PanelPlugin + check := false + for _, item := range plugins { + if item.Slug == slug { + check = true + plugin = item + break + } + } + if !check { + Error(ctx, http.StatusBadRequest, "插件不存在") + return + } + + var installedPlugin models.Plugin + if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil { + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + if installedPlugin.ID == 0 { + Error(ctx, http.StatusBadRequest, "插件未安装") + } + + var task models.Task + task.Name = "更新插件 " + plugin.Name + task.Status = models.TaskStatusWaiting + task.Shell = "bash scripts/plugins/" + plugin.Slug + "/update.sh >> /tmp/" + plugin.Slug + ".log 2>&1" + task.Log = "/tmp/" + plugin.Slug + ".log" + if err := facades.Orm().Query().Create(&task); err != nil { + facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error()) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + + processTask(task.ID) + Success(ctx, "任务已提交") +} + +// UpdateShow 更新插件首页显示状态 +func (r *PluginController) UpdateShow(ctx http.Context) { + slug := ctx.Request().Input("slug") + show := ctx.Request().InputBool("show") + + var plugin models.Plugin + if err := facades.Orm().Query().Where("slug", slug).First(&plugin); err != nil { + facades.Log().Error("[面板][PluginController] 查询插件失败: " + err.Error()) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + if plugin.ID == 0 { + Error(ctx, http.StatusBadRequest, "插件未安装") + return + } + + plugin.Show = show + if err := facades.Orm().Query().Save(&plugin); err != nil { + facades.Log().Error("[面板][PluginController] 更新插件失败: " + err.Error()) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + return + } + + Success(ctx, "操作成功") +} + +// processTask 处理任务 +func processTask(taskID uint) { + go func() { + err := facades.Queue().Job(&jobs.ProcessTask{}, []queue.Arg{ + {Type: "uint", Value: taskID}, + }).Dispatch() + if err != nil { + facades.Log().Error("[面板][PluginController] 运行任务失败: " + err.Error()) + return + } + }() +} diff --git a/plugins/openresty/http/controllers/openresty_controller.go b/app/http/controllers/plugins/openresty_controller.go similarity index 98% rename from plugins/openresty/http/controllers/openresty_controller.go rename to app/http/controllers/plugins/openresty_controller.go index 73ba2a55..85c8d679 100644 --- a/plugins/openresty/http/controllers/openresty_controller.go +++ b/app/http/controllers/plugins/openresty_controller.go @@ -1,4 +1,4 @@ -package controllers +package plugins import ( "os" @@ -29,6 +29,8 @@ func NewOpenrestyController() *OpenRestyController { // Status 获取运行状态 func (r *OpenRestyController) Status(ctx http.Context) { + Check(ctx, "openresty") + cmd := exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'") out, err := cmd.CombinedOutput() if err != nil { @@ -51,6 +53,8 @@ func (r *OpenRestyController) Status(ctx http.Context) { // Reload 重载配置 func (r *OpenRestyController) Reload(ctx http.Context) { + Check(ctx, "openresty") + cmd := exec.Command("bash", "-c", "systemctl reload openresty") _, err := cmd.CombinedOutput() if err != nil { @@ -82,6 +86,8 @@ func (r *OpenRestyController) Reload(ctx http.Context) { // Start 启动OpenResty func (r *OpenRestyController) Start(ctx http.Context) { + Check(ctx, "openresty") + cmd := exec.Command("bash", "-c", "systemctl start openresty") _, err := cmd.CombinedOutput() if err != nil { @@ -113,6 +119,8 @@ func (r *OpenRestyController) Start(ctx http.Context) { // Stop 停止OpenResty func (r *OpenRestyController) Stop(ctx http.Context) { + Check(ctx, "openresty") + cmd := exec.Command("bash", "-c", "systemctl stop openresty") _, err := cmd.CombinedOutput() if err != nil { diff --git a/app/http/controllers/plugins/plugins.go b/app/http/controllers/plugins/plugins.go new file mode 100644 index 00000000..c18b7a45 --- /dev/null +++ b/app/http/controllers/plugins/plugins.go @@ -0,0 +1,52 @@ +package plugins + +import ( + "sync" + + "github.com/goravel/framework/contracts/http" + "github.com/goravel/framework/facades" + + "panel/app/services" +) + +// Check 检查插件是否可用 +func Check(ctx http.Context, slug string) { + plugin := services.NewPluginImpl().GetBySlug(slug) + installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug) + installedPlugins, err := services.NewPluginImpl().AllInstalled() + if err != nil { + facades.Log().Error("[面板][插件] 获取已安装插件失败") + ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, "系统内部错误") + } + + if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug { + ctx.Request().AbortWithStatusJson(http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本") + } + + var lock sync.RWMutex + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + lock.Lock() + pluginsMap[p.Slug] = true + lock.Unlock() + } + + for _, require := range plugin.Requires { + lock.RLock() + _, requireFound := pluginsMap[require] + lock.RUnlock() + if !requireFound { + ctx.Request().AbortWithStatusJson(http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件") + } + } + + for _, exclude := range plugin.Excludes { + lock.RLock() + _, excludeFound := pluginsMap[exclude] + lock.RUnlock() + if excludeFound { + ctx.Request().AbortWithStatusJson(http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件") + } + } +} diff --git a/app/http/middleware/jwt.go b/app/http/middleware/jwt.go index f6e2f991..b92860ab 100644 --- a/app/http/middleware/jwt.go +++ b/app/http/middleware/jwt.go @@ -13,7 +13,7 @@ import ( // Jwt 确保通过 JWT 鉴权 func Jwt() http.Middleware { return func(ctx http.Context) { - token := ctx.Request().Input("access_token", "") + token := ctx.Request().Header("access_token", ctx.Request().Input("access_token", "")) if len(token) == 0 { ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ "code": 401, diff --git a/app/jobs/process_task.go b/app/jobs/process_task.go new file mode 100644 index 00000000..22679f01 --- /dev/null +++ b/app/jobs/process_task.go @@ -0,0 +1,84 @@ +package jobs + +import ( + "os/exec" + "time" + + "github.com/goravel/framework/facades" + + "panel/app/models" +) + +// ProcessTask 处理面板任务 +type ProcessTask struct { +} + +// Signature The name and signature of the job. +func (receiver *ProcessTask) Signature() string { + return "process_task" +} + +// Handle Execute the job. +func (receiver *ProcessTask) Handle(args ...any) error { + taskID, ok := args[0].(uint) + if !ok { + facades.Log().Error("[面板][ProcessTask] 任务ID参数错误") + return nil + } + + for { + if !haveRunningTask() { + break + } + time.Sleep(5 * time.Second) + } + + var task models.Task + if err := facades.Orm().Query().Where("id = ?", taskID).Get(&task); err != nil { + facades.Log().Errorf("[面板][ProcessTask] 获取任务%d失败: %s", taskID, err.Error()) + return nil + } + + task.Status = models.TaskStatusRunning + if err := facades.Orm().Query().Save(&task); err != nil { + facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error()) + return nil + } + + facades.Log().Infof("[面板][ProcessTask] 开始执行任务%d", taskID) + cmd := exec.Command("bash", "-c", task.Shell) + err := cmd.Run() + if err != nil { + task.Status = models.TaskStatusFailed + if err := facades.Orm().Query().Save(&task); err != nil { + facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error()) + return nil + } + facades.Log().Errorf("[面板][ProcessTask] 任务%d执行失败: %s", taskID, err.Error()) + return nil + } + + task.Status = models.TaskStatusSuccess + if err := facades.Orm().Query().Save(&task); err != nil { + facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error()) + return nil + } + + facades.Log().Infof("[面板][ProcessTask] 任务%d执行成功", taskID) + return nil +} + +// haveRunningTask 是否有任务正在执行 +func haveRunningTask() bool { + var task models.Task + if err := facades.Orm().Query().Where("status = ?", models.TaskStatusRunning).Get(&task); err != nil { + facades.Log().Error("[面板][ProcessTask] 获取任务失败: " + err.Error()) + return true + } + + if task.ID != 0 { + return true + } + + return false +} diff --git a/app/providers/queue_service_provider.go b/app/providers/queue_service_provider.go index d65f2113..0ca292ea 100644 --- a/app/providers/queue_service_provider.go +++ b/app/providers/queue_service_provider.go @@ -4,6 +4,8 @@ import ( "github.com/goravel/framework/contracts/foundation" "github.com/goravel/framework/contracts/queue" "github.com/goravel/framework/facades" + + "panel/app/jobs" ) type QueueServiceProvider struct { @@ -18,5 +20,7 @@ func (receiver *QueueServiceProvider) Boot(app foundation.Application) { } func (receiver *QueueServiceProvider) Jobs() []queue.Job { - return []queue.Job{} + return []queue.Job{ + &jobs.ProcessTask{}, + } } diff --git a/app/providers/route_service_provider.go b/app/providers/route_service_provider.go index 032d1e24..d7255c3e 100644 --- a/app/providers/route_service_provider.go +++ b/app/providers/route_service_provider.go @@ -20,6 +20,7 @@ func (receiver *RouteServiceProvider) Boot(app foundation.Application) { receiver.configureRateLimiting() routes.Web() + routes.Plugin() } func (receiver *RouteServiceProvider) configureRateLimiting() { diff --git a/app/services/plugin.go b/app/services/plugin.go new file mode 100644 index 00000000..72764dfb --- /dev/null +++ b/app/services/plugin.go @@ -0,0 +1,79 @@ +package services + +import ( + "github.com/goravel/framework/facades" + + "panel/app/models" + "panel/plugins/openresty" +) + +// PanelPlugin 插件元数据结构 +type PanelPlugin struct { + Name string + Author string + Description string + Slug string + Version string + Requires []string + Excludes []string +} + +type Plugin interface { + AllInstalled() ([]models.Plugin, error) + All() []PanelPlugin +} + +type PluginImpl struct { +} + +func NewPluginImpl() *PluginImpl { + return &PluginImpl{} +} + +// AllInstalled 获取已安装的所有插件 +func (r *PluginImpl) AllInstalled() ([]models.Plugin, error) { + var plugins []models.Plugin + if err := facades.Orm().Query().Get(&plugins); err != nil { + return plugins, err + } + + return plugins, nil +} + +// All 获取所有插件 +func (r *PluginImpl) All() []PanelPlugin { + var p []PanelPlugin + + p = append(p, PanelPlugin{ + Name: openresty.Name, + Author: openresty.Author, + Description: openresty.Description, + Slug: openresty.Slug, + Version: openresty.Version, + Requires: openresty.Requires, + Excludes: openresty.Excludes, + }) + + return p +} + +// GetBySlug 根据slug获取插件 +func (r *PluginImpl) GetBySlug(slug string) PanelPlugin { + for _, item := range r.All() { + if item.Slug == slug { + return item + } + } + + return PanelPlugin{} +} + +// GetInstalledBySlug 根据slug获取已安装的插件 +func (r *PluginImpl) GetInstalledBySlug(slug string) models.Plugin { + var plugin models.Plugin + if err := facades.Orm().Query().Where("slug", slug).Get(&plugin); err != nil { + return plugin + } + + return plugin +} diff --git a/bootstrap/plugins.go b/bootstrap/plugins.go deleted file mode 100644 index 82394537..00000000 --- a/bootstrap/plugins.go +++ /dev/null @@ -1,7 +0,0 @@ -package bootstrap - -import "panel/plugins/openresty" - -func Plugins() { - openresty.Boot() -} diff --git a/config/queue.go b/config/queue.go index ff79c6d3..a13007df 100644 --- a/config/queue.go +++ b/config/queue.go @@ -8,7 +8,7 @@ func init() { config := facades.Config() config.Add("queue", map[string]any{ // Default Queue Connection Name - "default": config.Env("QUEUE_CONNECTION", "sync"), + "default": "sync", // Queue Connections // @@ -18,11 +18,6 @@ func init() { "sync": map[string]any{ "driver": "sync", }, - "redis": map[string]any{ - "driver": "redis", - "connection": "default", - "queue": config.Env("REDIS_QUEUE", "default"), - }, }, }) } diff --git a/main.go b/main.go index 30e4b37b..d548c45d 100644 --- a/main.go +++ b/main.go @@ -25,9 +25,6 @@ func main() { // 启动框架 bootstrap.Boot() - // 加载插件 - bootstrap.Plugins() - // 启动 HTTP 服务 go func() { if err := facades.Route().Run(); err != nil { diff --git a/plugins/openresty/openresty.go b/plugins/openresty/openresty.go index be7371f1..1b5af222 100644 --- a/plugins/openresty/openresty.go +++ b/plugins/openresty/openresty.go @@ -1,15 +1,11 @@ package openresty -const ( +var ( Name = "OpenResty" Author = "耗子" Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。" Slug = "openresty" Version = "1.21.4.1" - Requires = "" - Excludes = "" + Requires = []string{} + Excludes = []string{} ) - -func Boot() { - Route() -} diff --git a/public/panel/adminui/src/modules/view.js b/public/panel/adminui/src/modules/view.js index ee2e9794..b6a74b5e 100644 --- a/public/panel/adminui/src/modules/view.js +++ b/public/panel/adminui/src/modules/view.js @@ -63,8 +63,10 @@ layui.define(['laytpl', 'layer'], function (exports) { delete options.success delete options.error + options.data = JSON.stringify(options.data) + return $.ajax($.extend({ - type: 'get', dataType: 'json', success: function (res) { + type: 'get', dataType: 'json', contentType: 'application/json', success: function (res) { var statusCode = response.statusCode //只有 response 的 code 一切正常才执行 done diff --git a/public/panel/views/plugin.html b/public/panel/views/plugin.html new file mode 100644 index 00000000..8aab854d --- /dev/null +++ b/public/panel/views/plugin.html @@ -0,0 +1,184 @@ +插件中心 + +
+
+
+ 按钮点击一次即可,请勿重复点击以免重复操作,任务中心在右上方! +
+
+
+ + + + +
+
+
+ + diff --git a/plugins/openresty/routes.go b/routes/plugin.go similarity index 83% rename from plugins/openresty/routes.go rename to routes/plugin.go index a5c5f95c..37235069 100644 --- a/plugins/openresty/routes.go +++ b/routes/plugin.go @@ -1,16 +1,17 @@ -package openresty +package routes import ( "github.com/goravel/framework/contracts/route" "github.com/goravel/framework/facades" + "panel/app/http/controllers/plugins" "panel/app/http/middleware" - "panel/plugins/openresty/http/controllers" ) -func Route() { +// Plugin 加载插件路由 +func Plugin() { facades.Route().Prefix("api/plugins/openresty").Middleware(middleware.Jwt()).Group(func(route route.Route) { - openRestyController := controllers.NewOpenrestyController() + openRestyController := plugins.NewOpenrestyController() route.Get("status", openRestyController.Status) route.Post("reload", openRestyController.Reload) route.Post("start", openRestyController.Start) diff --git a/routes/web.go b/routes/web.go index dd497079..a81a7313 100644 --- a/routes/web.go +++ b/routes/web.go @@ -33,6 +33,14 @@ func Web() { websiteController := controllers.NewWebsiteController() r.Get("list", websiteController.List) }) + r.Prefix("plugin").Middleware(middleware.Jwt()).Group(func(r route.Route) { + pluginController := controllers.NewPluginController() + r.Get("list", pluginController.List) + r.Post("install", pluginController.Install) + r.Post("uninstall", pluginController.Uninstall) + r.Post("update", pluginController.Update) + r.Post("updateShow", pluginController.UpdateShow) + }) }) facades.Route().Fallback(func(ctx http.Context) { diff --git a/scripts/plugins/openresty/install.sh b/scripts/plugins/openresty/install.sh new file mode 100644 index 00000000..ffe4ed19 --- /dev/null +++ b/scripts/plugins/openresty/install.sh @@ -0,0 +1,353 @@ +#!/bin/bash + +: ' +Copyright [2022] [HaoZi Technology Co., Ltd.] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +' + +HR="+----------------------------------------------------" + +ARCH=$(uname -m) +cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) +OS=$(source /etc/os-release && [[ "$ID" == "debian" ]] && echo "debian" || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]]; } && echo "centos" || echo "unknown") +downloadUrl="https://dl.cdn.haozi.net" +setupPath="/www" +openrestyPath="${setupPath}/server/openresty" +openrestyVersion="1.21.4.1" + +# 安装依赖 +if [ "${OS}" == "centos" ]; then + dnf install gcc gcc-c++ make tar unzip gd gd-devel git-core perl oniguruma oniguruma-devel bison yajl yajl-devel curl curl-devel libtermcap-devel ncurses-devel libevent-devel readline-devel libuuid-devel brotli-devel icu libicu libicu-devel openssl openssl-devel -y +elif [ "${OS}" == "debian" ]; then + apt install gcc g++ make tar unzip libgd3 libgd-dev git perl libonig-dev bison libyajl-dev curl libcurl4-openssl-dev libncurses5-dev libevent-dev libreadline-dev uuid-dev libbrotli-dev icu-devtools libicu-dev openssl libssl-dev -y +else + echo -e $HR + echo "错误:耗子Linux面板不支持该系统" + exit 1 +fi + +# 准备目录 +rm -rf ${openrestyPath} +mkdir -p ${openrestyPath} +cd ${openrestyPath} + +# 下载源码 +wget -T 120 -O ${openrestyPath}/openresty-${openrestyVersion}.tar.gz ${downloadUrl}/panel/plugins/openresty/openresty-${openrestyVersion}.tar.gz +tar -xvf openresty-${openrestyVersion}.tar.gz +rm -f openresty-${openrestyVersion}.tar.gz +mv openresty-${openrestyVersion} src +cd src + +# openssl +wget -T 120 -O openssl.tar.gz ${downloadUrl}/panel/plugins/openresty/openssl-1.1.1u.tar.gz +tar -zxvf openssl.tar.gz +rm -f openssl.tar.gz +mv openssl-1.1.1u openssl +rm -f openssl.tar.gz + +# pcre +wget -T 60 -O pcre-8.45.tar.gz ${downloadUrl}/panel/plugins/openresty/pcre-8.45.tar.gz +tar -zxvf pcre-8.45.tar.gz +rm -f pcre-8.45.tar.gz +mv pcre-8.45 pcre +rm -f pcre-8.45.tar.gz + +# ngx_cache_purge +wget -T 20 -O ngx_cache_purge.tar.gz ${downloadUrl}/panel/plugins/openresty/ngx_cache_purge-2.3.tar.gz +tar -zxvf ngx_cache_purge.tar.gz +rm -f ngx_cache_purge.tar.gz +mv ngx_cache_purge-2.3 ngx_cache_purge +rm -f ngx_cache_purge.tar.gz + +# nginx-sticky-module +wget -T 20 -O nginx-sticky-module.zip ${downloadUrl}/panel/plugins/openresty/nginx-sticky-module.zip +unzip -o nginx-sticky-module.zip +rm -f nginx-sticky-module.zip + +# nginx-dav-ext-module +wget -T 20 -O nginx-dav-ext-module-3.0.0.tar.gz ${downloadUrl}/panel/plugins/openresty/nginx-dav-ext-module-3.0.0.tar.gz +tar -xvf nginx-dav-ext-module-3.0.0.tar.gz +rm -f nginx-dav-ext-module-3.0.0.tar.gz +mv nginx-dav-ext-module-3.0.0 nginx-dav-ext-module + +# waf +cd ${openrestyPath} +git clone -b lts https://ghproxy.com/https://github.com/ADD-SP/ngx_waf.git +if [ "$?" != "0" ]; then + echo -e $HR + echo "错误:OpenResty waf拓展下载失败,请截图错误信息寻求帮助。" + rm -rf ${openrestyPath} + exit 1 +fi +git clone -b v2.3.0 https://ghproxy.com/https://github.com/troydhanson/uthash.git +if [ "$?" != "0" ]; then + echo -e $HR + echo "错误:OpenResty waf拓展uthash下载失败,请截图错误信息寻求帮助。" + rm -rf ${openrestyPath} + exit 1 +fi +cd ngx_waf/inc +wget -T 60 -O libinjection.zip ${downloadUrl}/panel/plugins/openresty/libinjection-3.10.0.zip +unzip -o libinjection.zip +mv libinjection-3.10.0 libinjection +rm -rf libinjection.zip +cd ../ +make -j${cpuCore} +if [ "$?" != "0" ]; then + echo -e $HR + echo "错误:OpenResty waf拓展初始化失败,请截图错误信息寻求帮助。" + rm -rf ${openrestyPath} + exit 1 +fi +cd ${openrestyPath}/src + +# brotli +wget -T 20 -O ngx_brotli.zip ${downloadUrl}/panel/plugins/openresty/ngx_brotli-1.0.0rc.zip +unzip -o ngx_brotli.zip +mv ngx_brotli-1.0.0rc ngx_brotli +cd ngx_brotli/deps +rm -rf brotli +wget -T 20 -O brotli.zip ${downloadUrl}/panel/plugins/openresty/brotli-1.0.9.zip +unzip -o brotli.zip +mv brotli-1.0.9 brotli +cd ${openrestyPath}/src + +cd ${openrestyPath}/src +export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH +export LIB_UTHASH=${openrestyPath}/uthash + +./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/openssl --with-pcre=${openrestyPath}/src/pcre --with-http_v2_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-ld-opt="-Wl,-E" --with-cc-opt="-O2 -std=gnu99" --with-cpu-opt="amd64" --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/ngx_waf +make -j${cpuCore} +if [ "$?" != "0" ]; then + echo -e $HR + echo "提示:OpenResty多线程编译失败,尝试单线程编译..." + make + if [ "$?" != "0" ]; then + echo -e $HR + echo "错误:OpenResty编译失败,请截图错误信息寻求帮助。" + rm -rf ${openrestyPath} + exit 1 + fi +fi +make install +if [ ! -f "${openrestyPath}/nginx/sbin/nginx" ]; then + echo -e $HR + echo "错误:OpenResty安装失败,请截图错误信息寻求帮助。" + rm -rf ${openrestyPath} + exit 1 +fi + +# 设置软链接 +ln -sf ${openrestyPath}/nginx/html ${openrestyPath}/html +ln -sf ${openrestyPath}/nginx/conf ${openrestyPath}/conf +ln -sf ${openrestyPath}/nginx/logs ${openrestyPath}/logs +ln -sf ${openrestyPath}/nginx/sbin ${openrestyPath}/sbin +ln -sf ${openrestyPath}/nginx/sbin/nginx /usr/bin/openresty +rm -f ${openrestyPath}/conf/nginx.conf + +# 创建配置目录 +cd ${openrestyPath} +rm -f openresty-${openrestyVersion}.tar.gz +rm -rf src +mkdir -p /www/wwwroot/default +mkdir -p /www/wwwlogs +mkdir -p /www/server/vhost/openresty +mkdir -p /www/server/vhost/openresty/rewrite +mkdir -p /www/server/vhost/openresty/ssl + +# 写入主配置文件 +cat >${openrestyPath}/conf/nginx.conf <\d+)$ { + fastcgi_pass unix:/tmp/php-cgi-$version.sock; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $fastcgi_script_name; + } + } + include /www/server/vhost/openresty/*.conf; +} +EOF +# 写入pathinfo配置文件 +cat >${openrestyPath}/conf/pathinfo.conf <${openrestyPath}/html/index.html < + + + +耗子Linux面板 + + +

耗子Linux面板

+

这是耗子Linux面板的OpenResty默认页面!

+

当您看到此页面,说明该域名尚未与站点绑定。

+ + +EOF + +# 写入站点停止页 +cat >${openrestyPath}/html/stop.html < + + + +网站已停止 - 耗子Linux面板 + + +

耗子Linux面板

+

该网站已被管理员停止访问!

+

当您看到此页面,说明该网站已被管理员停止对外访问,请联系管理员了解详情。

+ + +EOF + +# 处理文件权限 +chmod 755 ${openrestyPath} +chmod 644 ${openrestyPath}/html +chmod -R 755 /www/wwwroot +chown -R www:www /www/wwwroot +chmod -R 644 /www/server/vhost + +# 写入无php配置文件 +echo "" >${openrestyPath}/conf/enable-php-00.conf +# 写入代理默认配置文件 +cat >${openrestyPath}/conf/proxy.conf </lib/systemd/system/openresty.service <