2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: 网站重构1

This commit is contained in:
2025-12-01 22:50:16 +08:00
parent b17b7ccfee
commit 2b8890305c
24 changed files with 175 additions and 301 deletions

View File

@@ -78,8 +78,8 @@ func initWeb() (*app.Web, error) {
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(locale, db, logger)
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo)
settingRepo := data.NewSettingRepo(locale, db, koanf, taskRepo)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
cronRepo := data.NewCronRepo(locale, db)
backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo)
homeService := service.NewHomeService(locale, koanf, taskRepo, websiteRepo, appRepo, settingRepo, cronRepo, backupRepo)

View File

@@ -69,7 +69,7 @@ func initCli() (*app.Cli, error) {
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(locale, db, logger)
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo)
cliService := service.NewCliService(locale, koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo)
cli := route.NewCli(locale, cliService)

6
go.sum
View File

@@ -167,8 +167,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
@@ -464,8 +462,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -538,8 +534,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=

View File

@@ -19,6 +19,7 @@ const (
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
SettingKeyAutoUpdate SettingKey = "auto_update"
SettingKeyWebServer SettingKey = "web_server"
)
type Setting struct {

View File

@@ -20,6 +20,7 @@ type Website struct {
UpdatedAt time.Time `json:"updated_at"`
CertExpire string `gorm:"-:all" json:"cert_expire"` // 仅显示
PHP uint `gorm:"-:all" json:"php"` // 仅显示
Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"`
}
@@ -30,7 +31,7 @@ type WebsiteRepo interface {
Count() (int64, error)
Get(id uint) (*types.WebsiteSetting, error)
GetByName(name string) (*types.WebsiteSetting, error)
List(page, limit uint) ([]*Website, int64, error)
List(typ string, page, limit uint) ([]*Website, int64, error)
Create(req *request.WebsiteCreate) (*Website, error)
Update(req *request.WebsiteUpdate) error
Delete(req *request.WebsiteDelete) error

View File

@@ -29,6 +29,8 @@ import (
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/systemctl"
"github.com/acepanel/panel/pkg/types"
"github.com/acepanel/panel/pkg/webserver"
webservertypes "github.com/acepanel/panel/pkg/webserver/types"
)
type websiteRepo struct {
@@ -40,9 +42,10 @@ type websiteRepo struct {
databaseUser biz.DatabaseUserRepo
cert biz.CertRepo
certAccount biz.CertAccountRepo
setting biz.SettingRepo
}
func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) biz.WebsiteRepo {
func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo, setting biz.SettingRepo) biz.WebsiteRepo {
return &websiteRepo{
t: t,
db: db,
@@ -52,6 +55,7 @@ func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database
databaseUser: databaseUser,
cert: cert,
certAccount: certAccount,
setting: setting,
}
}
@@ -211,7 +215,7 @@ func (r *websiteRepo) GetByName(name string) (*types.WebsiteSetting, error) {
}
func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
func (r *websiteRepo) List(typ string, page, limit uint) ([]*biz.Website, int64, error) {
websites := make([]*biz.Website, 0)
var total int64
@@ -219,35 +223,56 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
return nil, 0, err
}
if err := r.db.Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&websites).Error; err != nil {
query := r.db
if typ != "all" {
query = query.Where("type = ?", typ)
}
if err := query.Order("id DESC").Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&websites).Error; err != nil {
return nil, 0, err
}
// 取证书剩余有效时间
// 取证书剩余有效时间和PHP版本
for _, website := range websites {
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
if decode, err := cert.ParseCert(crt); err == nil {
hours := time.Until(decode.NotAfter).Hours()
website.CertExpire = fmt.Sprintf("%.2f", hours/24)
}
if website.Type == "php" {
website.PHP = r.getPHPVersion(website.Name)
}
}
return websites, total, nil
}
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
// 初始化nginx配置
config := nginx.DefaultConf
p, err := nginx.NewParser(config)
webServer, err := r.setting.Get(biz.SettingKeyWebServer)
if err != nil {
return nil, err
}
// 监听地址
var listens [][]string
for _, listen := range req.Listens {
listens = append(listens, []string{listen})
var vhost webservertypes.Vhost
switch req.Type {
case "proxy":
vhost, err = webserver.NewProxyVhost(webserver.Type(webServer), filepath.Join(app.Root, "sites", req.Name, "config"))
case "php":
vhost, err = webserver.NewPHPVhost(webserver.Type(webServer), filepath.Join(app.Root, "sites", req.Name, "config"))
case "static":
vhost, err = webserver.NewStaticVhost(webserver.Type(webServer), filepath.Join(app.Root, "sites", req.Name, "config"))
default:
return nil, errors.New(r.t.Get("unsupported website type: %s", req.Type))
}
if err = p.SetListen(listens); err != nil {
if err != nil {
return nil, err
}
// 监听地址
var listens []webservertypes.Listen
for _, listen := range req.Listens {
listens = append(listens, webservertypes.Listen{Address: listen})
}
if err = vhost.SetListen(listens); err != nil {
return nil, err
}
// 域名
@@ -255,37 +280,35 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
if err != nil {
return nil, err
}
if err = p.SetServerName(domains); err != nil {
if err = vhost.SetServerName(domains); err != nil {
return nil, err
}
// 运行目录
if err = p.SetRoot(req.Path); err != nil {
return nil, err
}
// PHP
if err = p.SetPHP(req.PHP); err != nil {
return nil, err
}
// 伪静态和acme
includes, comments, err := p.GetIncludes()
if err != nil {
return nil, err
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"))
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"))
comments = append(comments, []string{r.t.Get("# Rewrite rule")})
comments = append(comments, []string{"# acme http-01"})
if err = p.SetIncludes(includes, comments); err != nil {
if err = vhost.SetRoot(req.Path); err != nil {
return nil, err
}
// 日志
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil {
if err = vhost.SetAccessLog(filepath.Join(app.Root, "sites", req.Name, "log", "access.log")); err != nil {
return nil, err
}
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil {
if err = vhost.SetErrorLog(filepath.Join(app.Root, "sites", req.Name, "log", "error.log")); err != nil {
return nil, err
}
// 反向代理支持
if proxyVhost, ok := vhost.(webservertypes.ProxyVhost); ok {
if err = proxyVhost.SetProxies([]webservertypes.Proxy{req.Proxy}); err != nil {
return nil, err
}
}
// PHP 支持
if phpVhost, ok := vhost.(webservertypes.PHPVhost); ok {
if err = phpVhost.SetPHP(req.PHP); err != nil {
return nil, err
}
}
// 初始化网站目录
if err = os.MkdirAll(req.Path, 0755); err != nil {
return nil, err
@@ -321,20 +344,20 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
return nil, err
}
// 写nginx配置
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); err != nil {
// 写配置
if err = vhost.Save(); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "vhost", "001-acme.conf"), "", 0644); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".pem"), "", 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "fullchain.pem"), "", 0644); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".key"), "", 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "privatekey.key"), "", 0644); err != nil {
return nil, err
}
@@ -347,7 +370,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
}
// PHP 网站默认开启防跨站
if req.PHP > 0 {
if req.Type == "php" {
userIni := filepath.Join(req.Path, ".user.ini")
if !io.Exists(userIni) {
if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Path), 0644); err != nil {
@@ -360,7 +383,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
// 创建面板网站
w := &biz.Website{
Name: req.Name,
Type: "php", // TODO 支持网站类型
Type: req.Type,
Status: true,
Path: req.Path,
Https: false,
@@ -370,8 +393,8 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
return nil, err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
// 重载 Web 服务器
if err = vhost.Reload(); err != nil {
return nil, err
}
@@ -839,3 +862,11 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
return r.cert.Deploy(newCert.ID, website.ID)
}
func (r *websiteRepo) getPHPVersion(name string) uint {
vhost, err := webserver.NewPHPVhost(webserver.TypeNginx, filepath.Join(app.Root, "sites", name, "config"))
if err != nil {
return 0
}
return vhost.PHP()
}

View File

@@ -1,24 +1,35 @@
package request
import "github.com/acepanel/panel/pkg/types"
import (
"github.com/acepanel/panel/pkg/types"
webservertypes "github.com/acepanel/panel/pkg/webserver/types"
)
type WebsiteDefaultConfig struct {
Index string `json:"index" form:"index" validate:"required"`
Stop string `json:"stop" form:"stop" validate:"required"`
}
type WebsiteList struct {
Type string `json:"type" form:"type" validate:"required|in:all,proxy,static,php"`
Paginate
}
type WebsiteCreate struct {
Type string `json:"type" form:"type" validate:"required|in:proxy,static,php"`
Name string `form:"name" json:"name" validate:"required|notExists:websites,name|not_in:phpmyadmin,default|regex:^[a-zA-Z0-9_-]+$"`
Listens []string `form:"listens" json:"listens" validate:"required|isSlice"`
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
Path string `form:"path" json:"path"`
PHP int `form:"php" json:"php"`
DB bool `form:"db" json:"db"`
DBType string `form:"db_type" json:"db_type" validate:"requiredIf:DB,true"`
DBName string `form:"db_name" json:"db_name" validate:"requiredIf:DB,true"`
DBUser string `form:"db_user" json:"db_user" validate:"requiredIf:DB,true"`
DBPassword string `form:"db_password" json:"db_password" validate:"requiredIf:DB,true"`
Remark string `form:"remark" json:"remark"`
PHP uint `form:"php" json:"php" validate:"requiredIf:Type,php"` // 仅 PHP 网站需要
Proxy webservertypes.Proxy `form:"proxy" json:"proxy" validate:"requiredIf:Type,proxy"` // 仅反向代理网站需要
}
type WebsiteDelete struct {

View File

@@ -570,7 +570,7 @@ func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error
Domains: cmd.StringSlice("domains"),
Listens: cmd.StringSlice("listens"),
Path: cmd.String("path"),
PHP: int(cmd.Int("php")),
PHP: cmd.Uint("php"),
DB: false,
}

View File

@@ -83,14 +83,15 @@ func (s *WebsiteService) UpdateCert(w http.ResponseWriter, r *http.Request) {
Success(w, nil)
}
// List 网站列表
func (s *WebsiteService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.Paginate](r)
req, err := Bind[request.WebsiteList](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
websites, total, err := s.websiteRepo.List(req.Page, req.Limit)
websites, total, err := s.websiteRepo.List(req.Type, req.Page, req.Limit)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return

View File

@@ -137,18 +137,7 @@ func (v *baseVhost) Listen() []types.Listen {
// Apache 的监听配置通常在 VirtualHost 的参数中
// 例如: <VirtualHost *:80> 或 <VirtualHost 192.168.1.1:443>
for _, arg := range v.vhost.Args {
listen := types.Listen{
Address: arg,
Options: make(map[string]string),
}
// 检查是否是 HTTPS
if strings.Contains(arg, ":443") || v.HTTPS() {
listen.Protocol = "https"
} else {
listen.Protocol = "http"
}
listen := types.Listen{Address: arg}
result = append(result, listen)
}
@@ -583,7 +572,7 @@ func (v *baseVhost) SetRedirects(redirects []types.Redirect) error {
// ========== PHPVhost ==========
func (v *PHPVhost) PHP() int {
func (v *PHPVhost) PHP() uint {
// Apache 通常通过 FilesMatch 块配置 PHP
// 或者通过 SetHandler 指令
handler := v.vhost.GetDirectiveValue("SetHandler")
@@ -606,7 +595,7 @@ func (v *PHPVhost) PHP() int {
if len(parts) >= 2 {
major, _ := strconv.Atoi(parts[0])
minor, _ := strconv.Atoi(parts[1])
return major*10 + minor
return uint(major*10 + minor)
}
}
}
@@ -630,7 +619,7 @@ func (v *PHPVhost) PHP() int {
return 0
}
func (v *PHPVhost) SetPHP(version int) error {
func (v *PHPVhost) SetPHP(version uint) error {
// 移除现有的 PHP 配置
v.vhost.RemoveDirective("SetHandler")

View File

@@ -131,8 +131,8 @@ func (s *VhostTestSuite) TestIndexEmpty() {
func (s *VhostTestSuite) TestListen() {
listens := []types.Listen{
{Address: "*:80", Protocol: "http"},
{Address: "*:443", Protocol: "https"},
{Address: "*:80"},
{Address: "*:443"},
}
err := s.vhost.SetListen(listens)
s.NoError(err)
@@ -345,7 +345,7 @@ func (s *VhostTestSuite) TestExportWithSSL() {
func (s *VhostTestSuite) TestListenProtocolDetection() {
listens := []types.Listen{
{Address: "*:443", Protocol: "https"},
{Address: "*:443"},
}
s.NoError(s.vhost.SetListen(listens))
@@ -357,7 +357,7 @@ func (s *VhostTestSuite) TestListenProtocolDetection() {
got := s.vhost.Listen()
s.Len(got, 1)
s.Equal("https", got[0].Protocol)
s.Equal("*:443", got[0].Address)
}
func (s *VhostTestSuite) TestDirectoryBlock() {

View File

@@ -6,47 +6,6 @@ import (
"strings"
)
func (p *Parser) GetListen() ([][]string, error) {
directives, err := p.Find("server.listen")
if err != nil {
return nil, err
}
var result [][]string
for _, dir := range directives {
result = append(result, p.parameters2Slices(dir.GetParameters()))
}
return result, nil
}
func (p *Parser) GetServerName() ([]string, error) {
directive, err := p.FindOne("server.server_name")
if err != nil {
return nil, err
}
return p.parameters2Slices(directive.GetParameters()), nil
}
func (p *Parser) GetIndex() ([]string, error) {
directive, err := p.FindOne("server.index")
if err != nil {
return nil, err
}
return p.parameters2Slices(directive.GetParameters()), nil
}
func (p *Parser) GetIndexWithComment() ([]string, []string, error) {
directive, err := p.FindOne("server.index")
if err != nil {
return nil, nil, err
}
return p.parameters2Slices(directive.GetParameters()), directive.GetComment(), nil
}
func (p *Parser) GetRoot() (string, error) {
directive, err := p.FindOne("server.root")
if err != nil {
@@ -88,36 +47,6 @@ func (p *Parser) GetIncludes() (includes []string, comments [][]string, err erro
return includes, comments, nil
}
func (p *Parser) GetPHP() int {
directives, err := p.Find("server.include")
if err != nil {
return 0
}
var result int
for _, dir := range directives {
if slices.ContainsFunc(p.parameters2Slices(dir.GetParameters()), func(s string) bool {
return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf")
}) {
_, _ = fmt.Sscanf(dir.GetParameters()[0].GetValue(), "enable-php-%d.conf", &result)
}
}
return result
}
func (p *Parser) GetHTTPS() bool {
directive, err := p.FindOne("server.ssl_certificate")
if err != nil {
return false
}
if len(p.parameters2Slices(directive.GetParameters())) == 0 {
return false
}
return true
}
func (p *Parser) GetHTTPSProtocols() []string {
directive, err := p.FindOne("server.ssl_protocols")
if err != nil {

View File

@@ -15,56 +15,6 @@ func TestNginxTestSuite(t *testing.T) {
suite.Run(t, &NginxTestSuite{})
}
func (s *NginxTestSuite) TestListen() {
parser, err := NewParser()
s.NoError(err)
listen, err := parser.GetListen()
s.NoError(err)
s.Equal([][]string{{"80"}}, listen)
s.NoError(parser.SetListen([][]string{{"80"}, {"443"}}))
listen, err = parser.GetListen()
s.NoError(err)
s.Equal([][]string{{"80"}, {"443"}}, listen)
}
func (s *NginxTestSuite) TestServerName() {
parser, err := NewParser()
s.NoError(err)
serverName, err := parser.GetServerName()
s.NoError(err)
s.Equal([]string{"localhost"}, serverName)
s.NoError(parser.SetServerName([]string{"example.com"}))
serverName, err = parser.GetServerName()
s.NoError(err)
s.Equal([]string{"example.com"}, serverName)
}
func (s *NginxTestSuite) TestIndex() {
parser, err := NewParser()
s.NoError(err)
index, err := parser.GetIndex()
s.NoError(err)
s.Equal([]string{"index.php", "index.html"}, index)
s.NoError(parser.SetIndex([]string{"index.html", "index.php"}))
index, err = parser.GetIndex()
s.NoError(err)
s.Equal([]string{"index.html", "index.php"}, index)
}
func (s *NginxTestSuite) TestIndexWithComment() {
parser, err := NewParser()
s.NoError(err)
index, comment, err := parser.GetIndexWithComment()
s.NoError(err)
s.Equal([]string{"index.php", "index.html"}, index)
s.Equal([]string(nil), comment)
s.NoError(parser.SetIndexWithComment([]string{"index.html", "index.php"}, []string{"# 测试"}))
index, comment, err = parser.GetIndexWithComment()
s.NoError(err)
s.Equal([]string{"index.html", "index.php"}, index)
s.Equal([]string{"# 测试"}, comment)
}
func (s *NginxTestSuite) TestRoot() {
parser, err := NewParser()
s.NoError(err)
@@ -110,16 +60,6 @@ func (s *NginxTestSuite) TestIncludes() {
s.Equal([][]string{{"# 伪静态规则测试"}}, comments)
}
func (s *NginxTestSuite) TestPHP() {
parser, err := NewParser()
s.NoError(err)
s.Equal(0, parser.GetPHP())
s.NoError(parser.SetPHP(80))
s.Equal(80, parser.GetPHP())
s.NoError(parser.SetPHP(0))
s.Equal(0, parser.GetPHP())
}
func (s *NginxTestSuite) TestHTTP() {
parser, err := NewParser()
s.NoError(err)
@@ -128,17 +68,6 @@ func (s *NginxTestSuite) TestHTTP() {
s.Equal(string(expect), parser.Dump())
}
func (s *NginxTestSuite) TestHTTPS() {
parser, err := NewParser()
s.NoError(err)
s.False(parser.GetHTTPS())
s.NoError(parser.SetHTTPSCert("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
s.True(parser.GetHTTPS())
expect, err := os.ReadFile("testdata/https.conf")
s.NoError(err)
s.Equal(string(expect), parser.Dump())
}
func (s *NginxTestSuite) TestHTTPSProtocols() {
parser, err := NewParser()
s.NoError(err)

View File

@@ -216,7 +216,6 @@ func generateProxyConfig(proxy types.Proxy) string {
location = "/"
}
sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass))
sb.WriteString(fmt.Sprintf("location %s {\n", location))
// resolver 配置(如果启用自动刷新)

View File

@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"github.com/acepanel/panel/pkg/webserver/types"
@@ -123,39 +124,17 @@ func (v *baseVhost) SetEnable(enable bool, _ ...string) error {
}
func (v *baseVhost) Listen() []types.Listen {
listens, err := v.parser.GetListen()
directives, err := v.parser.Find("server.listen")
if err != nil {
return nil
}
var result []types.Listen
for _, l := range listens {
if len(l) == 0 {
continue
}
listen := types.Listen{
Address: l[0],
Options: make(map[string]string),
}
// 解析 Nginx 特有的选项
for _, dir := range directives {
l := v.parser.parameters2Slices(dir.GetParameters())
listen := types.Listen{Address: l[0]}
for i := 1; i < len(l); i++ {
switch l[i] {
case "ssl":
listen.Protocol = "https"
case "http2":
listen.Protocol = "http2"
case "http3", "quic":
listen.Protocol = "http3"
default:
listen.Options[l[i]] = "true"
}
}
// 如果没有指定协议,默认为 http
if listen.Protocol == "" {
listen.Protocol = "http"
listen.Args = append(listen.Args, l[i])
}
result = append(result, listen)
@@ -169,26 +148,7 @@ func (v *baseVhost) SetListen(listens []types.Listen) error {
var nginxListens [][]string
for _, l := range listens {
listen := []string{l.Address}
// 添加协议标识
switch l.Protocol {
case "https":
listen = append(listen, "ssl")
case "http2":
listen = append(listen, "http2")
case "http3":
listen = append(listen, "http3")
}
// 添加其他选项
for k, v := range l.Options {
if v == "true" {
listen = append(listen, k)
} else {
listen = append(listen, fmt.Sprintf("%s=%s", k, v))
}
}
listen = append(listen, l.Args...)
nginxListens = append(nginxListens, listen)
}
@@ -196,11 +156,12 @@ func (v *baseVhost) SetListen(listens []types.Listen) error {
}
func (v *baseVhost) ServerName() []string {
names, err := v.parser.GetServerName()
directive, err := v.parser.FindOne("server.server_name")
if err != nil {
return nil
}
return names
return v.parser.parameters2Slices(directive.GetParameters())
}
func (v *baseVhost) SetServerName(serverName []string) error {
@@ -208,11 +169,12 @@ func (v *baseVhost) SetServerName(serverName []string) error {
}
func (v *baseVhost) Index() []string {
index, err := v.parser.GetIndex()
directive, err := v.parser.FindOne("server.index")
if err != nil {
return nil
}
return index
return v.parser.parameters2Slices(directive.GetParameters())
}
func (v *baseVhost) SetIndex(index []string) error {
@@ -294,6 +256,9 @@ func (v *baseVhost) Save() error {
func (v *baseVhost) Reload() error {
parts := strings.Fields("systemctl reload openresty")
if err := exec.Command(parts[0], parts[1:]...).Run(); err != nil {
if testErr := exec.Command("nginx", "-t").Run(); testErr != nil {
return fmt.Errorf("nginx config test failed: %w", testErr)
}
return fmt.Errorf("failed to reload nginx config: %w", err)
}
@@ -317,7 +282,15 @@ func (v *baseVhost) Reset() error {
}
func (v *baseVhost) HTTPS() bool {
return v.parser.GetHTTPS()
directive, err := v.parser.FindOne("server.ssl_certificate")
if err != nil {
return false
}
if len(v.parser.parameters2Slices(directive.GetParameters())) == 0 {
return false
}
return true
}
func (v *baseVhost) SSLConfig() *types.SSLConfig {
@@ -479,11 +452,25 @@ func (v *baseVhost) SetRedirects(redirects []types.Redirect) error {
// ========== PHPVhost ==========
func (v *PHPVhost) PHP() int {
return v.parser.GetPHP()
func (v *PHPVhost) PHP() uint {
directives, err := v.parser.Find("server.include")
if err != nil {
return 0
}
var result uint
for _, dir := range directives {
if slices.ContainsFunc(v.parser.parameters2Slices(dir.GetParameters()), func(s string) bool {
return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf")
}) {
_, _ = fmt.Sscanf(dir.GetParameters()[0].GetValue(), "enable-php-%d.conf", &result)
}
}
return result
}
func (v *PHPVhost) SetPHP(version int) error {
func (v *PHPVhost) SetPHP(version uint) error {
// 先移除所有 PHP 相关的 include
includes := v.Includes()
var newIncludes []types.IncludeFile

View File

@@ -4,21 +4,21 @@ import "time"
// Proxy 反向代理配置
type Proxy struct {
Location string // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/"
AutoRefresh bool // 是否自动刷新解析
Pass string // 代理地址,如: "http://example.com", "http://backend"
Host string // 代理 Host如: "example.com"
SNI string // 代理 SNI如: "example.com"
Cache bool // 是否启用缓存
Buffering bool // 是否启用缓冲
Resolver []string // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"]
ResolverTimeout time.Duration // DNS 解析超时时间,如: 5 * time.Second
Replaces map[string]string // 响应内容替换,如: map["/old"] = "/new"
Location string `form:"location" json:"location" validate:"required"` // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/"
AutoRefresh bool `form:"auto_refresh" json:"auto_refresh"` // 是否自动刷新解析
Pass string `form:"pass" json:"pass" validate:"required"` // 代理地址,如: "http://example.com", "http://backend"
Host string `form:"host" json:"host"` // 代理 Host如: "example.com"
SNI string `form:"sni" json:"sni"` // 代理 SNI如: "example.com"
Cache bool `form:"cache" json:"cache"` // 是否启用缓存
Buffering bool `form:"buffering" json:"buffering"` // 是否启用缓冲
Resolver []string `form:"resolver" json:"resolver"` // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"]
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间,如: 5 * time.Second
Replaces map[string]string `form:"replaces" json:"replaces"` // 响应内容替换,如: map["/old"] = "/new"
}
// Upstream 上游服务器配置
type Upstream struct {
Servers map[string]string // 上游服务器及权重,如: map["server1"] = "weight=5"
Algo string // 负载均衡算法,如: "least_conn", "ip_hash"
Keepalive int // 保持连接数,如: 32
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及权重,如: map["server1"] = "weight=5"
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "ip_hash"
Keepalive int `form:"keepalive" json:"keepalive"` // 保持连接数,如: 32
}

View File

@@ -92,15 +92,15 @@ type PHPVhost interface {
type ProxyVhost interface {
Vhost
VhostRedirect
VhostProxyConfig
VhostProxy
}
// VhostPHP PHP 相关接口
type VhostPHP interface {
// PHP 取 PHP 版本,如: 84, 81, 80, 0 表示未启用 PHP
PHP() int
PHP() uint
// SetPHP 设置 PHP 版本
SetPHP(version int) error
SetPHP(version uint) error
}
// VhostRedirect 重定向相关接口
@@ -111,8 +111,8 @@ type VhostRedirect interface {
SetRedirects(redirects []Redirect) error
}
// VhostProxyConfig 反向代理相关接口
type VhostProxyConfig interface {
// VhostProxy 反向代理相关接口
type VhostProxy interface {
// Proxies 取所有反向代理配置
Proxies() []Proxy
// SetProxies 设置反向代理配置
@@ -130,9 +130,8 @@ type VhostProxyConfig interface {
// Listen 监听配置
type Listen struct {
Address string // 监听地址,如: "80", "0.0.0.0:80", "[::]:443"
Protocol string // 协议类型,如: "http", "https", "http2", "http3"
Options map[string]string // 服务器特定选项,如: map["default_server"] = "true"
Address string // 监听地址,如: "80", "0.0.0.0:80", "[::]:443"
Args []string // 其他参数,如: ["default_server", "ssl", "quic"]
}
// SSLConfig SSL/TLS 配置

View File

@@ -2,7 +2,8 @@ import { http } from '@/utils'
export default {
// 列表
list: (page: number, limit: number): any => http.Get('/website', { params: { page, limit } }),
list: (type: string, page: number, limit: number): any =>
http.Get('/website', { params: { type, page, limit } }),
// 创建
create: (data: any): any => http.Post('/website', data),
// 删除

View File

@@ -162,7 +162,7 @@ const handleSaveWhiteList = () => {
}
const getWebsiteList = async (page: number, limit: number) => {
const data = await website.list(page, limit)
const data = await website.list('all', page, limit)
for (const item of data.items) {
websites.value.push({
label: item.name,

View File

@@ -164,7 +164,7 @@ watch(
onMounted(() => {
useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => {
if (data) {
useRequest(website.list(1, 10000)).onSuccess(({ data }: { data: any }) => {
useRequest(website.list('all', 1, 10000)).onSuccess(({ data }: { data: any }) => {
for (const item of data.items) {
websites.value.push({
label: item.name,

View File

@@ -40,7 +40,7 @@ const getAsyncData = () => {
websites.value = []
useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => {
if (data) {
useRequest(website.list(1, 10000)).onSuccess(({ data }) => {
useRequest(website.list('all', 1, 10000)).onSuccess(({ data }) => {
for (const item of data.items) {
websites.value.push({
label: item.name,

View File

@@ -68,7 +68,7 @@ watch(createModel, (value) => {
onMounted(() => {
useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => {
if (data) {
useRequest(website.list(1, 10000)).onSuccess(({ data }: { data: any }) => {
useRequest(website.list('all', 1, 10000)).onSuccess(({ data }: { data: any }) => {
for (const item of data.items) {
websites.value.push({
label: item.name,

View File

@@ -3,7 +3,7 @@ defineOptions({
name: 'website-index'
})
import PhpView from '@/views/website/PhpView.vue'
import ListView from '@/views/website/ListView.vue'
import SettingView from '@/views/website/SettingView.vue'
const currentTab = ref('proxy')
@@ -19,7 +19,7 @@ const currentTab = ref('proxy')
<n-tab name="setting" :tab="$gettext('Settings')" />
</n-tabs>
</template>
<php-view v-if="currentTab === 'php'" />
<list-view v-if="currentTab != 'setting'" v-model:type="currentTab" />
<setting-view v-if="currentTab === 'setting'" />
</common-page>
</template>

View File

@@ -8,6 +8,8 @@ import { useFileStore } from '@/store'
import { generateRandomString, isNullOrUndef } from '@/utils'
import BulkCreate from '@/views/website/BulkCreate.vue'
const type = defineModel<string>('type', { type: String, required: true }) //
const fileStore = useFileStore()
const { $gettext } = useGettext()
const router = useRouter()
@@ -240,7 +242,7 @@ const { data: installedDbAndPhp } = useRequest(home.installedDbAndPhp, {
})
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => website.list(page, pageSize),
(page, pageSize) => website.list(type.value, page, pageSize),
{
initialData: { total: 0, list: [] },
initialPageSize: 20,