2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 09:13:49 +08:00

refactor: plugin

This commit is contained in:
耗子
2023-07-04 01:36:32 +08:00
parent a07ac77d2b
commit 86d5d22209
19 changed files with 1072 additions and 34 deletions

View File

@@ -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

View File

@@ -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
}
}()
}

View File

@@ -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 {

View File

@@ -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+" 插件")
}
}
}

View File

@@ -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,

84
app/jobs/process_task.go Normal file
View File

@@ -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
}

View File

@@ -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{},
}
}

View File

@@ -20,6 +20,7 @@ func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
receiver.configureRateLimiting()
routes.Web()
routes.Plugin()
}
func (receiver *RouteServiceProvider) configureRateLimiting() {

79
app/services/plugin.go Normal file
View File

@@ -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
}

View File

@@ -1,7 +0,0 @@
package bootstrap
import "panel/plugins/openresty"
func Plugins() {
openresty.Boot()
}

View File

@@ -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"),
},
},
})
}

View File

@@ -25,9 +25,6 @@ func main() {
// 启动框架
bootstrap.Boot()
// 加载插件
bootstrap.Plugins()
// 启动 HTTP 服务
go func() {
if err := facades.Route().Run(); err != nil {

View File

@@ -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()
}

View File

@@ -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

View File

@@ -0,0 +1,184 @@
<title>插件中心</title>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
按钮点击一次即可,请勿重复点击以免重复操作,任务中心在右上方!
</div>
<div class="layui-card-body">
<table id="panel-plugin" lay-filter="panel-plugin"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="panel-plugin-control">
{{# if(d.installed && d.installed_version != d.version){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
{{# } }}
{{# if(d.installed && d.installed_version == d.version){ }}
<a class="layui-btn layui-btn-xs" lay-event="open">管理</a>
{{# } }}
{{# if(d.installed && d.installed_version == d.version){ }}
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
{{# } }}
{{# if(!d.installed){ }}
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
{{# } }}
</script>
<!-- 首页显示开关 -->
<script type="text/html" id="plugin-show">
<input type="checkbox" name="plugin-show-home" lay-skin="switch" lay-text="ON|OFF"
lay-filter="plugin-show-home"
value="{{ d.show }}" data-plugin-slug="{{ d.slug }}"
{{ d.show==
1 ? 'checked' : '' }}>
</script>
</div>
</div>
</div>
<script>
layui.use(['admin', 'table', 'jquery'], function () {
var $ = layui.$
, form = layui.form
, table = layui.table
, admin = layui.admin
table.render({
elem: '#panel-plugin'
, url: '/api/panel/plugin/list'
, cols: [[
{ field: 'slug', hide: true, title: 'Slug' }
, { field: 'name', width: 150, title: '插件名', sort: true }
, { field: 'description', title: '描述' }
, { field: 'author', width: 100, title: '作者' }
, { field: 'installed_version', width: 140, title: '已装版本' }
, { field: 'version', width: 140, title: '最新版本' }
, { field: 'show', title: '首页显示', width: 90, templet: '#plugin-show', unresize: true }
, {
field: 'control',
width: 180,
title: '操作',
templet: '#panel-plugin-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: '耗子Linux面板数据加载出现异常'
, done: function () {
//element.render('progress');
}
})
// 工具条
table.on('tool(panel-plugin)', function (obj) {
let data = obj.data
if (obj.event === 'open') {
location.hash = '/plugin/' + data.slug
} else if (obj.event === 'install') {
layer.confirm('确定安装该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/install',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
})
})
} else if (obj.event === 'uninstall') {
layer.confirm('确定卸载该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/uninstall',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
})
})
} else if (obj.event === 'update') {
layer.confirm('确定升级该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/update',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('升级:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
})
})
}
})
form.on('switch(plugin-show-home)', function (obj) {
let $ = layui.$
let plugin_slug = $(this).data('plugin-slug')
let show = obj.elem.checked ? 1 : 0
admin.req({
url: '/api/panel/plugin/updateShow',
type: 'POST',
data: {
slug: plugin_slug,
show: show
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', { icon: 1, time: 1000 })
} else {
// 还原开关状态
obj.elem.checked = !obj.elem.checked
form.render('checkbox')
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
}
})
})
/*form.render(null, 'plugin-form');
//搜索
form.on('submit(plugin-search-submit)', function (data) {
var field = data.field;
//执行重载
table.reload('plugin-search-submit', {
where: field
});
});*/
})
</script>

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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 <<EOF
# 该文件为OpenResty主配置文件不建议随意修改
user www www;
worker_processes auto;
error_log /www/wwwlogs/openresty_error.log crit;
pid /www/server/openresty/logs/nginx.pid;
worker_rlimit_nofile 51200;
stream {
log_format tcp_format '\$time_local|\$remote_addr|\$protocol|\$status|\$bytes_sent|\$bytes_received|\$session_time|\$upstream_addr|\$upstream_bytes_sent|\$upstream_bytes_received|\$upstream_connect_time';
access_log /www/wwwlogs/tcp-access.log tcp_format;
error_log /www/wwwlogs/tcp-error.log;
}
events {
use epoll;
worker_connections 51200;
multi_accept on;
}
http {
include mime.types;
include proxy.conf;
default_type application/octet-stream;
server_names_hash_bucket_size 512;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 200m;
client_body_buffer_size 10M;
client_body_in_file_only off;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
tcp_nodelay on;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 64k;
fastcgi_buffers 8 64k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_intercept_errors on;
gzip on;
gzip_min_length 1k;
gzip_buffers 32 4k;
gzip_http_version 1.1;
gzip_comp_level 6;
gzip_types *;
gzip_vary on;
gzip_proxied any;
gzip_disable "MSIE [1-6]\.";
brotli on;
brotli_comp_level 6;
brotli_min_length 10;
brotli_window 1m;
brotli_types *;
brotli_static on;
limit_conn_zone \$binary_remote_addr zone=perip:10m;
limit_conn_zone \$server_name zone=perserver:10m;
server_tokens off;
access_log off;
# 服务状态页
server {
listen 80;
server_name 127.0.0.1;
allow 127.0.0.1;
location /nginx_status {
stub_status on;
access_log off;
}
location ~ ^/phpfpm_status/(?<version>\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 <<EOF
set \$real_script_name \$fastcgi_script_name;
if (\$fastcgi_script_name ~ "^(.+?\.php)(/.+)$") {
set \$real_script_name \$1;
set \$path_info \$2;
}
fastcgi_param SCRIPT_FILENAME \$document_root\$real_script_name;
fastcgi_param SCRIPT_NAME \$real_script_name;
fastcgi_param PATH_INFO \$path_info;
EOF
# 写入默认站点页
cat >${openrestyPath}/html/index.html <<EOF
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>耗子Linux面板</title>
</head>
<body>
<h1>耗子Linux面板</h1>
<p>这是耗子Linux面板的OpenResty默认页面</p>
<p>当您看到此页面,说明该域名尚未与站点绑定。</p>
</body>
</html>
EOF
# 写入站点停止页
cat >${openrestyPath}/html/stop.html <<EOF
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>网站已停止 - 耗子Linux面板</title>
</head>
<body>
<h1>耗子Linux面板</h1>
<p>该网站已被管理员停止访问!</p>
<p>当您看到此页面,说明该网站已被管理员停止对外访问,请联系管理员了解详情。</p>
</body>
</html>
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 <<EOF
proxy_temp_path ${openrestyPath}/proxy_temp_dir;
proxy_cache_path ${openrestyPath}/proxy_cache_dir levels=1:2 keys_zone=cache_one:20m inactive=1d max_size=5g;
proxy_connect_timeout 60;
proxy_read_timeout 60;
proxy_send_timeout 60;
proxy_buffer_size 32k;
proxy_buffers 4 64k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 128k;
proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;
proxy_cache cache_one;
EOF
# 建立日志目录
mkdir -p /www/wwwlogs/waf
chown www:www /www/wwwlogs/waf
chmod 755 /www/wwwlogs/waf
# 写入服务文件
cat >/lib/systemd/system/openresty.service <<EOF
[Unit]
Description=The OpenResty Application Platform
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/www/server/openresty/logs/nginx.pid
ExecStartPre=/www/server/openresty/sbin/nginx -t -c /www/server/openresty/conf/nginx.conf
ExecStart=/www/server/openresty/sbin/nginx -c /www/server/openresty/conf/nginx.conf
ExecReload=/www/server/openresty/sbin/nginx -s reload
ExecStop=/www/server/openresty/sbin/nginx -s quit
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable openresty.service
systemctl start openresty.service

View File

@@ -0,0 +1,19 @@
#!/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="+----------------------------------------------------"