Compare commits

...

25 Commits

Author SHA1 Message Date
耗子 aa080dab15 feat: 发布v2.2.17 2024-06-25 03:37:37 +08:00
耗子 b4e5498b48 feat: OpenResty支持返回具体错误 2024-06-25 03:35:35 +08:00
耗子 63838bffa1 feat: phpMyAdmin支持编辑配置 2024-06-25 03:07:38 +08:00
耗子 fa41b18ced fix: 优化查询 2024-06-25 02:51:34 +08:00
耗子 31bd15217b feat: 添加日志记录中间件 2024-06-25 02:38:13 +08:00
耗子 e6b6f06706 feat: 尝试优化内存 2024-06-25 02:08:13 +08:00
耗子 1b836297bf feat: 尝试优化内存 2024-06-25 01:52:41 +08:00
耗子 96bea2cf90 fix: 去掉无用的检查 2024-06-25 01:23:01 +08:00
耗子 8aff513284 fix: 禁止80端口使用HTTPS 2024-06-25 01:13:24 +08:00
耗子 368e41a512 feat: 发布v2.2.16 2024-06-25 01:01:07 +08:00
耗子 924fdee7c5 feat: 支持设置HTTPS端口 2024-06-25 01:00:08 +08:00
耗子 438ba1db1e fix: lint 2024-06-24 23:50:53 +08:00
耗子 d4e52a2d87 feat: ZeroSSL支持自动获取EAB 2024-06-24 23:46:13 +08:00
耗子 4432a12c7a feat: 临时性手段阻止任务重复添加 2024-06-24 23:28:35 +08:00
耗子 a75e36e447 feat: 优化任务 2024-06-24 23:10:39 +08:00
耗子 05674ee04e feat: 优化迁移 2024-06-24 22:53:43 +08:00
耗子 09703761eb fix(文件): 权限设置错误 2024-06-24 22:20:41 +08:00
耗子 dfa14c3fd4 refactor: MySQL改密逻辑 2024-06-24 21:52:46 +08:00
耗子 7b80f034ea feat: 安装前先清理文件 2024-06-24 19:46:43 +08:00
耗子 71923093fd feat(文件): 解压不再判断路径存在 2024-06-24 19:11:01 +08:00
耗子 aae3cf0518 fix: 统一格式 2024-06-24 03:46:21 +08:00
耗子 ed22e79552 feat: 防止密码爆破攻击 2024-06-24 03:45:38 +08:00
耗子 15dd5bb718 feat: 发布v2.2.15 2024-06-23 05:09:30 +08:00
耗子 0e7af2914e feat: 发布v2.2.14 2024-06-23 04:52:18 +08:00
耗子 7ec378829b feat: 优化https证书签发 2024-06-23 04:44:14 +08:00
53 changed files with 786 additions and 322 deletions
+2 -2
View File
@@ -46,8 +46,8 @@ func (receiver *Monitoring) Handle(console.Context) error {
// 将等待中的任务分发
// TODO 有bug,需要设计一个锁机制防止重复分发
//task := services.NewTaskImpl()
//_ = task.DispatchWaiting()
task := services.NewTaskImpl()
_ = task.DispatchWaiting()
setting := services.NewSettingImpl()
monitor := setting.Get(models.SettingKeyMonitor)
+1 -1
View File
@@ -614,7 +614,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
_, err = website.Add(types.Website{
_, err = website.Add(types.WebsiteAdd{
Name: name,
Status: true,
Domains: domains,
+6
View File
@@ -2,6 +2,8 @@ package commands
import (
"context"
"runtime"
"runtime/debug"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
@@ -68,6 +70,10 @@ func (receiver *PanelTask) Handle(console.Context) error {
return err
}
// 回收内存
runtime.GC()
debug.FreeOSMemory()
types.Status = types.StatusNormal
return nil
}
+1 -1
View File
@@ -29,7 +29,7 @@ func Success(ctx http.Context, data any) http.Response {
// Error 响应错误
func Error(ctx http.Context, code int, message string) http.Response {
return ctx.Response().Json(code, &ErrorResponse{
Message: facades.Lang(ctx).Get("messages.mistake") + ": " + message,
Message: message,
})
}
+11 -4
View File
@@ -5,6 +5,7 @@ import (
stdio "io"
stdos "os"
"path/filepath"
"strconv"
"strings"
"syscall"
@@ -354,10 +355,16 @@ func (r *FileController) Permission(ctx http.Context) http.Response {
return sanitize
}
if err := io.Chmod(request.Path, stdos.FileMode(request.Mode)); err != nil {
// 解析成8进制
mode, err := strconv.ParseUint(request.Mode, 8, 64)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := io.Chown(request.Path, request.Owner, request.Group); err != nil {
if err = io.Chmod(request.Path, stdos.FileMode(mode)); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = io.Chown(request.Path, request.Owner, request.Group); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -505,7 +512,7 @@ func (r *FileController) List(ctx http.Context) http.Response {
}
// setPermission
func (r *FileController) setPermission(path string, mode uint, owner, group string) {
_ = io.Chmod(path, stdos.FileMode(mode))
func (r *FileController) setPermission(path string, mode stdos.FileMode, owner, group string) {
_ = io.Chmod(path, mode)
_ = io.Chown(path, owner, group)
}
+85 -137
View File
@@ -1,7 +1,6 @@
package plugins
import (
"database/sql"
"fmt"
"regexp"
@@ -12,6 +11,7 @@ import (
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
@@ -71,7 +71,7 @@ func (r *MySQLController) Load(ctx http.Context) http.Response {
return controllers.Success(ctx, []types.NV{})
}
raw, err := shell.Execf("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
}
@@ -175,80 +175,45 @@ func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response {
// SetRootPassword 设置root密码
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
status, err := systemctl.Status("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
rootPassword := ctx.Request().Input("password")
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
}
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
if _, err = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';"`, oldRootPassword, rootPassword)); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置root密码失败: %v", err))
mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix")
if err != nil {
// 尝试安全模式直接改密
if err = db.MySQLResetRootPassword(rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if _, err = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "FLUSH PRIVILEGES;"`, rootPassword)); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
_, _ = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';"`, rootPassword, oldRootPassword))
_, _ = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "FLUSH PRIVILEGES;"`, oldRootPassword))
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
} else {
if err = mysql.UserPassword("root", rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
}
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
defer db.Close()
if err = db.Ping(); err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
"items": []types.MySQLDatabase{},
})
}
rows, err := db.Query("SHOW DATABASES")
databases, err := mysql.Databases()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
if err = rows.Scan(&d.Name); err != nil {
continue
}
databases = append(databases, d)
}
if err = rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
}
paged, total := controllers.Paginate(ctx, databases)
return controllers.Success(ctx, http.Json{
@@ -272,17 +237,18 @@ func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.DatabaseCreate(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.UserCreate(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -298,8 +264,12 @@ func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.DatabaseDrop(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -394,71 +364,20 @@ func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
// UserList 用户列表
func (r *MySQLController) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
defer db.Close()
if err = db.Ping(); err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
"items": []types.MySQLUser{},
})
}
rows, err := db.Query("SELECT user, host FROM mysql.user")
users, err := mysql.Users()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
if err = rows.Scan(&u.User, &u.Host); err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
if err = grantsRows.Scan(&grant); err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err = grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err = rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
}
paged, total := controllers.Paginate(ctx, userGrants)
paged, total := controllers.Paginate(ctx, users)
return controllers.Success(ctx, http.Json{
"total": total,
@@ -480,14 +399,15 @@ func (r *MySQLController) AddUser(ctx http.Context) http.Response {
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.UserCreate(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -503,8 +423,12 @@ func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.UserDrop(user); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -522,11 +446,12 @@ func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.UserPassword(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -544,15 +469,38 @@ func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// getSock 获取sock文件位置
func (r *MySQLController) getSock() string {
if io.Exists("/tmp/mysql.sock") {
return "/tmp/mysql.sock"
}
if io.Exists("/www/server/mysql/config/my.cnf") {
config, _ := io.Read("/www/server/mysql/config/my.cnf")
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 2 {
return matches[2]
}
}
if io.Exists("/etc/my.cnf") {
config, _ := io.Read("/etc/my.cnf")
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 2 {
return matches[2]
}
}
return "/tmp/mysql.sock"
}
@@ -1,6 +1,7 @@
package plugins
import (
"fmt"
"regexp"
"time"
@@ -61,7 +62,8 @@ func (r *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
}
if err := systemctl.Reload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err))
}
return controllers.Success(ctx, nil)
@@ -1,6 +1,7 @@
package plugins
import (
"fmt"
"regexp"
"strings"
@@ -87,8 +88,39 @@ func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
}
}
if err := systemctl.Reload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return controllers.Success(ctx, nil)
}
func (r *PhpMyAdminController) GetConfig(ctx http.Context) http.Response {
config, err := io.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PhpMyAdminController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
}
if err := io.Write("/www/server/vhost/phpmyadmin.conf", config, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{
"error": err.Error(),
}).Info("修改 phpMyAdmin 配置失败")
return controllers.ErrorSystem(ctx)
}
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return controllers.Success(ctx, nil)
+16 -7
View File
@@ -14,6 +14,7 @@ import (
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"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"
@@ -84,7 +85,7 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name
}
website := types.Website{
website := types.WebsiteAdd{
Name: addRequest.Name,
Status: true,
Domains: addRequest.Domains,
@@ -202,7 +203,7 @@ func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse{data=types.Website}
// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd}
// @Router /panel/websites/{id}/config [get]
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
var idRequest requests.ID
@@ -549,9 +550,12 @@ server
# waf标记位结束
# 错误页配置,可自行设置
#error_page 404 /404.html;
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置,不可修改
include /www/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/%s.conf;
@@ -572,15 +576,19 @@ server
error_log /www/wwwlogs/%s.log;
}
`, website.Path, website.Php, website.Name, website.Name, website.Name)
`, website.Path, website.Php, website.Name, website.Name, website.Name, website.Name)
if err := io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return nil
}
if err := io.Write("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644); err != nil {
if err := io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil {
return nil
}
if err := io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil {
return nil
}
if err := systemctl.Reload("openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
_, err = shell.Execf("openresty -t")
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return Success(ctx, nil)
@@ -646,7 +654,8 @@ func (r *WebsiteController) Status(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
if err = systemctl.Reload("openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
_, err = shell.Execf("openresty -t")
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return Success(ctx, nil)
+1
View File
@@ -13,6 +13,7 @@ type Kernel struct {
// These middleware are run during every request to your application.
func (kernel Kernel) Middleware() []http.Middleware {
return []http.Middleware{
middleware.Log(),
middleware.Status(),
}
}
+20
View File
@@ -0,0 +1,20 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// Log 记录请求日志
func Log() http.Middleware {
return func(ctx http.Context) {
facades.Log().Channel("http").With(map[string]any{
"Method": ctx.Request().Method(),
"URL": ctx.Request().FullUrl(),
"IP": ctx.Request().Ip(),
"UA": ctx.Request().Header("User-Agent"),
"Body": ctx.Request().All(),
}).Info("HTTP Request")
ctx.Request().Next()
}
}
+2 -2
View File
@@ -21,8 +21,8 @@ func (r *UserStore) Rules(ctx http.Context) map[string]string {
return map[string]string{
"ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass",
"email": "required|email",
"kid": "required_unless:ca,letsencrypt,buypass",
"hmac_encoded": "required_unless:ca,letsencrypt,buypass",
"kid": "required_if:ca,sslcom,google",
"hmac_encoded": "required_if:ca,sslcom,google",
"key_type": "required|in:P256,P384,2048,4096",
}
}
+2 -2
View File
@@ -23,8 +23,8 @@ func (r *UserUpdate) Rules(ctx http.Context) map[string]string {
"id": "required|uint|min:1|exists:cert_users,id",
"ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass",
"email": "required|email",
"kid": "required_unless:ca,letsencrypt,buypass",
"hmac_encoded": "required_unless:ca,letsencrypt,buypass",
"kid": "required_if:ca,sslcom,google",
"hmac_encoded": "required_if:ca,sslcom,google",
"key_type": "required|in:P256,P384,2048,4096",
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
type Permission struct {
Path string `form:"path" json:"path"`
Mode uint `form:"mode" json:"mode" filter:"uint"`
Mode string `form:"mode" json:"mode"`
Owner string `form:"owner" json:"owner"`
Group string `form:"group" json:"group"`
}
@@ -19,7 +19,7 @@ func (r *Permission) Authorize(ctx http.Context) error {
func (r *Permission) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_exists`,
"mode": "regex:^[0-7]{3}$|uint",
"mode": "regex:^0[0-7]{3}$",
"owner": "regex:^[a-zA-Z0-9_-]+$",
"group": "regex:^[a-zA-Z0-9_-]+$",
}
+1 -1
View File
@@ -17,7 +17,7 @@ func (r *UnArchive) Authorize(ctx http.Context) error {
func (r *UnArchive) Rules(ctx http.Context) map[string]string {
return map[string]string{
"file": `regex:^/.*$|path_exists`,
"path": `regex:^/.*$|path_not_exists`,
"path": `regex:^/.*$`,
}
}
+2
View File
@@ -9,6 +9,7 @@ type SaveConfig struct {
ID uint `form:"id" json:"id" filter:"uint"`
Domains []string `form:"domains" json:"domains"`
Ports []uint `form:"ports" json:"ports"`
TLSPorts []uint `form:"tls_ports" json:"tls_ports"`
Hsts bool `form:"hsts" json:"hsts"`
Ssl bool `form:"ssl" json:"ssl"`
HttpRedirect bool `form:"http_redirect" json:"http_redirect"`
@@ -36,6 +37,7 @@ func (r *SaveConfig) Rules(ctx http.Context) map[string]string {
"id": "required|exists:websites,id",
"domains": "required|slice",
"ports": "required|slice",
"tls_ports": "required_if:ssl,true|slice|not_in:80",
"hsts": "bool",
"ssl": "bool",
"http_redirect": "bool",
+5 -26
View File
@@ -27,20 +27,11 @@ func (receiver *ProcessTask) Handle(args ...any) error {
}
var task models.Task
if err := facades.Orm().Query().Where("id = ?", taskID).Get(&task); err != nil {
_ = facades.Orm().Query().Where("id = ?", taskID).Get(&task)
if task.ID == 0 {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("获取任务失败")
return nil
}
task.Status = models.TaskStatusRunning
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务失败")
}).Infof("任务不存在")
return nil
}
@@ -50,13 +41,7 @@ func (receiver *ProcessTask) Handle(args ...any) error {
if _, err := shell.Execf(task.Shell); err != nil {
task.Status = models.TaskStatusFailed
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务失败")
return nil
}
_ = facades.Orm().Query().Save(&task)
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
@@ -65,13 +50,7 @@ func (receiver *ProcessTask) Handle(args ...any) error {
}
task.Status = models.TaskStatusSuccess
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务失败")
return nil
}
_ = facades.Orm().Query().Save(&task)
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
+2 -18
View File
@@ -1,14 +1,12 @@
package providers
import (
"fmt"
"github.com/goravel/framework/contracts/database/seeder"
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/database/gorm"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/pkg/migrate"
)
type DatabaseServiceProvider struct {
@@ -20,19 +18,5 @@ func (receiver *DatabaseServiceProvider) Register(app foundation.Application) {
func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) {
facades.Seeder().Register([]seeder.Seeder{})
if err := facades.Orm().Query().(*gorm.QueryImpl).Instance().AutoMigrate(
&models.Cert{},
&models.CertDNS{},
&models.CertUser{},
&models.Cron{},
&models.Database{},
&models.Monitor{},
&models.Plugin{},
&models.Setting{},
&models.Task{},
&models.User{},
&models.Website{},
); err != nil {
panic(fmt.Sprintf("Failed to migrate database: %v", err))
}
migrate.Migrate(facades.Orm().Query().(*gorm.QueryImpl).Instance())
}
+21 -1
View File
@@ -2,7 +2,9 @@ package providers
import (
"github.com/goravel/framework/contracts/foundation"
contractshttp "github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/http/limit"
"github.com/TheTNB/panel/app/http"
"github.com/TheTNB/panel/routes"
@@ -25,5 +27,23 @@ func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
}
func (receiver *RouteServiceProvider) configureRateLimiting() {
facades.RateLimiter().ForWithLimits("login", func(ctx contractshttp.Context) []contractshttp.Limit {
return []contractshttp.Limit{
limit.PerMinute(5).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{
"message": "请求过于频繁,请等待一分钟后再试",
})
}),
limit.PerHour(100).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{
"message": "请求过于频繁,请等待一小时后再试",
})
}),
limit.PerDay(1000).Response(func(ctx contractshttp.Context) {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{
"message": "面板遭受登录爆破攻击过多,已暂时屏蔽登录,请立刻更换面板端口",
})
}),
}
})
}
+5
View File
@@ -1,6 +1,8 @@
package bootstrap
import (
"runtime/debug"
"github.com/gookit/validate/locales/zhcn"
"github.com/goravel/framework/foundation"
@@ -8,6 +10,9 @@ import (
)
func Boot() {
debug.SetGCPercent(10)
debug.SetMemoryLimit(512 << 20)
zhcn.RegisterGlobal()
app := foundation.NewApplication()
+2 -2
View File
@@ -14,10 +14,10 @@ func init() {
// in web browsers. You are free to adjust these settings as needed.
//
// To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
"paths": []string{"*"},
"paths": []string{"1145141919810"}, // 避免框架使用CORS中间件
"allowed_methods": []string{"*"},
"allowed_origins": []string{"*"},
"allowed_headers": []string{"*"},
"allowed_headers": []string{""},
"exposed_headers": []string{""},
"max_age": 0,
"supports_credentials": false,
+7
View File
@@ -37,6 +37,13 @@ func init() {
"days": 7,
"print": true,
},
"http": map[string]any{
"driver": "daily",
"path": "storage/logs/http.log",
"level": "info",
"days": 7,
"print": false,
},
},
})
}
+1 -1
View File
@@ -8,7 +8,7 @@ func init() {
config := facades.Config()
config.Add("panel", map[string]any{
"name": "耗子面板",
"version": "v2.2.13",
"version": "v2.2.17",
"ssl": config.Env("APP_SSL", false),
})
}
+9 -3
View File
@@ -3648,7 +3648,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/types.Website"
"$ref": "#/definitions/types.WebsiteAdd"
}
}
}
@@ -5638,7 +5638,7 @@ const docTemplate = `{
"type": "string"
},
"mode": {
"type": "integer"
"type": "string"
},
"owner": {
"type": "string"
@@ -5721,6 +5721,12 @@ const docTemplate = `{
"ssl_certificate_key": {
"type": "string"
},
"tls_ports": {
"type": "array",
"items": {
"type": "integer"
}
},
"waf": {
"type": "boolean"
},
@@ -5915,7 +5921,7 @@ const docTemplate = `{
}
}
},
"types.Website": {
"types.WebsiteAdd": {
"type": "object",
"properties": {
"db": {
+9 -3
View File
@@ -3641,7 +3641,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/types.Website"
"$ref": "#/definitions/types.WebsiteAdd"
}
}
}
@@ -5631,7 +5631,7 @@
"type": "string"
},
"mode": {
"type": "integer"
"type": "string"
},
"owner": {
"type": "string"
@@ -5714,6 +5714,12 @@
"ssl_certificate_key": {
"type": "string"
},
"tls_ports": {
"type": "array",
"items": {
"type": "integer"
}
},
"waf": {
"type": "boolean"
},
@@ -5908,7 +5914,7 @@
}
}
},
"types.Website": {
"types.WebsiteAdd": {
"type": "object",
"properties": {
"db": {
+7 -3
View File
@@ -462,7 +462,7 @@ definitions:
group:
type: string
mode:
type: integer
type: string
owner:
type: string
path:
@@ -516,6 +516,10 @@ definitions:
type: string
ssl_certificate_key:
type: string
tls_ports:
items:
type: integer
type: array
waf:
type: boolean
waf_cache:
@@ -641,7 +645,7 @@ definitions:
value:
type: string
type: object
types.Website:
types.WebsiteAdd:
properties:
db:
type: boolean
@@ -2878,7 +2882,7 @@ paths:
- $ref: '#/definitions/controllers.SuccessResponse'
- properties:
data:
$ref: '#/definitions/types.Website'
$ref: '#/definitions/types.WebsiteAdd'
type: object
security:
- BearerToken: []
+3 -2
View File
@@ -5,7 +5,9 @@ go 1.22
require (
github.com/docker/docker v26.1.4+incompatible
github.com/docker/go-connections v0.5.0
github.com/go-gormigrate/gormigrate/v2 v2.1.2
github.com/go-resty/resty/v2 v2.13.1
github.com/go-sql-driver/mysql v1.8.1
github.com/gookit/validate v1.5.2
github.com/goravel/framework v1.14.1-0.20240618022250-731b8d9930a3
github.com/goravel/gin v1.2.1
@@ -26,6 +28,7 @@ require (
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.24.0
golang.org/x/net v0.26.0
gorm.io/gorm v1.25.10
)
require (
@@ -87,7 +90,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
@@ -203,7 +205,6 @@ require (
gorm.io/driver/mysql v1.5.7 // indirect
gorm.io/driver/postgres v1.5.9 // indirect
gorm.io/driver/sqlserver v1.5.3 // indirect
gorm.io/gorm v1.25.10 // indirect
gorm.io/plugin/dbresolver v1.5.2 // indirect
gotest.tools/v3 v3.5.0 // indirect
modernc.org/libc v1.37.6 // indirect
+2
View File
@@ -170,6 +170,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY=
github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+46 -4
View File
@@ -4,9 +4,11 @@ package services
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/goravel/framework/facades"
requests "github.com/TheTNB/panel/app/http/requests/cert"
@@ -14,6 +16,7 @@ import (
"github.com/TheTNB/panel/pkg/acme"
"github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
@@ -42,7 +45,11 @@ func (s *CertImpl) UserStore(request requests.UserStore) error {
case "buypass":
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CABuypass, nil, acme.KeyType(user.KeyType))
case "zerossl":
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType))
eab, eabErr := s.getZeroSSLEAB(user.Email)
if eabErr != nil {
return eabErr
}
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, eab, acme.KeyType(user.KeyType))
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CASSLcom, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType))
case "google":
@@ -85,7 +92,11 @@ func (s *CertImpl) UserUpdate(request requests.UserUpdate) error {
case "buypass":
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CABuypass, nil, acme.KeyType(user.KeyType))
case "zerossl":
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType))
eab, eabErr := s.getZeroSSLEAB(user.Email)
if eabErr != nil {
return eabErr
}
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, eab, acme.KeyType(user.KeyType))
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CASSLcom, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType))
case "google":
@@ -107,6 +118,31 @@ func (s *CertImpl) UserUpdate(request requests.UserUpdate) error {
return facades.Orm().Query().Save(&user)
}
// getZeroSSLEAB 获取 ZeroSSL EAB
func (s *CertImpl) getZeroSSLEAB(email string) (*acme.EAB, error) {
type data struct {
Success bool `json:"success"`
EabKid string `json:"eab_kid"`
EabHmacKey string `json:"eab_hmac_key"`
}
client := resty.New()
client.SetTimeout(5 * time.Second)
client.SetRetryCount(2)
resp, err := client.R().SetFormData(map[string]string{
"email": email,
}).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email")
if err != nil || !resp.IsSuccess() {
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
}
eab := resp.Result().(*data)
if !eab.Success {
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
}
return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil
}
// UserShow 根据 ID 获取用户
func (s *CertImpl) UserShow(ID uint) (models.CertUser, error) {
var user models.CertUser
@@ -255,7 +291,8 @@ func (s *CertImpl) ObtainAuto(ID uint) (acme.Certificate, error) {
return acme.Certificate{}, errors.New("通配符域名无法使用 HTTP 验证")
}
}
client.UseHTTP(cert.Website.Path)
conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
}
}
@@ -280,6 +317,7 @@ func (s *CertImpl) ObtainAuto(ID uint) (acme.Certificate, error) {
return acme.Certificate{}, err
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return acme.Certificate{}, err
}
}
@@ -320,6 +358,7 @@ func (s *CertImpl) ObtainManual(ID uint) (acme.Certificate, error) {
return acme.Certificate{}, err
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return acme.Certificate{}, err
}
}
@@ -380,7 +419,8 @@ func (s *CertImpl) Renew(ID uint) (acme.Certificate, error) {
return acme.Certificate{}, errors.New("通配符域名无法使用 HTTP 验证")
}
}
client.UseHTTP(cert.Website.Path)
conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
}
}
@@ -405,6 +445,7 @@ func (s *CertImpl) Renew(ID uint) (acme.Certificate, error) {
return acme.Certificate{}, err
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return acme.Certificate{}, err
}
}
@@ -437,6 +478,7 @@ func (s *CertImpl) Deploy(ID, WebsiteID uint) error {
return err
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return err
}
+27
View File
@@ -113,6 +113,10 @@ func (r *PluginImpl) Install(slug string) error {
}
}
if err = r.checkTaskExists(slug); err != nil {
return err
}
var task models.Task
task.Name = "安装插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
@@ -159,6 +163,10 @@ func (r *PluginImpl) Uninstall(slug string) error {
}
}
if err = r.checkTaskExists(slug); err != nil {
return err
}
var task models.Task
task.Name = "卸载插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
@@ -205,6 +213,10 @@ func (r *PluginImpl) Update(slug string) error {
}
}
if err = r.checkTaskExists(slug); err != nil {
return err
}
var task models.Task
task.Name = "更新插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
@@ -217,3 +229,18 @@ func (r *PluginImpl) Update(slug string) error {
_ = io.Remove(task.Log)
return r.task.Process(task.ID)
}
func (r *PluginImpl) checkTaskExists(slug string) error {
var count int64
if err := facades.Orm().Query().
Model(&models.Task{}).
Where("log LIKE ? AND (status = ? OR status = ?)", "%"+slug+"%", models.TaskStatusWaiting, models.TaskStatusRunning).
Count(&count); err != nil {
return errors.New("查询任务失败")
}
if count > 0 {
return errors.New("任务已添加,请勿重复添加")
}
return nil
}
+24
View File
@@ -1,12 +1,17 @@
package services
import (
"sync"
"github.com/goravel/framework/database/orm"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/jobs"
"github.com/TheTNB/panel/app/models"
)
var taskMap sync.Map
type TaskImpl struct {
}
@@ -15,6 +20,9 @@ func NewTaskImpl() *TaskImpl {
}
func (r *TaskImpl) Process(taskID uint) error {
if err := r.markAsRunning(taskID); err != nil {
return err
}
return facades.Queue().Job(&jobs.ProcessTask{}, []any{taskID}).Dispatch()
}
@@ -25,6 +33,9 @@ func (r *TaskImpl) DispatchWaiting() error {
}
for _, task := range tasks {
if _, ok := taskMap.Load(task.ID); ok {
continue
}
if err := r.Process(task.ID); err != nil {
return err
}
@@ -32,3 +43,16 @@ func (r *TaskImpl) DispatchWaiting() error {
return nil
}
func (r *TaskImpl) markAsRunning(taskID uint) error {
task := models.Task{
Model: orm.Model{ID: taskID},
Status: models.TaskStatusRunning,
}
if _, err := facades.Orm().Query().Where("id", taskID).Update(&task); err != nil {
return err
}
taskMap.Store(taskID, true)
return nil
}
+66 -56
View File
@@ -4,7 +4,9 @@ package services
import (
"errors"
"fmt"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
@@ -44,7 +46,7 @@ func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) {
}
// Add 添加网站
func (r *WebsiteImpl) Add(website types.Website) (models.Website, error) {
func (r *WebsiteImpl) Add(website types.WebsiteAdd) (models.Website, error) {
w := models.Website{
Name: website.Name,
Status: website.Status,
@@ -240,6 +242,9 @@ server
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置,不可修改
include /www/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/%s.conf;
@@ -259,7 +264,7 @@ server
access_log /www/wwwlogs/%s.log;
error_log /www/wwwlogs/%s.log;
}
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name)
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name, website.Name)
if err := io.Write("/www/server/vhost/"+website.Name+".conf", nginxConf, 0644); err != nil {
return models.Website{}, err
@@ -267,6 +272,9 @@ server
if err := io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil {
return models.Website{}, err
}
if err := io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil {
return models.Website{}, err
}
if err := io.Write("/www/server/vhost/ssl/"+website.Name+".pem", "", 0644); err != nil {
return models.Website{}, err
}
@@ -282,6 +290,7 @@ server
}
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return models.Website{}, err
}
@@ -326,6 +335,7 @@ func (r *WebsiteImpl) SaveConfig(config requests.SaveConfig) error {
return err
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return err
}
@@ -356,34 +366,30 @@ func (r *WebsiteImpl) SaveConfig(config requests.SaveConfig) error {
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
// 端口
var port strings.Builder
var portConf strings.Builder
ports := config.Ports
for i, v := range ports {
vStr := cast.ToString(v)
if v == 443 && config.Ssl {
vStr = ` listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic;
listen [::]:443 quic;`
port.WriteString(vStr)
if i != len(ports)-1 {
port.WriteString("\n")
}
continue
for _, port := range ports {
https := ""
quic := false
if slices.Contains(config.TLSPorts, port) {
https = " ssl"
quic = true
}
if i != len(ports)-1 {
port.WriteString(" listen " + vStr + ";\n")
port.WriteString(" listen [::]:" + vStr + ";\n")
} else {
port.WriteString(" listen " + vStr + ";\n")
port.WriteString(" listen [::]:" + vStr + ";")
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https))
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https))
if quic {
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic"))
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic"))
}
}
portConfigOld := str.Cut(raw, "# port标记位开始", "# port标记位结束")
if len(strings.TrimSpace(portConfigOld)) == 0 {
portConf.WriteString(" ")
portConfNew := portConf.String()
portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束")
if len(strings.TrimSpace(portConfOld)) == 0 {
return errors.New("配置文件中缺少port标记位")
}
raw = strings.Replace(raw, portConfigOld, "\n"+port.String()+"\n ", -1)
raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1)
// 运行目录
root := str.Cut(raw, "# root标记位开始", "# root标记位结束")
@@ -525,7 +531,12 @@ func (r *WebsiteImpl) SaveConfig(config requests.SaveConfig) error {
return err
}
return systemctl.Reload("openresty")
err = systemctl.Reload("openresty")
if err != nil {
_, err = shell.Execf("openresty -t")
}
return err
}
// Delete 删除网站
@@ -543,23 +554,19 @@ func (r *WebsiteImpl) Delete(id uint) error {
return err
}
if err := io.Remove("/www/server/vhost/" + website.Name + ".conf"); err != nil {
return err
}
if err := io.Remove("/www/server/vhost/rewrite/" + website.Name + ".conf"); err != nil {
return err
}
if err := io.Remove("/www/server/vhost/ssl/" + website.Name + ".pem"); err != nil {
return err
}
if err := io.Remove("/www/server/vhost/ssl/" + website.Name + ".key"); err != nil {
return err
}
if err := io.Remove(website.Path); err != nil {
return err
_ = io.Remove("/www/server/vhost/" + website.Name + ".conf")
_ = io.Remove("/www/server/vhost/rewrite/" + website.Name + ".conf")
_ = io.Remove("/www/server/vhost/acme/" + website.Name + ".conf")
_ = io.Remove("/www/server/vhost/ssl/" + website.Name + ".pem")
_ = io.Remove("/www/server/vhost/ssl/" + website.Name + ".key")
_ = io.Remove(website.Path)
err := systemctl.Reload("openresty")
if err != nil {
_, err = shell.Execf("openresty -t")
}
return systemctl.Reload("openresty")
return err
}
// GetConfig 获取网站配置
@@ -582,7 +589,7 @@ func (r *WebsiteImpl) GetConfig(id uint) (types.WebsiteSetting, error) {
setting.Raw = config
ports := str.Cut(config, "# port标记位开始", "# port标记位结束")
matches := regexp.MustCompile(`listen\s+(.*);`).FindAllStringSubmatch(ports, -1)
matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(ports, -1)
for _, match := range matches {
if len(match) < 2 {
continue
@@ -598,26 +605,27 @@ func (r *WebsiteImpl) GetConfig(id uint) (types.WebsiteSetting, error) {
setting.Ports = append(setting.Ports, cast.ToUint(ports[0]))
} else if len(ports) > 1 && ports[1] == "ssl" {
setting.Ports = append(setting.Ports, cast.ToUint(ports[0]))
setting.TLSPorts = append(setting.TLSPorts, cast.ToUint(ports[0]))
}
}
serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
match := regexp.MustCompile(`server_name\s+(.*);`).FindStringSubmatch(serverName)
match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName)
if len(match) > 1 {
setting.Domains = strings.Split(match[1], " ")
}
root := str.Cut(config, "# root标记位开始", "# root标记位结束")
match = regexp.MustCompile(`root\s+(.*);`).FindStringSubmatch(root)
match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root)
if len(match) > 1 {
setting.Root = match[1]
}
index := str.Cut(config, "# index标记位开始", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.*);`).FindStringSubmatch(index)
match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index)
if len(match) > 1 {
setting.Index = match[1]
}
if io.Exists(setting.Root + "/.user.ini") {
userIni, _ := io.Read(setting.Root + "/.user.ini")
if io.Exists(filepath.Join(setting.Root, ".user.ini")) {
userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini"))
if strings.Contains(userIni, "open_basedir") {
setting.OpenBasedir = true
} else {
@@ -635,29 +643,31 @@ func (r *WebsiteImpl) GetConfig(id uint) (types.WebsiteSetting, error) {
ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
setting.HttpRedirect = strings.Contains(ssl, "# http重定向标记位")
setting.Hsts = strings.Contains(ssl, "# hsts标记位")
if decode, err := cert.ParseCert(crt); err == nil {
setting.SslNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
setting.SslNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05")
setting.SslIssuer = decode.Issuer.CommonName
setting.SslOCSPServer = decode.OCSPServer
setting.SSlDNSNames = decode.DNSNames
}
} else {
setting.HttpRedirect = false
setting.Hsts = false
}
// 解析证书信息
if decode, err := cert.ParseCert(crt); err == nil {
setting.SslNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
setting.SslNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05")
setting.SslIssuer = decode.Issuer.CommonName
setting.SslOCSPServer = decode.OCSPServer
setting.SSlDNSNames = decode.DNSNames
}
waf := str.Cut(config, "# waf标记位开始", "# waf标记位结束")
setting.Waf = strings.Contains(waf, "waf on;")
match = regexp.MustCompile(`waf_mode\s+(.+);`).FindStringSubmatch(waf)
match = regexp.MustCompile(`waf_mode\s+([^;]*);?`).FindStringSubmatch(waf)
if len(match) > 1 {
setting.WafMode = match[1]
}
match = regexp.MustCompile(`waf_cc_deny\s+(.+);`).FindStringSubmatch(waf)
match = regexp.MustCompile(`waf_cc_deny\s+([^;]*);?`).FindStringSubmatch(waf)
if len(match) > 1 {
setting.WafCcDeny = match[1]
}
match = regexp.MustCompile(`waf_cache\s+(.+);`).FindStringSubmatch(waf)
match = regexp.MustCompile(`waf_cache\s+([^;]*);?`).FindStringSubmatch(waf)
if len(match) > 1 {
setting.WafCache = match[1]
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Website interface {
List(page int, limit int) (int64, []models.Website, error)
Add(website types.Website) (models.Website, error)
Add(website types.WebsiteAdd) (models.Website, error)
SaveConfig(config requests.SaveConfig) error
Delete(id uint) error
GetConfig(id uint) (types.WebsiteSetting, error)
-3
View File
@@ -194,9 +194,6 @@
"incompatible": "plugin :slug is incompatible with :exclude plugin"
}
},
"messages": {
"mistake": "mistake"
},
"status": {
"upgrade": "Panel is currently undergoing an upgrade. Please try again later.",
"maintain": "Panel is currently undergoing maintenance. Please try again later.",
-3
View File
@@ -194,9 +194,6 @@
"incompatible": "插件 :slug 不兼容 :exclude 插件"
}
},
"messages": {
"mistake": "错误"
},
"status": {
"upgrade": "面板升级中,请稍后",
"maintain": "面板正在运行维护,请稍后",
+4 -1
View File
@@ -49,9 +49,12 @@ func (c *Client) UseManualDns(total int, check ...bool) {
}
// UseHTTP 使用 HTTP 验证
func (c *Client) UseHTTP(path string) {
// conf openresty 配置文件路径
// path 验证文件存放路径
func (c *Client) UseHTTP(conf, path string) {
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: httpSolver{
conf: conf,
path: path,
},
}
+32 -23
View File
@@ -13,9 +13,13 @@ import (
"github.com/libdns/libdns"
"github.com/mholt/acmez/v2/acme"
"golang.org/x/net/publicsuffix"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
type httpSolver struct {
conf string
path string
}
@@ -26,12 +30,25 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
}
challengeFilePath := filepath.Join(s.path, challenge.HTTP01ResourcePath())
if err = os.MkdirAll(filepath.Dir(challengeFilePath), 0o755); err != nil {
return fmt.Errorf("无法在网站目录创建 HTTP 挑战所需的目录: %w", err)
if err = os.MkdirAll(filepath.Dir(challengeFilePath), 0755); err != nil {
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的目录: %w", err)
}
if err = os.WriteFile(challengeFilePath, []byte(challenge.KeyAuthorization), 0o644); err != nil {
return fmt.Errorf("无法在网站目录创建 HTTP 挑战所需的文件: %w", err)
if err = os.WriteFile(challengeFilePath, []byte(challenge.KeyAuthorization), 0644); err != nil {
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的文件: %w", err)
}
conf := fmt.Sprintf(`location = /.well-known/acme-challenge/%s {
default_type text/plain;
return 200 %q;
}
`, challenge.Token, challenge.KeyAuthorization)
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
return fmt.Errorf("无法写入OpenResty配置文件: %w", err)
}
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return fmt.Errorf("无法重载OpenResty: %w", err)
}
return nil
@@ -43,10 +60,9 @@ func (s httpSolver) CleanUp(_ context.Context, challenge acme.Challenge) error {
return nil
}
if err := os.Remove(filepath.Join(s.path, challenge.HTTP01ResourcePath())); err != nil {
return fmt.Errorf("无法删除 HTTP 挑战文件: %w", err)
}
_ = os.Remove(filepath.Join(s.path, challenge.HTTP01ResourcePath()))
_ = os.WriteFile(s.conf, []byte{}, 0644)
_ = systemctl.Reload("openresty")
return nil
}
@@ -61,11 +77,11 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
keyAuth := challenge.DNS01KeyAuthorization()
provider, err := s.getDNSProvider()
if err != nil {
return fmt.Errorf("获取 DNS 提供商失败: %w", err)
return fmt.Errorf("获取DNS提供商失败: %w", err)
}
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
if err != nil {
return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err)
return fmt.Errorf("获取域名%q的顶级域失败: %w", dnsName, err)
}
rec := libdns.Record{
@@ -76,10 +92,10 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
results, err := provider.AppendRecords(ctx, zone+".", []libdns.Record{rec})
if err != nil {
return fmt.Errorf("域名 %q 添加临时记录 %q 失败: %w", zone, dnsName, err)
return fmt.Errorf("域名%q添加临时记录%q失败: %w", zone, dnsName, err)
}
if len(results) != 1 {
return fmt.Errorf("预期添加 1 条记录,但实际添加了 %d 条记录", len(results))
return fmt.Errorf("预期添加1条记录,但实际添加了%d条记录", len(results))
}
s.records = &results
@@ -90,21 +106,14 @@ func (s dnsSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error
dnsName := challenge.DNS01TXTRecordName()
provider, err := s.getDNSProvider()
if err != nil {
return fmt.Errorf("获取 DNS 提供商失败: %w", err)
}
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
if err != nil {
return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err)
return fmt.Errorf("获取DNS提供商失败: %w", err)
}
zone, _ := publicsuffix.EffectiveTLDPlusOne(dnsName)
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
_, err = provider.DeleteRecords(ctx, zone+".", *s.records)
if err != nil {
return fmt.Errorf("域名 %q 删除临时记录 %q 失败: %w", zone, dnsName, err)
}
_, _ = provider.DeleteRecords(ctx, zone+".", *s.records)
return nil
}
@@ -126,7 +135,7 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) {
APIToken: s.param.APIkey,
}
default:
return nil, fmt.Errorf("未知的 DNS 提供商 %q", s.dns)
return nil, fmt.Errorf("未知的DNS提供商 %q", s.dns)
}
return dns, nil
+173
View File
@@ -0,0 +1,173 @@
package db
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/TheTNB/panel/pkg/types"
)
type MySQL struct {
db *sql.DB
username string
password string
address string
}
func NewMySQL(username, password, address string, typ ...string) (*MySQL, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/", username, password, address)
if len(typ) > 0 && typ[0] == "unix" {
dsn = fmt.Sprintf("%s:%s@unix(%s)/", username, password, address)
}
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("初始化MySQL连接失败: %w", err)
}
if db.Ping() != nil {
return nil, fmt.Errorf("连接MySQL失败: %w", err)
}
return &MySQL{
db: db,
username: username,
password: password,
address: address,
}, nil
}
func (m *MySQL) Close() error {
return m.db.Close()
}
func (m *MySQL) Ping() error {
return m.db.Ping()
}
func (m *MySQL) Query(query string, args ...any) (*sql.Rows, error) {
return m.db.Query(query, args...)
}
func (m *MySQL) QueryRow(query string, args ...any) *sql.Row {
return m.db.QueryRow(query, args...)
}
func (m *MySQL) Exec(query string, args ...any) (sql.Result, error) {
return m.db.Exec(query, args...)
}
func (m *MySQL) Prepare(query string) (*sql.Stmt, error) {
return m.db.Prepare(query)
}
func (m *MySQL) DatabaseCreate(name string) error {
_, err := m.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", name))
m.flushPrivileges()
return err
}
func (m *MySQL) DatabaseDrop(name string) error {
_, err := m.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", name))
m.flushPrivileges()
return err
}
func (m *MySQL) UserCreate(user, password string) error {
_, err := m.Exec(fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'localhost' IDENTIFIED BY '%s'", user, password))
m.flushPrivileges()
return err
}
func (m *MySQL) UserDrop(user string) error {
_, err := m.Exec(fmt.Sprintf("DROP USER IF EXISTS '%s'@'localhost'", user))
m.flushPrivileges()
return err
}
func (m *MySQL) UserPassword(user, password string) error {
_, err := m.Exec(fmt.Sprintf("ALTER USER '%s'@'localhost' IDENTIFIED BY '%s'", user, password))
m.flushPrivileges()
return err
}
func (m *MySQL) PrivilegesGrant(user, database string) error {
_, err := m.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO '%s'@'localhost'", database, user))
m.flushPrivileges()
return err
}
func (m *MySQL) PrivilegesRevoke(user, database string) error {
_, err := m.Exec(fmt.Sprintf("REVOKE ALL PRIVILEGES ON %s.* FROM '%s'@'localhost'", database, user))
m.flushPrivileges()
return err
}
func (m *MySQL) Users() ([]types.MySQLUser, error) {
rows, err := m.Query("SELECT user, host FROM mysql.user")
if err != nil {
return nil, err
}
defer rows.Close()
var users []types.MySQLUser
for rows.Next() {
var user, host string
if err := rows.Scan(&user, &host); err != nil {
continue
}
grants, err := m.userGrants(user, host)
if err != nil {
continue
}
users = append(users, types.MySQLUser{
User: user,
Host: host,
Grants: grants,
})
}
return users, nil
}
func (m *MySQL) Databases() ([]types.MySQLDatabase, error) {
rows, err := m.Query("SHOW DATABASES")
if err != nil {
return nil, err
}
defer rows.Close()
var databases []types.MySQLDatabase
for rows.Next() {
var database string
if err := rows.Scan(&database); err != nil {
continue
}
databases = append(databases, types.MySQLDatabase{
Name: database,
})
}
return databases, nil
}
func (m *MySQL) userGrants(user, host string) ([]string, error) {
rows, err := m.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", user, host))
if err != nil {
return nil, err
}
defer rows.Close()
var grants []string
for rows.Next() {
var grant string
if err := rows.Scan(&grant); err != nil {
continue
}
grants = append(grants, grant)
}
return grants, nil
}
func (m *MySQL) flushPrivileges() {
_, _ = m.Exec("FLUSH PRIVILEGES")
}
+33
View File
@@ -0,0 +1,33 @@
package db
import (
"errors"
"fmt"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
// MySQLResetRootPassword 重置 MySQL root密码
func MySQLResetRootPassword(password string) error {
_ = systemctl.Stop("mysqld")
if run, err := systemctl.Status("mysqld"); err != nil || run {
return fmt.Errorf("停止MySQL失败: %w", err)
}
_, _ = shell.Execf(`systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"`)
if err := systemctl.Start("mysqld"); err != nil {
return fmt.Errorf("以安全模式启动MySQL失败: %w", err)
}
if _, err := shell.Execf(`mysql -uroot -e "FLUSH PRIVILEGES;UPDATE mysql.user SET authentication_string=null WHERE user='root' AND host='localhost';ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';FLUSH PRIVILEGES;"`, password); err != nil {
return errors.New("设置root密码失败")
}
if err := systemctl.Stop("mysqld"); err != nil {
return fmt.Errorf("停止MySQL失败: %w", err)
}
_, _ = shell.Execf(`systemctl unset-environment MYSQLD_OPTS`)
if err := systemctl.Start("mysqld"); err != nil {
return fmt.Errorf("启动MySQL失败: %w", err)
}
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package migrate
import (
"fmt"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
func Migrate(db *gorm.DB) {
options := &gormigrate.Options{
TableName: "new_migrations",
IDColumnName: "id",
IDColumnSize: 255,
}
migrator := gormigrate.New(db, options, []*gormigrate.Migration{
Init,
})
if err := migrator.Migrate(); err != nil {
panic(fmt.Sprintf("Failed to migrate database: %v", err))
}
}
+42
View File
@@ -0,0 +1,42 @@
package migrate
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
"github.com/TheTNB/panel/app/models"
)
var Init = &gormigrate.Migration{
ID: "20240624-init",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&models.Cert{},
&models.CertDNS{},
&models.CertUser{},
&models.Cron{},
&models.Database{},
&models.Monitor{},
&models.Plugin{},
&models.Setting{},
&models.Task{},
&models.User{},
&models.Website{},
)
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable(
&models.Cert{},
&models.CertDNS{},
&models.CertUser{},
&models.Cron{},
&models.Database{},
&models.Monitor{},
&models.Plugin{},
&models.Setting{},
&models.Task{},
&models.User{},
&models.Website{},
)
},
}
+11
View File
@@ -0,0 +1,11 @@
package types
type MySQLUser struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
type MySQLDatabase struct {
Name string `json:"name"`
}
+1 -1
View File
@@ -137,7 +137,7 @@ var PluginPHPMyAdmin = Plugin{
Description: "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具",
Slug: "phpmyadmin",
Version: "5.2.1",
Requires: []string{},
Requires: []string{"openresty"},
Excludes: []string{},
Install: `bash /www/panel/scripts/phpmyadmin/install.sh`,
Uninstall: `bash /www/panel/scripts/phpmyadmin/uninstall.sh`,
+2 -1
View File
@@ -1,6 +1,6 @@
package types
type Website struct {
type WebsiteAdd struct {
Name string `json:"name"`
Status bool `json:"status"`
Domains []string `json:"domains"`
@@ -21,6 +21,7 @@ type WebsiteSetting struct {
Name string `json:"name"`
Domains []string `json:"domains"`
Ports []uint `json:"ports"`
TLSPorts []uint `json:"tls_ports"`
Root string `json:"root"`
Path string `json:"path"`
Index string `json:"index"`
+2 -1
View File
@@ -3,6 +3,7 @@ package routes
import (
"github.com/goravel/framework/contracts/route"
"github.com/goravel/framework/facades"
frameworkmiddleware "github.com/goravel/framework/http/middleware"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/http/middleware"
@@ -25,7 +26,7 @@ func Api() {
})
r.Prefix("user").Group(func(r route.Router) {
userController := controllers.NewUserController()
r.Post("login", userController.Login)
r.Middleware(frameworkmiddleware.Throttle("login")).Post("login", userController.Login)
r.Middleware(middleware.Jwt()).Get("info", userController.Info)
})
r.Prefix("task").Middleware(middleware.Jwt()).Group(func(r route.Router) {
+2
View File
@@ -85,6 +85,8 @@ func Plugin() {
phpMyAdminController := plugins.NewPhpMyAdminController()
route.Get("info", phpMyAdminController.Info)
route.Post("port", phpMyAdminController.SetPort)
route.Get("config", phpMyAdminController.GetConfig)
route.Post("config", phpMyAdminController.SaveConfig)
})
r.Prefix("pureftpd").Group(func(route route.Router) {
pureFtpdController := plugins.NewPureFtpdController()
+4 -1
View File
@@ -204,12 +204,15 @@ Auto_Swap() {
}
Init_Panel() {
systemctl stop panel
systemctl disable panel
rm -f /etc/systemd/system/panel.service
rm -rf ${setup_Path}/panel
mkdir ${setup_Path}/server
mkdir ${setup_Path}/server/cron
mkdir ${setup_Path}/server/cron/logs
chmod -R 755 ${setup_Path}/server
mkdir ${setup_Path}/panel
rm -rf ${setup_Path}/panel/*
# 下载面板zip包并解压
if [ "${ARCH}" == "x86_64" ]; then
if ${inChina}; then
+2 -2
View File
@@ -287,8 +287,8 @@ chmod 644 ${mysqlPath}/conf/my.cnf
${mysqlPath}/bin/mysqld --initialize-insecure --user=mysql --basedir=${mysqlPath} --datadir=${mysqlPath}/data
echo "export PATH=${mysqlPath}/bin:\$PATH" >> /etc/profile.d/mysql.sh
source /etc/profile
# 软链接
ln -sf ${mysqlPath}/bin/* /usr/bin/
# 写入 systemd 配置
cat > /etc/systemd/system/mysqld.service << EOF
-3
View File
@@ -34,9 +34,6 @@ rm -f /usr/lib64/libmysql*
userdel -r mysql
groupdel mysql
rm -f /etc/profile.d/mysql.sh
source /etc/profile
panel deletePlugin mysql${1}
echo -e "${HR}\nMySQL-${1} 卸载完成\n${HR}"
+3
View File
@@ -120,6 +120,9 @@ chown -R mysql:mysql ${mysqlPath}
chmod -R 755 ${mysqlPath}
chmod 644 ${mysqlPath}/conf/my.cnf
# 软链接
ln -sf ${mysqlPath}/bin/* /usr/bin/
# 启动服务
systemctl start mysqld
+2 -1
View File
@@ -282,6 +282,7 @@ mkdir -p /www/server/vhost
mkdir -p /www/server/vhost
mkdir -p /www/server/vhost/rewrite
mkdir -p /www/server/vhost/ssl
mkdir -p /www/server/vhost/acme
# 写入主配置文件
cat > ${openrestyPath}/conf/nginx.conf << EOF
@@ -561,7 +562,7 @@ cat > ${openrestyPath}/html/block.html << EOF
<div class="container">
<h1>耗子面板</h1>
<p>本次请求判断为危险的攻击请求,已被拦截!</p>
<p>当您看到此页面,说明您的请求被WAF拦截,可能是由于您的请求中包含了危险的攻击内容,或者您的请求被误判为攻击请求。</p>
<p>可能您的请求中包含了危险的攻击内容,或者您的请求被误判为攻击请求。</p>
<p>如果您认为这是误判,请联系服务器管理员解决。</p>
</div>
</body>
+6
View File
@@ -28,6 +28,12 @@ systemctl disable php-fpm-${phpVersion}
rm -rf /lib/systemd/system/php-fpm-${phpVersion}.service
systemctl daemon-reload
# 检查是否存在phpMyAdmin
if [ -d "${setupPath}/server/phpmyadmin" ]; then
sed -i "s/enable-php-${phpVersion}/enable-php-0/g" ${setupPath}/server/vhost/phpmyadmin.conf
systemctl reload openresty
fi
rm -rf ${phpPath}
rm -f /usr/bin/php-${phpVersion}
+19
View File
@@ -124,6 +124,25 @@ if version_lt "$oldVersion" "2.2.10"; then
fi
fi
if version_lt "$oldVersion" "2.2.14"; then
echo "更新面板到 v2.2.14 ..."
echo "Update panel to v2.2.14 ..."
if [ -f "/www/server/openresty/bin/openresty" ]; then
mkdir -p /www/server/vhost/acme
chmod -R 644 /www/server/vhost/acme
fi
fi
if version_lt "$oldVersion" "2.2.16"; then
echo "更新面板到 v2.2.16 ..."
echo "Update panel to v2.2.16 ..."
if [ -f "/www/server/mysql/bin/mysql" ]; then
ln -sf /www/server/mysql/bin/* /usr/bin/
rm -f /etc/profile.d/mysql.sh
source /etc/profile
fi
fi
echo $HR
echo "更新结束"
echo "Update finished"