2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

feat: 测试插件

This commit is contained in:
耗子
2024-09-18 02:25:24 +08:00
parent 3193a15131
commit 615744e0cf
11 changed files with 646 additions and 272 deletions

View File

@@ -25,6 +25,7 @@ type WebsiteRepo interface {
UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error
Count() (int64, error)
Get(id uint) (*types.WebsiteSetting, error)
GetByName(name string) (*types.WebsiteSetting, error)
List(page, limit uint) ([]*Website, int64, error)
Create(req *request.WebsiteCreate) (*Website, error)
Update(req *request.WebsiteUpdate) error

View File

@@ -163,6 +163,16 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
return setting, err
}
func (r *websiteRepo) GetByName(name string) (*types.WebsiteSetting, error) {
website := new(biz.Website)
if err := app.Orm.Where("name", name).First(website).Error; err != nil {
return nil, err
}
return r.Get(website.ID)
}
func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
var websites []*biz.Website
var total int64

View File

@@ -0,0 +1,32 @@
package fail2ban
import (
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/pluginloader"
"github.com/TheTNB/panel/pkg/types"
)
func init() {
pluginloader.Register(&types.Plugin{
Slug: "fail2ban",
Name: "Fail2ban",
Description: "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址将该IP地址加入防火墙的拒绝访问列表中",
Version: "1.0.2",
Requires: []string{},
Excludes: []string{},
Install: `bash /www/panel/scripts/fail2ban/install.sh`,
Uninstall: `bash /www/panel/scripts/fail2ban/uninstall.sh`,
Update: `bash /www/panel/scripts/fail2ban/update.sh`,
Route: func(r chi.Router) {
service := NewService()
r.Get("/jails", service.List)
r.Post("/jails", service.Add)
r.Delete("/jails", service.Delete)
r.Get("/jails/{name}", service.BanList)
r.Post("/unban", service.Unban)
r.Post("/whiteList", service.SetWhiteList)
r.Get("/whiteList", service.GetWhiteList)
},
})
}

View File

@@ -0,0 +1,29 @@
package fail2ban
type Add struct {
Name string `json:"name"`
Type string `json:"type"`
MaxRetry string `json:"maxretry"`
FindTime string `json:"findtime"`
BanTime string `json:"bantime"`
WebsiteName string `json:"website_name"`
WebsiteMode string `json:"website_mode"`
WebsitePath string `json:"website_path"`
}
type Delete struct {
Name string `json:"name"`
}
type BanList struct {
Name string `json:"name"`
}
type Unban struct {
Name string `json:"name"`
IP string `json:"ip"`
}
type SetWhiteList struct {
IP string `json:"ip"`
}

View File

@@ -0,0 +1,361 @@
package fail2ban
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/go-rat/chix"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/types"
)
type Service struct {
websiteRepo biz.WebsiteRepo
}
func NewService() *Service {
return &Service{
websiteRepo: data.NewWebsiteRepo(),
}
}
// List 所有 Fail2ban 规则
func (s *Service) List(w http.ResponseWriter, r *http.Request) {
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
if len(jailList) == 0 {
service.Error(w, http.StatusUnprocessableEntity, "Fail2ban 规则为空")
return
}
var jails []types.Fail2banJail
for i, jail := range jailList {
if i == 0 {
continue
}
jailName := jail[1]
jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
if len(jailRaw) == 0 {
continue
}
jailEnabled := strings.Contains(jailRaw, "enabled = true")
jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw)
jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw)
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
jails = append(jails, types.Fail2banJail{
Name: jailName,
Enabled: jailEnabled,
LogPath: jailLogPath[1],
MaxRetry: cast.ToInt(jailMaxRetry[1]),
FindTime: cast.ToInt(jailFindTime[1]),
BanTime: cast.ToInt(jailBanTime[1]),
})
}
paged, total := service.Paginate(r, jails)
service.Success(w, chix.M{
"total": total,
"items": paged,
})
}
// Add 添加 Fail2ban 规则
func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Add](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
jailName := req.Name
jailType := req.Type
jailMaxRetry := req.MaxRetry
jailFindTime := req.FindTime
jailBanTime := req.BanTime
jailWebsiteName := req.WebsiteName
jailWebsiteMode := req.WebsiteMode
jailWebsitePath := req.WebsitePath
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") {
service.Error(w, http.StatusUnprocessableEntity, "规则已存在")
return
}
switch jailType {
case "website":
website, err := s.websiteRepo.GetByName(jailWebsiteName)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, fmt.Sprintf("获取网站配置失败:%s", err))
return
}
var ports string
for _, port := range website.Ports {
fields := strings.Fields(cast.ToString(port))
ports += fields[0] + ","
}
rule := `
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
[` + jailWebsiteName + `-` + jailWebsiteMode + `]
enabled = true
filter = haozi-` + jailWebsiteName + `-` + jailWebsiteMode + `
port = ` + ports + `
maxretry = ` + jailMaxRetry + `
findtime = ` + jailFindTime + `
bantime = ` + jailBanTime + `
action = %(action_mwl)s
logpath = /www/wwwlogs/` + website.Name + `.log
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END
`
raw += rule
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
return
}
var filter string
if jailWebsiteMode == "cc" {
filter = `
[Definition]
failregex = ^<HOST>\s-.*HTTP/.*$
ignoreregex =
`
} else {
filter = `
[Definition]
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
ignoreregex =
`
}
if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
return
}
case "service":
var logPath string
var filter string
var port string
var err error
switch jailName {
case "ssh":
if os.IsDebian() || os.IsUbuntu() {
logPath = "/var/log/auth.log"
} else {
logPath = "/var/log/secure"
}
filter = "sshd"
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
case "mysql":
logPath = "/www/server/mysql/mysql-error.log"
filter = "mysqld-auth"
port, err = shell.Execf("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'")
case "pure-ftpd":
logPath = "/var/log/messages"
filter = "pure-ftpd"
port, err = shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
default:
service.Error(w, http.StatusUnprocessableEntity, "未知服务")
return
}
if len(port) == 0 || err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
return
}
rule := `
# ` + jailName + `-START
[` + jailName + `]
enabled = true
filter = ` + filter + `
port = ` + port + `
maxretry = ` + jailMaxRetry + `
findtime = ` + jailFindTime + `
bantime = ` + jailBanTime + `
action = %(action_mwl)s
logpath = ` + logPath + `
# ` + jailName + `-END
`
raw += rule
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
return
}
}
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载配置失败")
return
}
service.Success(w, nil)
}
// Delete 删除规则
func (s *Service) Delete(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Delete](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
if !strings.Contains(raw, "["+req.Name+"]") {
service.Error(w, http.StatusUnprocessableEntity, "规则不存在")
return
}
rule := str.Cut(raw, "# "+req.Name+"-START", "# "+req.Name+"-END")
raw = strings.Replace(raw, "\n# "+req.Name+"-START"+rule+"# "+req.Name+"-END", "", -1)
raw = strings.TrimSpace(raw)
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
return
}
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载配置失败")
return
}
service.Success(w, nil)
}
// BanList 获取封禁列表
func (s *Service) BanList(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[BanList](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, req.Name)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取封禁列表失败")
return
}
totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, req.Name)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取封禁列表失败")
return
}
bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | awk -F ":" '{print $2}'`, req.Name)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取封禁列表失败")
return
}
bannedIpList := strings.Split(bannedIp, " ")
var list []map[string]string
for _, ip := range bannedIpList {
if len(ip) > 0 {
list = append(list, map[string]string{
"name": req.Name,
"ip": ip,
})
}
}
if list == nil {
list = []map[string]string{}
}
service.Success(w, chix.M{
"currently_ban": currentlyBan,
"total_ban": totalBan,
"baned_list": list,
})
}
// Unban 解封
func (s *Service) Unban(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Unban](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
if _, err = shell.Execf("fail2ban-client set %s unbanip %s", req.Name, req.IP); err != nil {
service.Error(w, http.StatusInternalServerError, "解封失败")
return
}
service.Success(w, nil)
}
// SetWhiteList 设置白名单
func (s *Service) SetWhiteList(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[SetWhiteList](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
// 正则替换
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
if reg.MatchString(raw) {
raw = reg.ReplaceAllString(raw, "ignoreip = "+req.IP+"\n")
} else {
service.Error(w, http.StatusInternalServerError, "解析Fail2ban规则失败Fail2ban可能已损坏")
return
}
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
return
}
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载配置失败")
return
}
service.Success(w, nil)
}
// GetWhiteList 获取白名单
func (s *Service) GetWhiteList(w http.ResponseWriter, r *http.Request) {
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, err.Error())
return
}
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
if reg.MatchString(raw) {
ignoreIp := reg.FindStringSubmatch(raw)[1]
service.Success(w, ignoreIp)
} else {
service.Error(w, http.StatusInternalServerError, "解析Fail2ban规则失败Fail2ban可能已损坏")
return
}
}

View File

@@ -3,6 +3,7 @@ package plugin
import (
"github.com/go-chi/chi/v5"
_ "github.com/TheTNB/panel/internal/plugin/fail2ban"
_ "github.com/TheTNB/panel/internal/plugin/openresty"
"github.com/TheTNB/panel/pkg/pluginloader"
)

View File

@@ -1,8 +1,6 @@
package openresty
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/pluginloader"
@@ -11,12 +9,23 @@ import (
func init() {
pluginloader.Register(&types.Plugin{
Slug: "openresty",
Name: "OpenResty",
Order: -100,
Slug: "openresty",
Name: "OpenResty",
Description: "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台",
Version: "1.25.3.1",
Requires: []string{},
Excludes: []string{},
Install: "bash /www/panel/scripts/openresty/install.sh",
Uninstall: "bash /www/panel/scripts/openresty/uninstall.sh",
Update: "bash /www/panel/scripts/openresty/install.sh",
Route: func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Hello, World!"))
})
service := NewService()
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.SaveConfig)
r.Get("/errorLog", service.ErrorLog)
r.Post("/clearErrorLog", service.ClearErrorLog)
},
})
}

View File

@@ -1 +1,188 @@
package openresty
import (
"fmt"
"net/http"
"regexp"
"time"
"github.com/go-resty/resty/v2"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
type Service struct {
// Dependent services
}
func NewService() *Service {
return &Service{}
}
// GetConfig
//
// @Summary 获取配置
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} h.SuccessResponse
// @Router /plugins/openresty/config [get]
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
config, err := io.Read("/www/server/openresty/conf/nginx.conf")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取配置失败")
return
}
service.Success(w, config)
}
// SaveConfig
//
// @Summary 保存配置
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Param config body string true "配置"
// @Success 200 {object} h.SuccessResponse
// @Router /plugins/openresty/config [post]
func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) {
config := r.FormValue("config")
if len(config) == 0 {
service.Error(w, http.StatusInternalServerError, "配置不能为空")
}
if err := io.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "保存配置失败")
}
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
service.Error(w, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err))
}
service.Success(w, nil)
}
// ErrorLog
//
// @Summary 获取错误日志
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} h.SuccessResponse
// @Router /plugins/openresty/errorLog [get]
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
if !io.Exists("/www/wwwlogs/nginx_error.log") {
service.Success(w, "")
}
out, err := shell.Execf("tail -n 100 /www/wwwlogs/openresty_error.log")
if err != nil {
service.Error(w, http.StatusInternalServerError, out)
}
service.Success(w, out)
}
// ClearErrorLog
//
// @Summary 清空错误日志
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} h.SuccessResponse
// @Router /plugins/openresty/clearErrorLog [post]
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
if out, err := shell.Execf("echo '' > /www/wwwlogs/openresty_error.log"); err != nil {
service.Error(w, http.StatusInternalServerError, out)
}
service.Success(w, nil)
}
// Load
//
// @Summary 获取负载状态
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} h.SuccessResponse
// @Router /plugins/openresty/load [get]
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
client := resty.New().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
if err != nil || !resp.IsSuccess() {
service.Success(w, []types.NV{})
}
raw := resp.String()
var data []types.NV
workers, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | wc -l")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取负载失败")
}
data = append(data, types.NV{
Name: "工作进程",
Value: workers,
})
out, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取负载失败")
}
mem := str.FormatBytes(cast.ToFloat64(out))
data = append(data, types.NV{
Name: "内存占用",
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data = append(data, types.NV{
Name: "活跃连接数",
Value: match[1],
})
}
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data = append(data, types.NV{
Name: "总连接次数",
Value: match[1],
})
data = append(data, types.NV{
Name: "总握手次数",
Value: match[2],
})
data = append(data, types.NV{
Name: "总请求次数",
Value: match[3],
})
}
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data = append(data, types.NV{
Name: "请求数",
Value: match[1],
})
data = append(data, types.NV{
Name: "响应数",
Value: match[2],
})
data = append(data, types.NV{
Name: "驻留进程",
Value: match[3],
})
}
service.Success(w, data)
}