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

refactor: 网站nginx配置解析生成

This commit is contained in:
耗子
2024-10-14 21:34:24 +08:00
parent bdee27541c
commit e8a01e2d04
14 changed files with 488 additions and 477 deletions

View File

@@ -1,6 +1,7 @@
package fail2ban
import (
"fmt"
"net/http"
"regexp"
"strings"
@@ -111,10 +112,12 @@ func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
return
}
var ports string
for _, port := range website.Ports {
fields := strings.Fields(cast.ToString(port))
ports += fields[0] + ","
for _, listen := range website.Listens {
if port, err := cast.ToIntE(listen.Address); err == nil {
ports += fmt.Sprintf("%d", port) + ","
}
}
ports = strings.TrimSuffix(ports, ",")
rule := `
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START

View File

@@ -12,8 +12,7 @@ type Website struct {
Name string `gorm:"not null;unique" json:"name"`
Status bool `gorm:"not null;default:true" json:"status"`
Path string `gorm:"not null" json:"path"`
PHP int `gorm:"not null" json:"php"`
SSL bool `gorm:"not null" json:"ssl"`
HTTPS bool `gorm:"not null" json:"https"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

View File

@@ -4,13 +4,9 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/embed"
@@ -18,8 +14,9 @@ import (
"github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/nginx"
"github.com/TheTNB/panel/pkg/punycode"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
@@ -59,79 +56,72 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
if err := app.Orm.Where("id", id).First(website).Error; err != nil {
return nil, err
}
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
if err != nil {
return nil, err
}
p, err := nginx.NewParser(config)
if err != nil {
return nil, err
}
setting := new(types.WebsiteSetting)
setting.ID = website.ID
setting.Name = website.Name
setting.Path = website.Path
setting.SSL = website.SSL
setting.PHP = website.PHP
setting.HTTPS = website.HTTPS
setting.PHP = p.GetPHP()
setting.Raw = config
portStr := str.Cut(config, "# port标记位开始", "# port标记位结束")
matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1)
for _, match := range matches {
if len(match) < 2 {
// 监听地址
listens, err := p.GetListen()
if err != nil {
return nil, err
}
for listen := range slices.Values(listens) {
if len(listen) == 0 {
continue
}
// 跳过 ipv6
if strings.Contains(match[1], "[::]") {
continue
}
// 处理 443 ssl 之类的情况
ports := strings.Fields(match[1])
if len(ports) == 0 {
continue
}
if !slices.Contains(setting.Ports, cast.ToUint(ports[0])) {
setting.Ports = append(setting.Ports, cast.ToUint(ports[0]))
}
if len(ports) > 1 && ports[1] == "ssl" {
setting.SSLPorts = append(setting.SSLPorts, cast.ToUint(ports[0]))
} else if len(ports) > 1 && ports[1] == "quic" {
setting.QUICPorts = append(setting.QUICPorts, cast.ToUint(ports[0]))
}
setting.Listens = append(setting.Listens, types.WebsiteListen{
Address: listen[0],
HTTPS: slices.Contains(listen, "ssl"),
QUIC: slices.Contains(listen, "quic"),
})
}
serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName)
if len(match) > 1 {
setting.Domains = strings.Split(match[1], " ")
// 域名
domains, err := p.GetServerName()
if err != nil {
return nil, err
}
root := str.Cut(config, "# root标记位开始", "# root标记位结束")
match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root)
if len(match) > 1 {
setting.Root = match[1]
domains, err = punycode.DecodeDomains(domains)
if err != nil {
return nil, err
}
index := str.Cut(config, "# index标记位开始", "# index标记位结束")
match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index)
if len(match) > 1 {
setting.Index = match[1]
}
setting.Domains = domains
// 运行目录
root, _ := p.GetRoot()
setting.Root = root
// 默认文档
index, _ := p.GetIndex()
setting.Index = index
// 防跨站
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
}
}
// HTTPS
if setting.HTTPS {
setting.HTTPRedirect = p.GetHTTPSRedirect()
setting.HSTS = p.GetHSTS()
setting.OCSP = p.GetOCSP()
}
// 证书
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
setting.SSLCertificate = crt
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
setting.SSLCertificateKey = key
if setting.SSL {
ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位")
setting.HSTS = strings.Contains(ssl, "# hsts标记位")
setting.OCSP = strings.Contains(ssl, "# ocsp标记位")
}
// 解析证书信息
if decode, err := cert.ParseCert(crt); err == nil {
setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
@@ -140,9 +130,10 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
setting.SSLOCSPServer = decode.OCSPServer
setting.SSLDNSNames = decode.DNSNames
}
// 伪静态
rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
setting.Rewrite = rewrite
// 访问日志
log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name)
setting.Log = log
@@ -175,21 +166,58 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
}
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
w := &biz.Website{
Name: req.Name,
Status: true,
Path: req.Path,
PHP: req.PHP,
SSL: false,
// 初始化nginx配置
p, err := nginx.NewParser()
if err != nil {
return nil, err
}
if err := app.Orm.Create(w).Error; err != nil {
// 监听地址
var listens [][]string
for _, listen := range req.Listens {
listens = append(listens, []string{listen}) // ipv4
listens = append(listens, []string{"[::]:" + listen}) // ipv6
}
if err = p.SetListen(listens); err != nil {
return nil, err
}
// 域名
domains, err := punycode.EncodeDomains(req.Domains)
if err != nil {
return nil, err
}
if err = p.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
}
// 伪静态
includes, comments, err := p.GetIncludes()
if err != nil {
return nil, err
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"))
comments = append(comments, []string{"# 伪静态规则"})
if err = p.SetIncludes(includes, comments); err != nil {
return nil, err
}
// 日志
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil {
return nil, err
}
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil {
return nil, err
}
if err := io.Mkdir(req.Path, 0755); err != nil {
// 初始化网站目录
if err = io.Mkdir(req.Path, 0755); err != nil {
return nil, err
}
index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
if err != nil {
return nil, fmt.Errorf("获取index模板文件失败: %w", err)
@@ -197,100 +225,21 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil {
return nil, err
}
notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
if err != nil {
return nil, fmt.Errorf("获取404模板文件失败: %w", err)
}
if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil {
if err = io.Write(filepath.Join(req.Path, "404.html"), string(notFound), 0644); err != nil {
return nil, err
}
portList := ""
domainList := ""
portUsed := make(map[uint]bool)
domainUsed := make(map[string]bool)
for i, port := range req.Ports {
if _, ok := portUsed[port]; !ok {
if i == len(req.Ports)-1 {
portList += " listen " + cast.ToString(port) + ";\n"
portList += " listen [::]:" + cast.ToString(port) + ";"
} else {
portList += " listen " + cast.ToString(port) + ";\n"
portList += " listen [::]:" + cast.ToString(port) + ";\n"
}
portUsed[port] = true
}
}
for _, domain := range req.Domains {
if _, ok := domainUsed[domain]; !ok {
domainList += " " + domain
domainUsed[domain] = true
}
}
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
%s
# port标记位结束
# server_name标记位开始
server_name%s;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# 错误页配置,可自行设置
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置不可修改
include %s/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include %s/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log %s/wwwlogs/%s.log;
error_log %s/wwwlogs/%s.log;
}
`, portList, domainList, req.Path, req.PHP, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name)
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil {
// 写nginx配置
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); 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 {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil {
return nil, err
}
@@ -298,6 +247,7 @@ server
return nil, err
}
// 设置目录权限
if err = io.Chmod(req.Path, 0755); err != nil {
return nil, err
}
@@ -305,11 +255,23 @@ server
return nil, err
}
// 创建面板网站
w := &biz.Website{
Name: req.Name,
Status: true,
Path: req.Path,
HTTPS: false,
}
if err = app.Orm.Create(w).Error; err != nil {
return nil, err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return nil, err
}
// 创建数据库
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
if err == nil && req.DB && req.DBType == "mysql" {
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
@@ -344,17 +306,17 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil {
return err
}
if !website.Status {
return errors.New("网站已停用,请先启用")
}
// 原文
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
if err != nil {
return err
}
if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) {
// 如果修改了原文,直接写入返回
if strings.TrimSpace(config) != strings.TrimSpace(req.Raw) {
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil {
return err
}
@@ -362,188 +324,129 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
_, err = shell.Execf("nginx -t")
return err
}
return nil
}
// 目录
path := req.Path
if !io.Exists(path) {
// 初始化nginx配置
p, err := nginx.NewParser(config)
if err != nil {
return err
}
// 监听地址
var listens [][]string
for _, listen := range req.Listens {
listens = append(listens, []string{listen.Address})
if listen.HTTPS {
listens = append(listens, []string{listen.Address, "ssl"})
}
if listen.QUIC {
listens = append(listens, []string{listen.Address, "quic"})
}
}
if err = p.SetListen(listens); err != nil {
return err
}
// 域名
domains, err := punycode.EncodeDomains(req.Domains)
if err != nil {
return err
}
if err = p.SetServerName(domains); err != nil {
return err
}
// 首页文件
if err = p.SetIndex(req.Index); err != nil {
return err
}
// 运行目录
if !io.Exists(req.Root) {
return errors.New("运行目录不存在")
}
if err = p.SetRoot(req.Root); err != nil {
return err
}
// 运行目录
if !io.Exists(req.Path) {
return errors.New("网站目录不存在")
}
website.Path = path
// 域名
domain := "server_name"
domains := req.Domains
for _, v := range domains {
if v == "" {
continue
website.Path = req.Path
// PHP
if err = p.SetPHP(req.PHP); err != nil {
return err
}
// HTTPS
certPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")
keyPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")
if err = io.Write(certPath, req.SSLCertificate, 0644); err != nil {
return err
}
if err = io.Write(keyPath, req.SSLCertificateKey, 0644); err != nil {
return err
}
website.HTTPS = req.HTTPS
if req.HTTPS {
if err = p.SetHTTPS(certPath, keyPath); err != nil {
return err
}
domain += " " + v
}
domain += ";"
domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
if len(strings.TrimSpace(domainConfigOld)) == 0 {
return errors.New("配置文件中缺少server_name标记位")
}
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
// 端口
var portConf strings.Builder
ports := req.Ports
for _, port := range ports {
https := ""
quic := false
if slices.Contains(req.SSLPorts, port) {
https = " ssl"
if slices.Contains(req.QUICPorts, port) {
quic = true
}
if err = p.SetHTTPRedirect(req.HTTPRedirect); err != nil {
return err
}
if err = p.SetHSTS(req.HSTS); err != nil {
return err
}
if err = p.SetOCSP(req.OCSP); err != nil {
return err
}
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"))
} else {
if err = p.ClearSetHTTPS(); err != nil {
return err
}
if err = p.SetHTTPRedirect(false); err != nil {
return err
}
if err = p.SetHSTS(false); err != nil {
return err
}
if err = p.SetOCSP(false); err != nil {
return err
}
}
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, portConfOld, "\n"+portConfNew, -1)
// 运行目录
root := str.Cut(raw, "# root标记位开始", "# root标记位结束")
if len(strings.TrimSpace(root)) == 0 {
return errors.New("配置文件中缺少root标记位")
}
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
if len(match) != 2 {
return errors.New("配置文件中root标记位格式错误")
}
rootNew := strings.Replace(root, match[1], req.Root, -1)
raw = strings.Replace(raw, root, rootNew, -1)
// 默认文件
index := str.Cut(raw, "# index标记位开始", "# index标记位结束")
if len(strings.TrimSpace(index)) == 0 {
return errors.New("配置文件中缺少index标记位")
}
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
if len(match) != 2 {
return errors.New("配置文件中index标记位格式错误")
}
indexNew := strings.Replace(index, match[1], req.Index, -1)
raw = strings.Replace(raw, index, indexNew, -1)
// 防跨站
if !strings.HasSuffix(req.Root, "/") {
req.Root += "/"
}
userIni := filepath.Join(req.Root, ".user.ini")
if req.OpenBasedir {
if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil {
if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Root), 0644); err != nil {
return err
}
_, _ = shell.Execf(`chattr +i '%s'`, userIni)
} else {
if io.Exists(req.Root + ".user.ini") {
if err = io.Remove(req.Root + ".user.ini"); err != nil {
if io.Exists(userIni) {
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
if err = io.Remove(userIni); err != nil {
return err
}
}
}
// SSL
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil {
return err
}
website.SSL = req.SSL
if req.SSL {
if _, err = cert.ParseCert(req.SSLCertificate); err != nil {
return errors.New("TLS证书格式错误")
}
if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil {
return errors.New("TLS私钥格式错误")
}
sslConfig := fmt.Sprintf(`# ssl标记位开始
ssl_certificate %s/server/vhost/ssl/%s.pem;
ssl_certificate_key %s/server/vhost/ssl/%s.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_early_data on;
`, app.Root, website.Name, app.Root, website.Name)
if req.HTTPRedirect {
sslConfig += `# http重定向标记位开始
if ($server_port !~ 443){
return 301 https://$host$request_uri;
}
error_page 497 https://$host$request_uri;
# http重定向标记位结束
`
}
if req.HSTS {
sslConfig += `# hsts标记位开始
add_header Strict-Transport-Security "max-age=63072000" always;
# hsts标记位结束
`
}
if req.OCSP {
sslConfig += `# ocsp标记位开始
ssl_stapling on;
ssl_stapling_verify on;
# ocsp标记位结束
`
}
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "", -1)
}
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
} else {
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
}
}
if website.PHP != req.PHP {
website.PHP = req.PHP
phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束")
phpConfig := `
include enable-php-` + strconv.Itoa(website.PHP) + `.conf;
`
if len(strings.TrimSpace(phpConfigOld)) != 0 {
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
}
}
if err = app.Orm.Save(website).Error; err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil {
return err
}
err = systemctl.Reload("nginx")
if err != nil {
if err = app.Orm.Save(website).Error; err != nil {
return err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
}
return err
return nil
}
func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
@@ -551,15 +454,10 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil {
return err
}
if website.Cert != nil {
return errors.New("网站" + website.Name + "已绑定SSL证书请先删除证书")
}
if err := app.Orm.Delete(website).Error; err != nil {
return err
}
_ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
@@ -584,12 +482,15 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
}
err := systemctl.Reload("nginx")
if err != nil {
if err := app.Orm.Delete(website).Error; err != nil {
return err
}
if err := systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
}
return err
return nil
}
func (r *websiteRepo) ClearLog(id uint) error {
@@ -618,75 +519,47 @@ func (r *websiteRepo) ResetConfig(id uint) error {
return err
}
// 初始化nginx配置
p, err := nginx.NewParser()
if err != nil {
return err
}
// 运行目录
if err = p.SetRoot(website.Path); err != nil {
return err
}
// 伪静态
includes, comments, err := p.GetIncludes()
if err != nil {
return err
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
comments = append(comments, []string{"# 伪静态规则"})
if err = p.SetIncludes(includes, comments); err != nil {
return err
}
// 日志
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
return err
}
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log")); err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
return nil
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
return nil
}
website.Status = true
website.SSL = false
website.HTTPS = false
if err := app.Orm.Save(website).Error; err != nil {
return err
}
raw := fmt.Sprintf(`
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
listen 80;
# port标记位结束
# server_name标记位开始
server_name localhost;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# 错误页配置,可自行设置
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置不可修改
include %s/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include %s/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log %s/wwwlogs/%s.log;
error_log %s/wwwlogs/%s.log;
}
`, website.Path, website.PHP, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name)
if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
return nil
}
if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
return nil
}
if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
return nil
}
if err := systemctl.Reload("nginx"); err != nil {
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
}
@@ -700,43 +573,61 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
return err
}
website.Status = status
if err := app.Orm.Save(website).Error; err != nil {
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
if err != nil {
return err
}
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
p, err := nginx.NewParser(config)
if err != nil {
return err
}
// 运行目录
rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束")
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
if len(match) == 2 {
if website.Status {
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1]))
} else {
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/nginx/html;\n # root %s;\n ", app.Root, match[1]))
}
}
// 默认文件
indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
if len(match) == 2 {
if website.Status {
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
} else {
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
}
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
// 运行目录和默认文档
root, rootComment, err := p.GetRootWithComment()
if err != nil {
return err
}
index, indexComment, err := p.GetIndexWithComment()
if err != nil {
return err
}
indexStr := strings.Join(index, " ")
if status {
if len(rootComment) == 0 {
return fmt.Errorf("未找到运行目录注释")
}
if !io.Exists(rootComment[0]) {
return fmt.Errorf("运行目录不存在")
}
if err = p.SetRoot(rootComment[0]); err != nil {
return err
}
if len(indexComment) == 0 {
return fmt.Errorf("未找到默认文档注释")
}
if err = p.SetIndex(strings.Fields(indexStr)); err != nil {
return err
}
} else {
if err = p.SetRootWithComment(filepath.Join(app.Root, "server/nginx/html"), []string{root}); err != nil {
return err
}
if err = p.SetIndexWithComment([]string{"stop.html"}, []string{indexStr}); err != nil {
return err
}
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
return err
}
website.Status = status
if err = app.Orm.Save(website).Error; err != nil {
return err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err

View File

@@ -1,5 +1,7 @@
package request
import "github.com/TheTNB/panel/pkg/types"
type WebsiteDefaultConfig struct {
Index string `json:"index" form:"index" validate:"required"`
Stop string `json:"stop" form:"stop" validate:"required"`
@@ -7,10 +9,10 @@ type WebsiteDefaultConfig struct {
type WebsiteCreate struct {
Name string `form:"name" json:"name" validate:"required"`
Listens []string `form:"listens" json:"listens" validate:"required"`
Domains []string `form:"domains" json:"domains" validate:"required"`
Ports []uint `form:"ports" json:"ports" validate:"required"`
Path string `form:"path" json:"path"`
PHP int `form:"php" json:"php"`
PHP int `form:"php" json:"php" validate:"required,number,gte=0"`
DB bool `form:"db" json:"db"`
DBType string `form:"db_type" json:"db_type"`
DBName string `form:"db_name" json:"db_name"`
@@ -25,24 +27,22 @@ type WebsiteDelete struct {
}
type WebsiteUpdate struct {
ID uint `form:"id" json:"id" validate:"required"`
Domains []string `form:"domains" json:"domains" validate:"required"`
Listens []string `form:"listens" json:"listens" validate:"required"`
SSLPorts []uint `form:"ssl_ports" json:"ssl_ports" validate:"required"`
QUICPorts []uint `form:"quic_ports" json:"quic_ports" validate:"required"`
OCSP bool `form:"ocsp" json:"ocsp"`
HSTS bool `form:"hsts" json:"hsts"`
SSL bool `form:"ssl" json:"ssl"`
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
Index string `form:"index" json:"index" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Root string `form:"root" json:"root" validate:"required"`
Raw string `form:"raw" json:"raw"`
Rewrite string `form:"rewrite" json:"rewrite"`
PHP int `form:"php" json:"php"`
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
ID uint `form:"id" json:"id" validate:"required"`
Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"required"`
Domains []string `form:"domains" json:"domains" validate:"required"`
HTTPS bool `form:"https" json:"https"`
OCSP bool `form:"ocsp" json:"ocsp"`
HSTS bool `form:"hsts" json:"hsts"`
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
Index []string `form:"index" json:"index" validate:"required"`
Path string `form:"path" json:"path" validate:"required"` // 网站目录
Root string `form:"root" json:"root" validate:"required"` // 运行目录
Raw string `form:"raw" json:"raw"`
Rewrite string `form:"rewrite" json:"rewrite"`
PHP int `form:"php" json:"php"`
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
}
type WebsiteUpdateRemark struct {

View File

@@ -108,15 +108,15 @@ func Cli() []*cli.Command {
Required: true,
},
&cli.StringSliceFlag{
Name: "domains",
Name: "domain",
Usage: "与网站关联的域名列表",
Aliases: []string{"d"},
Required: true,
},
&cli.UintSliceFlag{
Name: "ports",
Usage: "网站使用的端口列表",
Aliases: []string{"p"},
&cli.StringSliceFlag{
Name: "listen",
Usage: "网站关联的监听地址列表",
Aliases: []string{"l"},
Required: true,
},
&cli.StringFlag{

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"path/filepath"
"slices"
"time"
"github.com/go-rat/utils/hash"
@@ -350,16 +351,17 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error {
var ports []uint
for _, port := range cmd.IntSlice("ports") {
for port := range slices.Values(cmd.IntSlice("ports")) {
if port < 1 || port > 65535 {
return fmt.Errorf("端口范围错误")
}
ports = append(ports, uint(port))
}
req := &request.WebsiteCreate{
Name: cmd.String("name"),
Domains: cmd.StringSlice("domains"),
Ports: ports,
Listens: cmd.StringSlice("listen"),
Path: cmd.String("path"),
PHP: int(cmd.Int("php")),
DB: false,

View File

@@ -11,8 +11,6 @@ const defaultConf = `server {
root /www/wwwroot/default;
# 错误页配置
error_page 404 /404.html;
# 伪静态规则
include /www/server/vhost/rewrite/default.conf;
include enable-php-0.conf;
# 不记录静态文件日志
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {

View File

@@ -38,6 +38,15 @@ func (p *Parser) GetIndex() ([]string, error) {
return directive.GetParameters(), nil
}
func (p *Parser) GetIndexWithComment() ([]string, []string, error) {
directive, err := p.FindOne("server.index")
if err != nil {
return nil, nil, err
}
return directive.GetParameters(), directive.GetComment(), nil
}
func (p *Parser) GetRoot() (string, error) {
directive, err := p.FindOne("server.root")
if err != nil {
@@ -50,6 +59,18 @@ func (p *Parser) GetRoot() (string, error) {
return directive.GetParameters()[0], nil
}
func (p *Parser) GetRootWithComment() (string, []string, error) {
directive, err := p.FindOne("server.root")
if err != nil {
return "", nil, err
}
if len(directive.GetParameters()) == 0 {
return "", directive.GetComment(), nil
}
return directive.GetParameters()[0], directive.GetComment(), nil
}
func (p *Parser) GetIncludes() (includes []string, comments [][]string, err error) {
directives, err := p.Find("server.include")
if err != nil {
@@ -67,10 +88,10 @@ func (p *Parser) GetIncludes() (includes []string, comments [][]string, err erro
return includes, comments, nil
}
func (p *Parser) GetPHP() (int, error) {
func (p *Parser) GetPHP() int {
directives, err := p.Find("server.include")
if err != nil {
return 0, err
return 0
}
var result int
@@ -82,7 +103,7 @@ func (p *Parser) GetPHP() (int, error) {
}
}
return result, err
return result
}
func (p *Parser) GetHTTPS() bool {
@@ -145,21 +166,21 @@ func (p *Parser) GetHSTS() bool {
return false
}
func (p *Parser) GetHTTPSRedirect() (bool, error) {
func (p *Parser) GetHTTPSRedirect() bool {
directives, err := p.Find("server.if")
if err != nil {
return false, err
return false
}
for _, dir := range directives {
for _, dir2 := range dir.GetBlock().GetDirectives() {
if dir2.GetName() == "return" && slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
return true, nil
return true
}
}
}
return false, nil
return false
}
func (p *Parser) GetAccessLog() (string, error) {

View File

@@ -68,8 +68,8 @@ func (s *NginxTestSuite) TestIncludes() {
s.NoError(err)
includes, comments, err := parser.GetIncludes()
s.NoError(err)
s.Equal([]string{"/www/server/vhost/rewrite/default.conf", "enable-php-0.conf"}, includes)
s.Equal([][]string{{"# 伪静态规则"}, []string(nil)}, comments)
s.Equal([]string{"enable-php-0.conf"}, includes)
s.Equal([][]string{[]string(nil)}, comments)
s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/default.conf"}, nil))
includes, comments, err = parser.GetIncludes()
s.NoError(err)
@@ -82,6 +82,16 @@ 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)
@@ -126,6 +136,8 @@ func (s *NginxTestSuite) TestOCSP() {
s.NoError(err)
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
s.False(parser.GetOCSP())
s.NoError(parser.SetOCSP(false))
s.False(parser.GetOCSP())
s.NoError(parser.SetOCSP(true))
s.True(parser.GetOCSP())
s.NoError(parser.SetOCSP(false))
@@ -137,6 +149,8 @@ func (s *NginxTestSuite) TestHSTS() {
s.NoError(err)
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
s.False(parser.GetHSTS())
s.NoError(parser.SetHSTS(false))
s.False(parser.GetHSTS())
s.NoError(parser.SetHSTS(true))
s.True(parser.GetHSTS())
s.NoError(parser.SetHSTS(false))
@@ -148,9 +162,11 @@ func (s *NginxTestSuite) TestHTTPSRedirect() {
s.NoError(err)
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
s.False(parser.GetHTTPSRedirect())
s.NoError(parser.SetHTTPSRedirect(true))
s.NoError(parser.SetHTTPRedirect(false))
s.False(parser.GetHTTPSRedirect())
s.NoError(parser.SetHTTPRedirect(true))
s.True(parser.GetHTTPSRedirect())
s.NoError(parser.SetHTTPSRedirect(false))
s.NoError(parser.SetHTTPRedirect(false))
s.False(parser.GetHTTPSRedirect())
}

View File

@@ -50,6 +50,20 @@ func (p *Parser) SetIndex(index []string) error {
})
}
func (p *Parser) SetIndexWithComment(index []string, comment []string) error {
if err := p.Clear("server.index"); err != nil {
return err
}
return p.Set("server", []*config.Directive{
{
Name: "index",
Parameters: index,
Comment: comment,
},
})
}
func (p *Parser) SetRoot(root string) error {
if err := p.Clear("server.root"); err != nil {
return err
@@ -63,6 +77,20 @@ func (p *Parser) SetRoot(root string) error {
})
}
func (p *Parser) SetRootWithComment(root string, comment []string) error {
if err := p.Clear("server.root"); err != nil {
return err
}
return p.Set("server", []*config.Directive{
{
Name: "root",
Parameters: []string{root},
Comment: comment,
},
})
}
func (p *Parser) SetIncludes(includes []string, comments [][]string) error {
if err := p.Clear("server.include"); err != nil {
return err
@@ -127,7 +155,7 @@ func (p *Parser) SetPHP(php int) error {
return p.Set("server", directives)
}
func (p *Parser) UnSetHTTPS() error {
func (p *Parser) ClearSetHTTPS() error {
if err := p.Clear("server.ssl_certificate"); err != nil {
return err
}
@@ -157,7 +185,7 @@ func (p *Parser) UnSetHTTPS() error {
}
func (p *Parser) SetHTTPS(cert, key string) error {
if err := p.UnSetHTTPS(); err != nil {
if err := p.ClearSetHTTPS(); err != nil {
return err
}
@@ -287,7 +315,7 @@ func (p *Parser) SetHSTS(hsts bool) error {
return p.Set("server", directives)
}
func (p *Parser) SetHTTPSRedirect(httpRedirect bool) error {
func (p *Parser) SetHTTPRedirect(httpRedirect bool) error {
// if 重定向
ifs, err := p.Find("server.if")
if err != nil {

View File

@@ -5,8 +5,6 @@ server {
root /www/wwwroot/default;
# 错误页配置
error_page 404 /404.html;
# 伪静态规则
include /www/server/vhost/rewrite/default.conf;
include enable-php-0.conf;
# 不记录静态文件日志
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {

View File

@@ -13,8 +13,6 @@ server {
ssl_early_data on;
# 错误页配置
error_page 404 /404.html;
# 伪静态规则
include /www/server/vhost/rewrite/default.conf;
include enable-php-0.conf;
# 不记录静态文件日志
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {

52
pkg/punycode/punycode.go Normal file
View File

@@ -0,0 +1,52 @@
package punycode
import (
"fmt"
"slices"
"golang.org/x/net/idna"
)
// EncodeDomain 将 Unicode 域名编码为 Punycode
func EncodeDomain(domain string) (string, error) {
ascii, err := idna.ToASCII(domain)
if err != nil {
return "", fmt.Errorf("domain encode failed: %w", err)
}
return ascii, nil
}
// EncodeDomains 将 Unicode 域名列表编码为 Punycode
func EncodeDomains(domain []string) (encoded []string, err error) {
var punycode string
for item := range slices.Values(domain) {
punycode, err = EncodeDomain(item)
if err != nil {
return nil, err
}
encoded = append(encoded, punycode)
}
return encoded, nil
}
// DecodeDomain 将 Punycode 域名解码为 Unicode 域名
func DecodeDomain(punycodeDomain string) (string, error) {
unicode, err := idna.ToUnicode(punycodeDomain)
if err != nil {
return "", fmt.Errorf("domain decode failed: %w", err)
}
return unicode, nil
}
// DecodeDomains 将 Punycode 域名列表解码为 Unicode 域名
func DecodeDomains(punycode []string) (decoded []string, err error) {
var unicode string
for item := range slices.Values(punycode) {
unicode, err = DecodeDomain(item)
if err != nil {
return nil, err
}
decoded = append(decoded, unicode)
}
return decoded, nil
}

View File

@@ -1,30 +1,35 @@
package types
// WebsiteListen 网站监听配置
type WebsiteListen struct {
Address string `form:"address" json:"address" validate:"required"` // 监听地址 e.g. 80 0.0.0.0:80 [::]:80
HTTPS bool `form:"https" json:"https" validate:"required"` // 是否启用HTTPS
QUIC bool `form:"quic" json:"quic"` // 是否启用QUIC
}
// WebsiteSetting 网站设置
type WebsiteSetting struct {
ID uint `json:"id"`
Name string `json:"name"`
Domains []string `json:"domains"`
Ports []uint `json:"ports"`
SSLPorts []uint `json:"ssl_ports"`
QUICPorts []uint `json:"quic_ports"`
Root string `json:"root"`
Path string `json:"path"`
Index string `json:"index"`
PHP int `json:"php"`
OpenBasedir bool `json:"open_basedir"`
SSL bool `json:"ssl"`
SSLCertificate string `json:"ssl_certificate"`
SSLCertificateKey string `json:"ssl_certificate_key"`
SSLNotBefore string `json:"ssl_not_before"`
SSLNotAfter string `json:"ssl_not_after"`
SSLDNSNames []string `json:"ssl_dns_names"`
SSLIssuer string `json:"ssl_issuer"`
SSLOCSPServer []string `json:"ssl_ocsp_server"`
HTTPRedirect bool `json:"http_redirect"`
HSTS bool `json:"hsts"`
OCSP bool `json:"ocsp"`
Rewrite string `json:"rewrite"`
Raw string `json:"raw"`
Log string `json:"log"`
ID uint `json:"id"`
Name string `json:"name"`
Listens []WebsiteListen `form:"listens" json:"listens" validate:"required"`
Domains []string `json:"domains"`
Path string `json:"path"` // 网站目录
Root string `json:"root"` // 运行目录
Index []string `json:"index"`
PHP int `json:"php"`
OpenBasedir bool `json:"open_basedir"`
HTTPS bool `json:"https"`
SSLCertificate string `json:"ssl_certificate"`
SSLCertificateKey string `json:"ssl_certificate_key"`
SSLNotBefore string `json:"ssl_not_before"`
SSLNotAfter string `json:"ssl_not_after"`
SSLDNSNames []string `json:"ssl_dns_names"`
SSLIssuer string `json:"ssl_issuer"`
SSLOCSPServer []string `json:"ssl_ocsp_server"`
HTTPRedirect bool `json:"http_redirect"`
HSTS bool `json:"hsts"`
OCSP bool `json:"ocsp"`
Rewrite string `json:"rewrite"`
Raw string `json:"raw"`
Log string `json:"log"`
}