mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 11:27:17 +08:00
feat: 配置解析器优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package apache
|
||||
|
||||
// DisableConfName 禁用配置文件名
|
||||
const DisableConfName = "00-disable.conf"
|
||||
const DisableConfName = "000-disable.conf"
|
||||
|
||||
// DisableConfContent 禁用配置内容
|
||||
const DisableConfContent = `# 网站已停止
|
||||
@@ -9,6 +9,14 @@ RewriteEngine on
|
||||
RewriteRule ^.*$ - [R=503,L]
|
||||
`
|
||||
|
||||
// 配置文件序号范围
|
||||
const (
|
||||
RedirectStartNum = 100 // 重定向配置起始序号 (100-199)
|
||||
RedirectEndNum = 199
|
||||
ProxyStartNum = 200 // 代理配置起始序号 (200-299)
|
||||
ProxyEndNum = 299
|
||||
)
|
||||
|
||||
// DefaultVhostConf 默认配置模板
|
||||
const DefaultVhostConf = `<VirtualHost *:80>
|
||||
ServerName localhost
|
||||
@@ -19,7 +27,7 @@ const DefaultVhostConf = `<VirtualHost *:80>
|
||||
CustomLog /opt/ace/sites/default/log/access.log combined
|
||||
|
||||
# custom configs
|
||||
IncludeOptional /opt/ace/sites/default/config/server.d/*.conf
|
||||
IncludeOptional /opt/ace/sites/default/config/vhost/*.conf
|
||||
|
||||
<Directory /opt/ace/sites/default/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
|
||||
433
pkg/webserver/apache/proxy.go
Normal file
433
pkg/webserver/apache/proxy.go
Normal file
@@ -0,0 +1,433 @@
|
||||
package apache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// proxyFilePattern 匹配代理配置文件名 (200-299)
|
||||
var proxyFilePattern = regexp.MustCompile(`^(\d{3})-proxy\.conf$`)
|
||||
|
||||
// balancerFilePattern 匹配负载均衡配置文件名
|
||||
var balancerFilePattern = regexp.MustCompile(`^(\d{3})-balancer-(.+)\.conf$`)
|
||||
|
||||
// parseProxyFiles 从 vhost 目录解析所有代理配置
|
||||
func parseProxyFiles(vhostDir string) ([]types.Proxy, error) {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var proxies []types.Proxy
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := proxyFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num < ProxyStartNum || num > ProxyEndNum {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
proxy, err := parseProxyFile(filePath)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if proxy != nil {
|
||||
proxies = append(proxies, *proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
// parseProxyFile 解析单个代理配置文件
|
||||
func parseProxyFile(filePath string) (*types.Proxy, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
proxy := &types.Proxy{
|
||||
Replaces: make(map[string]string),
|
||||
}
|
||||
|
||||
// 解析 ProxyPass 指令
|
||||
// ProxyPass / http://backend/
|
||||
proxyPassPattern := regexp.MustCompile(`ProxyPass\s+(\S+)\s+(\S+)`)
|
||||
if matches := proxyPassPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
proxy.Location = matches[1]
|
||||
proxy.Pass = matches[2]
|
||||
}
|
||||
|
||||
// 解析 ProxyPreserveHost
|
||||
if regexp.MustCompile(`ProxyPreserveHost\s+On`).MatchString(contentStr) {
|
||||
// Host 由客户端提供
|
||||
}
|
||||
|
||||
// 解析 RequestHeader set Host
|
||||
hostPattern := regexp.MustCompile(`RequestHeader\s+set\s+Host\s+"([^"]+)"`)
|
||||
if matches := hostPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
proxy.Host = matches[1]
|
||||
}
|
||||
|
||||
// 解析 SSLProxyEngine 和 ProxySSL* (SNI)
|
||||
if regexp.MustCompile(`SSLProxyEngine\s+On`).MatchString(contentStr) {
|
||||
// 尝试获取 SNI
|
||||
sniPattern := regexp.MustCompile(`ProxyPassMatch.*ssl:([^/\s]+)`)
|
||||
if sm := sniPattern.FindStringSubmatch(contentStr); sm != nil {
|
||||
proxy.SNI = sm[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 ProxyIOBufferSize (buffering)
|
||||
if regexp.MustCompile(`ProxyIOBufferSize`).MatchString(contentStr) {
|
||||
proxy.Buffering = true
|
||||
}
|
||||
|
||||
// 解析 CacheEnable
|
||||
if regexp.MustCompile(`CacheEnable`).MatchString(contentStr) {
|
||||
proxy.Cache = true
|
||||
}
|
||||
|
||||
// 解析 ProxyTimeout (resolver timeout)
|
||||
timeoutPattern := regexp.MustCompile(`ProxyTimeout\s+(\d+)`)
|
||||
if tm := timeoutPattern.FindStringSubmatch(contentStr); tm != nil {
|
||||
timeout, _ := strconv.Atoi(tm[1])
|
||||
proxy.ResolverTimeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
|
||||
// 解析 Substitute (响应内容替换)
|
||||
subPattern := regexp.MustCompile(`Substitute\s+"s/([^/]+)/([^/]*)/[gin]*"`)
|
||||
subMatches := subPattern.FindAllStringSubmatch(contentStr, -1)
|
||||
for _, sm := range subMatches {
|
||||
proxy.Replaces[sm[1]] = sm[2]
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// writeProxyFiles 将代理配置写入文件
|
||||
func writeProxyFiles(vhostDir string, proxies []types.Proxy) error {
|
||||
// 删除现有的代理配置文件 (200-299)
|
||||
if err := clearProxyFiles(vhostDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
for i, proxy := range proxies {
|
||||
num := ProxyStartNum + i
|
||||
if num > ProxyEndNum {
|
||||
return fmt.Errorf("proxy rules exceed limit (%d)", ProxyEndNum-ProxyStartNum+1)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%03d-proxy.conf", num)
|
||||
filePath := filepath.Join(vhostDir, fileName)
|
||||
|
||||
content := generateProxyConfig(proxy)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write proxy config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearProxyFiles 清除所有代理配置文件
|
||||
func clearProxyFiles(vhostDir string) error {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := proxyFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num >= ProxyStartNum && num <= ProxyEndNum {
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete proxy config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateProxyConfig 生成代理配置内容
|
||||
func generateProxyConfig(proxy types.Proxy) string {
|
||||
var sb strings.Builder
|
||||
|
||||
location := proxy.Location
|
||||
if location == "" {
|
||||
location = "/"
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass))
|
||||
|
||||
// 启用代理模块
|
||||
sb.WriteString("<IfModule mod_proxy.c>\n")
|
||||
|
||||
// ProxyPass 和 ProxyPassReverse
|
||||
sb.WriteString(fmt.Sprintf(" ProxyPass %s %s\n", location, proxy.Pass))
|
||||
sb.WriteString(fmt.Sprintf(" ProxyPassReverse %s %s\n", location, proxy.Pass))
|
||||
|
||||
// Host 配置
|
||||
if proxy.Host != "" {
|
||||
sb.WriteString(fmt.Sprintf(" RequestHeader set Host \"%s\"\n", proxy.Host))
|
||||
} else {
|
||||
sb.WriteString(" ProxyPreserveHost On\n")
|
||||
}
|
||||
|
||||
// 标准代理头
|
||||
sb.WriteString(" RequestHeader set X-Real-IP \"%{REMOTE_ADDR}e\"\n")
|
||||
sb.WriteString(" RequestHeader set X-Forwarded-For \"%{X-Forwarded-For}e\"\n")
|
||||
sb.WriteString(" RequestHeader set X-Forwarded-Proto \"%{REQUEST_SCHEME}e\"\n")
|
||||
|
||||
// SSL/SNI 配置
|
||||
if proxy.SNI != "" || strings.HasPrefix(proxy.Pass, "https://") {
|
||||
sb.WriteString(" SSLProxyEngine On\n")
|
||||
sb.WriteString(" SSLProxyVerify none\n")
|
||||
sb.WriteString(" SSLProxyCheckPeerCN off\n")
|
||||
sb.WriteString(" SSLProxyCheckPeerName off\n")
|
||||
}
|
||||
|
||||
// Buffering 配置
|
||||
if proxy.Buffering {
|
||||
sb.WriteString(" ProxyIOBufferSize 65536\n")
|
||||
}
|
||||
|
||||
// Timeout 配置
|
||||
if proxy.ResolverTimeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" ProxyTimeout %d\n", int(proxy.ResolverTimeout.Seconds())))
|
||||
}
|
||||
|
||||
// Cache 配置
|
||||
if proxy.Cache {
|
||||
sb.WriteString(" <IfModule mod_cache.c>\n")
|
||||
sb.WriteString(fmt.Sprintf(" CacheEnable disk %s\n", location))
|
||||
sb.WriteString(" CacheDefaultExpire 600\n")
|
||||
sb.WriteString(" </IfModule>\n")
|
||||
}
|
||||
|
||||
// 响应内容替换
|
||||
if len(proxy.Replaces) > 0 {
|
||||
sb.WriteString(" <IfModule mod_substitute.c>\n")
|
||||
sb.WriteString(" AddOutputFilterByType SUBSTITUTE text/html text/plain text/xml\n")
|
||||
for from, to := range proxy.Replaces {
|
||||
sb.WriteString(fmt.Sprintf(" Substitute \"s/%s/%s/n\"\n", from, to))
|
||||
}
|
||||
sb.WriteString(" </IfModule>\n")
|
||||
}
|
||||
|
||||
sb.WriteString("</IfModule>\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// parseBalancerFiles 从 global 目录解析所有负载均衡配置(Apache 的 upstream 等价物)
|
||||
func parseBalancerFiles(globalDir string) (map[string]types.Upstream, error) {
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreams := make(map[string]types.Upstream)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := balancerFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := matches[2]
|
||||
filePath := filepath.Join(globalDir, entry.Name())
|
||||
upstream, err := parseBalancerFile(filePath)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if upstream != nil {
|
||||
upstreams[name] = *upstream
|
||||
}
|
||||
}
|
||||
|
||||
return upstreams, nil
|
||||
}
|
||||
|
||||
// parseBalancerFile 解析单个负载均衡配置文件
|
||||
func parseBalancerFile(filePath string) (*types.Upstream, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
upstream := &types.Upstream{
|
||||
Servers: make(map[string]string),
|
||||
}
|
||||
|
||||
// 解析 <Proxy balancer://name> 块
|
||||
// <Proxy balancer://backend>
|
||||
// BalancerMember http://127.0.0.1:8080 loadfactor=5
|
||||
// BalancerMember http://127.0.0.1:8081 loadfactor=3
|
||||
// ProxySet lbmethod=byrequests
|
||||
// </Proxy>
|
||||
|
||||
// 解析 BalancerMember
|
||||
memberPattern := regexp.MustCompile(`BalancerMember\s+(\S+)(?:\s+(.+))?`)
|
||||
memberMatches := memberPattern.FindAllStringSubmatch(contentStr, -1)
|
||||
for _, mm := range memberMatches {
|
||||
addr := mm[1]
|
||||
options := ""
|
||||
if len(mm) > 2 {
|
||||
options = strings.TrimSpace(mm[2])
|
||||
}
|
||||
upstream.Servers[addr] = options
|
||||
}
|
||||
|
||||
// 解析负载均衡方法
|
||||
lbMethodPattern := regexp.MustCompile(`lbmethod=(\S+)`)
|
||||
if lm := lbMethodPattern.FindStringSubmatch(contentStr); lm != nil {
|
||||
switch lm[1] {
|
||||
case "byrequests":
|
||||
upstream.Algo = ""
|
||||
case "bytraffic":
|
||||
upstream.Algo = "bytraffic"
|
||||
case "bybusyness":
|
||||
upstream.Algo = "least_conn"
|
||||
case "heartbeat":
|
||||
upstream.Algo = "heartbeat"
|
||||
}
|
||||
}
|
||||
|
||||
// 解析连接池大小 (类似 keepalive)
|
||||
maxPattern := regexp.MustCompile(`max=(\d+)`)
|
||||
if mm := maxPattern.FindStringSubmatch(contentStr); mm != nil {
|
||||
upstream.Keepalive, _ = strconv.Atoi(mm[1])
|
||||
}
|
||||
|
||||
return upstream, nil
|
||||
}
|
||||
|
||||
// writeBalancerFiles 将负载均衡配置写入文件
|
||||
func writeBalancerFiles(globalDir string, upstreams map[string]types.Upstream) error {
|
||||
// 删除现有的负载均衡配置文件
|
||||
if err := clearBalancerFiles(globalDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
num := 100
|
||||
for name, upstream := range upstreams {
|
||||
fileName := fmt.Sprintf("%03d-balancer-%s.conf", num, name)
|
||||
filePath := filepath.Join(globalDir, fileName)
|
||||
|
||||
content := generateBalancerConfig(name, upstream)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write balancer config: %w", err)
|
||||
}
|
||||
num++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearBalancerFiles 清除所有负载均衡配置文件
|
||||
func clearBalancerFiles(globalDir string) error {
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if balancerFilePattern.MatchString(entry.Name()) {
|
||||
filePath := filepath.Join(globalDir, entry.Name())
|
||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete balancer config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateBalancerConfig 生成负载均衡配置内容
|
||||
func generateBalancerConfig(name string, upstream types.Upstream) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Load balancer: %s\n", name))
|
||||
sb.WriteString("<IfModule mod_proxy_balancer.c>\n")
|
||||
sb.WriteString(fmt.Sprintf(" <Proxy balancer://%s>\n", name))
|
||||
|
||||
// 服务器列表
|
||||
for addr, options := range upstream.Servers {
|
||||
if options != "" {
|
||||
sb.WriteString(fmt.Sprintf(" BalancerMember %s %s\n", addr, options))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" BalancerMember %s\n", addr))
|
||||
}
|
||||
}
|
||||
|
||||
// 负载均衡方法
|
||||
lbMethod := "byrequests" // 默认轮询
|
||||
switch upstream.Algo {
|
||||
case "least_conn":
|
||||
lbMethod = "bybusyness"
|
||||
case "bytraffic":
|
||||
lbMethod = "bytraffic"
|
||||
case "heartbeat":
|
||||
lbMethod = "heartbeat"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" ProxySet lbmethod=%s\n", lbMethod))
|
||||
|
||||
// 连接池配置
|
||||
if upstream.Keepalive > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" ProxySet max=%d\n", upstream.Keepalive))
|
||||
}
|
||||
|
||||
sb.WriteString(" </Proxy>\n")
|
||||
sb.WriteString("</IfModule>\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
228
pkg/webserver/apache/redirect.go
Normal file
228
pkg/webserver/apache/redirect.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package apache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// redirectFilePattern 匹配重定向配置文件名 (100-199)
|
||||
var redirectFilePattern = regexp.MustCompile(`^(\d{3})-redirect\.conf$`)
|
||||
|
||||
// parseRedirectFiles 从 vhost 目录解析所有重定向配置
|
||||
func parseRedirectFiles(vhostDir string) ([]types.Redirect, error) {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var redirects []types.Redirect
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := redirectFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num < RedirectStartNum || num > RedirectEndNum {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
redirect, err := parseRedirectFile(filePath)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if redirect != nil {
|
||||
redirects = append(redirects, *redirect)
|
||||
}
|
||||
}
|
||||
|
||||
return redirects, nil
|
||||
}
|
||||
|
||||
// parseRedirectFile 解析单个重定向配置文件
|
||||
func parseRedirectFile(filePath string) (*types.Redirect, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// 解析 Redirect 指令: Redirect 308 /old /new
|
||||
redirectPattern := regexp.MustCompile(`Redirect\s+(\d+)\s+(\S+)\s+(\S+)`)
|
||||
if matches := redirectPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
statusCode, _ := strconv.Atoi(matches[1])
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: matches[2],
|
||||
To: matches[3],
|
||||
StatusCode: statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析 RedirectMatch 指令: RedirectMatch 308 ^/old(.*)$ /new$1
|
||||
redirectMatchPattern := regexp.MustCompile(`RedirectMatch\s+(\d+)\s+(\S+)\s+(\S+)`)
|
||||
if matches := redirectMatchPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
statusCode, _ := strconv.Atoi(matches[1])
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: matches[2],
|
||||
To: matches[3],
|
||||
KeepURI: strings.Contains(matches[3], "$1"),
|
||||
StatusCode: statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析 RewriteRule Host 重定向
|
||||
// RewriteCond %{HTTP_HOST} ^old\.example\.com$
|
||||
// RewriteRule ^(.*)$ https://new.example.com$1 [R=308,L]
|
||||
hostRewritePattern := regexp.MustCompile(`RewriteCond\s+%\{HTTP_HOST}\s+\^?([^$\s]+)\$?\s*\[?NC]?\s*\n\s*RewriteRule\s+\^\(\.\*\)\$\s+([^\s\[]+)\s*\[R=(\d+)`)
|
||||
if matches := hostRewritePattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
statusCode, _ := strconv.Atoi(matches[3])
|
||||
host := strings.ReplaceAll(matches[1], `\.`, ".")
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectTypeHost,
|
||||
From: host,
|
||||
To: matches[2],
|
||||
KeepURI: strings.Contains(matches[2], "$1"),
|
||||
StatusCode: statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析 ErrorDocument 404 重定向
|
||||
// ErrorDocument 404 /custom-404
|
||||
errorDocPattern := regexp.MustCompile(`ErrorDocument\s+404\s+(\S+)`)
|
||||
if matches := errorDocPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectType404,
|
||||
To: matches[1],
|
||||
StatusCode: 308,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// writeRedirectFiles 将重定向配置写入文件
|
||||
func writeRedirectFiles(vhostDir string, redirects []types.Redirect) error {
|
||||
// 删除现有的重定向配置文件 (100-199)
|
||||
if err := clearRedirectFiles(vhostDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
for i, redirect := range redirects {
|
||||
num := RedirectStartNum + i
|
||||
if num > RedirectEndNum {
|
||||
return fmt.Errorf("redirect rules exceed limit (%d)", RedirectEndNum-RedirectStartNum+1)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%03d-redirect.conf", num)
|
||||
filePath := filepath.Join(vhostDir, fileName)
|
||||
|
||||
content := generateRedirectConfig(redirect)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write redirect config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearRedirectFiles 清除所有重定向配置文件
|
||||
func clearRedirectFiles(vhostDir string) error {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := redirectFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num >= RedirectStartNum && num <= RedirectEndNum {
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete redirect config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRedirectConfig 生成重定向配置内容
|
||||
func generateRedirectConfig(redirect types.Redirect) string {
|
||||
statusCode := redirect.StatusCode
|
||||
if statusCode == 0 {
|
||||
statusCode = 308 // 默认使用 308 永久重定向
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
switch redirect.Type {
|
||||
case types.RedirectTypeURL:
|
||||
// URL 重定向
|
||||
sb.WriteString(fmt.Sprintf("# URL redirect: %s -> %s\n", redirect.From, redirect.To))
|
||||
if redirect.KeepURI {
|
||||
// 使用 RedirectMatch 保持 URI
|
||||
from := redirect.From
|
||||
if !strings.HasPrefix(from, "^") {
|
||||
from = "^" + from
|
||||
}
|
||||
if !strings.HasSuffix(from, "(.*)$") && !strings.HasSuffix(from, "$") {
|
||||
from = from + "(.*)$"
|
||||
}
|
||||
to := redirect.To
|
||||
if !strings.HasSuffix(to, "$1") {
|
||||
to = to + "$1"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("RedirectMatch %d %s %s\n", statusCode, from, to))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Redirect %d %s %s\n", statusCode, redirect.From, redirect.To))
|
||||
}
|
||||
|
||||
case types.RedirectTypeHost:
|
||||
// Host 重定向
|
||||
sb.WriteString(fmt.Sprintf("# Host redirect: %s -> %s\n", redirect.From, redirect.To))
|
||||
sb.WriteString("RewriteEngine on\n")
|
||||
escapedHost := strings.ReplaceAll(redirect.From, ".", `\.`)
|
||||
sb.WriteString(fmt.Sprintf("RewriteCond %%{HTTP_HOST} ^%s$ [NC]\n", escapedHost))
|
||||
if redirect.KeepURI {
|
||||
sb.WriteString(fmt.Sprintf("RewriteRule ^(.*)$ %s$1 [R=%d,L]\n", redirect.To, statusCode))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("RewriteRule ^(.*)$ %s [R=%d,L]\n", redirect.To, statusCode))
|
||||
}
|
||||
|
||||
case types.RedirectType404:
|
||||
// 404 重定向
|
||||
sb.WriteString(fmt.Sprintf("# 404 redirect -> %s\n", redirect.To))
|
||||
sb.WriteString(fmt.Sprintf("ErrorDocument 404 %s\n", redirect.To))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -11,17 +11,35 @@ import (
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// Vhost Apache 虚拟主机实现
|
||||
type Vhost struct {
|
||||
// StaticVhost 纯静态虚拟主机
|
||||
type StaticVhost struct {
|
||||
*baseVhost
|
||||
}
|
||||
|
||||
// PHPVhost PHP 虚拟主机
|
||||
type PHPVhost struct {
|
||||
*baseVhost
|
||||
}
|
||||
|
||||
// ProxyVhost 反向代理虚拟主机
|
||||
type ProxyVhost struct {
|
||||
*baseVhost
|
||||
}
|
||||
|
||||
// baseVhost Apache 虚拟主机基础实现
|
||||
type baseVhost struct {
|
||||
config *Config
|
||||
vhost *VirtualHost
|
||||
configDir string // 配置目录
|
||||
}
|
||||
|
||||
// NewVhost 创建 Apache 虚拟主机实例
|
||||
// configDir: 配置目录路径
|
||||
func NewVhost(configDir string) (*Vhost, error) {
|
||||
v := &Vhost{
|
||||
// newBaseVhost 创建基础虚拟主机实例
|
||||
func newBaseVhost(configDir string) (*baseVhost, error) {
|
||||
if configDir == "" {
|
||||
return nil, fmt.Errorf("config directory is required")
|
||||
}
|
||||
|
||||
v := &baseVhost{
|
||||
configDir: configDir,
|
||||
}
|
||||
|
||||
@@ -29,14 +47,12 @@ func NewVhost(configDir string) (*Vhost, error) {
|
||||
var config *Config
|
||||
var err error
|
||||
|
||||
if v.configDir != "" {
|
||||
// 从配置目录加载主配置文件
|
||||
configFile := filepath.Join(v.configDir, "apache.conf")
|
||||
if _, statErr := os.Stat(configFile); statErr == nil {
|
||||
config, err = ParseFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse apache config: %w", err)
|
||||
}
|
||||
// 从配置目录加载主配置文件
|
||||
configFile := filepath.Join(v.configDir, "apache.conf")
|
||||
if _, statErr := os.Stat(configFile); statErr == nil {
|
||||
config, err = ParseFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse apache config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,17 +77,42 @@ func NewVhost(configDir string) (*Vhost, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ========== VhostCore 接口实现 ==========
|
||||
// NewStaticVhost 创建纯静态虚拟主机实例
|
||||
func NewStaticVhost(configDir string) (*StaticVhost, error) {
|
||||
base, err := newBaseVhost(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StaticVhost{baseVhost: base}, nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Enable() bool {
|
||||
// NewPHPVhost 创建 PHP 虚拟主机实例
|
||||
func NewPHPVhost(configDir string) (*PHPVhost, error) {
|
||||
base, err := newBaseVhost(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PHPVhost{baseVhost: base}, nil
|
||||
}
|
||||
|
||||
// NewProxyVhost 创建反向代理虚拟主机实例
|
||||
func NewProxyVhost(configDir string) (*ProxyVhost, error) {
|
||||
base, err := newBaseVhost(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ProxyVhost{baseVhost: base}, nil
|
||||
}
|
||||
|
||||
func (v *baseVhost) Enable() bool {
|
||||
// 检查禁用配置文件是否存在
|
||||
disableFile := filepath.Join(v.configDir, "server.d", DisableConfName)
|
||||
disableFile := filepath.Join(v.configDir, "vhost", DisableConfName)
|
||||
_, err := os.Stat(disableFile)
|
||||
return os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (v *Vhost) SetEnable(enable bool, _ ...string) error {
|
||||
serverDir := filepath.Join(v.configDir, "server.d")
|
||||
func (v *baseVhost) SetEnable(enable bool, _ ...string) error {
|
||||
serverDir := filepath.Join(v.configDir, "vhost")
|
||||
disableFile := filepath.Join(serverDir, DisableConfName)
|
||||
|
||||
if enable {
|
||||
@@ -83,20 +124,14 @@ func (v *Vhost) SetEnable(enable bool, _ ...string) error {
|
||||
}
|
||||
|
||||
// 禁用:创建禁用配置文件
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(serverDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// 写入禁用配置
|
||||
if err := os.WriteFile(disableFile, []byte(DisableConfContent), 0644); err != nil {
|
||||
return fmt.Errorf("写入禁用配置失败: %w", err)
|
||||
return fmt.Errorf("failed to write disable config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Listen() []types.Listen {
|
||||
func (v *baseVhost) Listen() []types.Listen {
|
||||
var result []types.Listen
|
||||
|
||||
// Apache 的监听配置通常在 VirtualHost 的参数中
|
||||
@@ -120,7 +155,7 @@ func (v *Vhost) Listen() []types.Listen {
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Vhost) SetListen(listens []types.Listen) error {
|
||||
func (v *baseVhost) SetListen(listens []types.Listen) error {
|
||||
var args []string
|
||||
for _, l := range listens {
|
||||
args = append(args, l.Address)
|
||||
@@ -129,7 +164,7 @@ func (v *Vhost) SetListen(listens []types.Listen) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) ServerName() []string {
|
||||
func (v *baseVhost) ServerName() []string {
|
||||
var names []string
|
||||
|
||||
// 获取 ServerName
|
||||
@@ -147,7 +182,7 @@ func (v *Vhost) ServerName() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
func (v *Vhost) SetServerName(serverName []string) error {
|
||||
func (v *baseVhost) SetServerName(serverName []string) error {
|
||||
if len(serverName) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -166,7 +201,7 @@ func (v *Vhost) SetServerName(serverName []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Index() []string {
|
||||
func (v *baseVhost) Index() []string {
|
||||
values := v.vhost.GetDirectiveValues("DirectoryIndex")
|
||||
if values != nil {
|
||||
return values
|
||||
@@ -174,7 +209,7 @@ func (v *Vhost) Index() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) SetIndex(index []string) error {
|
||||
func (v *baseVhost) SetIndex(index []string) error {
|
||||
if len(index) == 0 {
|
||||
v.vhost.RemoveDirective("DirectoryIndex")
|
||||
return nil
|
||||
@@ -183,11 +218,11 @@ func (v *Vhost) SetIndex(index []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Root() string {
|
||||
func (v *baseVhost) Root() string {
|
||||
return v.vhost.GetDirectiveValue("DocumentRoot")
|
||||
}
|
||||
|
||||
func (v *Vhost) SetRoot(root string) error {
|
||||
func (v *baseVhost) SetRoot(root string) error {
|
||||
v.vhost.SetDirective("DocumentRoot", root)
|
||||
|
||||
// 同时更新 Directory 块
|
||||
@@ -210,7 +245,7 @@ func (v *Vhost) SetRoot(root string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Includes() []types.IncludeFile {
|
||||
func (v *baseVhost) Includes() []types.IncludeFile {
|
||||
var result []types.IncludeFile
|
||||
|
||||
// 获取所有 Include 和 IncludeOptional 指令
|
||||
@@ -232,7 +267,7 @@ func (v *Vhost) Includes() []types.IncludeFile {
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Vhost) SetIncludes(includes []types.IncludeFile) error {
|
||||
func (v *baseVhost) SetIncludes(includes []types.IncludeFile) error {
|
||||
// 删除现有的 Include 指令
|
||||
v.vhost.RemoveDirectives("Include")
|
||||
v.vhost.RemoveDirectives("IncludeOptional")
|
||||
@@ -245,76 +280,48 @@ func (v *Vhost) SetIncludes(includes []types.IncludeFile) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) AccessLog() string {
|
||||
func (v *baseVhost) AccessLog() string {
|
||||
return v.vhost.GetDirectiveValue("CustomLog")
|
||||
}
|
||||
|
||||
func (v *Vhost) SetAccessLog(accessLog string) error {
|
||||
func (v *baseVhost) SetAccessLog(accessLog string) error {
|
||||
v.vhost.SetDirective("CustomLog", accessLog, "combined")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) ErrorLog() string {
|
||||
func (v *baseVhost) ErrorLog() string {
|
||||
return v.vhost.GetDirectiveValue("ErrorLog")
|
||||
}
|
||||
|
||||
func (v *Vhost) SetErrorLog(errorLog string) error {
|
||||
func (v *baseVhost) SetErrorLog(errorLog string) error {
|
||||
v.vhost.SetDirective("ErrorLog", errorLog)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Save() error {
|
||||
if v.configDir == "" {
|
||||
return fmt.Errorf("配置目录为空,无法保存")
|
||||
}
|
||||
|
||||
func (v *baseVhost) Save() error {
|
||||
configFile := filepath.Join(v.configDir, "apache.conf")
|
||||
content := v.config.Export()
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("保存配置文件失败: %w", err)
|
||||
return fmt.Errorf("failed to save config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Reload() error {
|
||||
// 重载 Apache 配置
|
||||
cmds := []string{
|
||||
"/opt/ace/apps/apache/bin/apachectl graceful",
|
||||
"/usr/sbin/apachectl graceful",
|
||||
"apachectl graceful",
|
||||
"systemctl reload apache2",
|
||||
"systemctl reload httpd",
|
||||
func (v *baseVhost) Reload() error {
|
||||
parts := strings.Fields("systemctl reload httpd")
|
||||
if err := exec.Command(parts[0], parts[1:]...).Run(); err != nil {
|
||||
return fmt.Errorf("failed to reload apache config: %w", err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, cmd := range cmds {
|
||||
parts := strings.Fields(cmd)
|
||||
if len(parts) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查命令是否存在
|
||||
if _, err := os.Stat(parts[0]); err == nil || parts[0] == "systemctl" {
|
||||
err := exec.Command(parts[0], parts[1:]...).Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("重载 Apache 配置失败: %w", lastErr)
|
||||
}
|
||||
return fmt.Errorf("未找到 apachectl 或 apache2 命令")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Reset() error {
|
||||
func (v *baseVhost) Reset() error {
|
||||
// 重置配置为默认值
|
||||
config, err := ParseString(DefaultVhostConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("重置配置失败: %w", err)
|
||||
return fmt.Errorf("failed to reset config: %w", err)
|
||||
}
|
||||
|
||||
v.config = config
|
||||
@@ -325,15 +332,13 @@ func (v *Vhost) Reset() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== VhostSSL 接口实现 ==========
|
||||
|
||||
func (v *Vhost) HTTPS() bool {
|
||||
func (v *baseVhost) HTTPS() bool {
|
||||
// 检查是否有 SSL 相关配置
|
||||
return v.vhost.HasDirective("SSLEngine") &&
|
||||
strings.EqualFold(v.vhost.GetDirectiveValue("SSLEngine"), "on")
|
||||
}
|
||||
|
||||
func (v *Vhost) SSLConfig() *types.SSLConfig {
|
||||
func (v *baseVhost) SSLConfig() *types.SSLConfig {
|
||||
if !v.HTTPS() {
|
||||
return nil
|
||||
}
|
||||
@@ -376,9 +381,9 @@ func (v *Vhost) SSLConfig() *types.SSLConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
func (v *Vhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
func (v *baseVhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("SSL 配置不能为空")
|
||||
return fmt.Errorf("SSL config cannot be nil")
|
||||
}
|
||||
|
||||
// 启用 SSL
|
||||
@@ -449,7 +454,7 @@ func (v *Vhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) ClearHTTPS() error {
|
||||
func (v *baseVhost) ClearHTTPS() error {
|
||||
// 移除 SSL 相关指令
|
||||
v.vhost.RemoveDirective("SSLEngine")
|
||||
v.vhost.RemoveDirective("SSLCertificateFile")
|
||||
@@ -491,9 +496,94 @@ func (v *Vhost) ClearHTTPS() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== VhostPHP 接口实现 ==========
|
||||
func (v *baseVhost) RateLimit() *types.RateLimit {
|
||||
// Apache 使用 mod_ratelimit
|
||||
rate := v.vhost.GetDirectiveValue("SetOutputFilter")
|
||||
if rate != "RATE_LIMIT" {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) PHP() int {
|
||||
rateLimit := &types.RateLimit{
|
||||
Options: make(map[string]string),
|
||||
}
|
||||
|
||||
// 获取速率限制值
|
||||
rateValue := v.vhost.GetDirectiveValue("SetEnv")
|
||||
if rateValue != "" {
|
||||
rateLimit.Rate = rateValue
|
||||
}
|
||||
|
||||
return rateLimit
|
||||
}
|
||||
|
||||
func (v *baseVhost) SetRateLimit(limit *types.RateLimit) error {
|
||||
if limit == nil {
|
||||
// 清除限速配置
|
||||
v.vhost.RemoveDirective("SetOutputFilter")
|
||||
v.vhost.RemoveDirectives("SetEnv")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置 mod_ratelimit
|
||||
v.vhost.SetDirective("SetOutputFilter", "RATE_LIMIT")
|
||||
if limit.Rate != "" {
|
||||
v.vhost.SetDirective("SetEnv", "rate-limit", limit.Rate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *baseVhost) BasicAuth() map[string]string {
|
||||
authType := v.vhost.GetDirectiveValue("AuthType")
|
||||
if authType == "" || !strings.EqualFold(authType, "Basic") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"realm": v.vhost.GetDirectiveValue("AuthName"),
|
||||
"user_file": v.vhost.GetDirectiveValue("AuthUserFile"),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *baseVhost) SetBasicAuth(auth map[string]string) error {
|
||||
if auth == nil || len(auth) == 0 {
|
||||
// 清除基本认证配置
|
||||
v.vhost.RemoveDirective("AuthType")
|
||||
v.vhost.RemoveDirective("AuthName")
|
||||
v.vhost.RemoveDirective("AuthUserFile")
|
||||
v.vhost.RemoveDirective("Require")
|
||||
return nil
|
||||
}
|
||||
|
||||
realm := auth["realm"]
|
||||
userFile := auth["user_file"]
|
||||
|
||||
if realm == "" {
|
||||
realm = "Restricted"
|
||||
}
|
||||
|
||||
v.vhost.SetDirective("AuthType", "Basic")
|
||||
v.vhost.SetDirective("AuthName", fmt.Sprintf(`"%s"`, realm))
|
||||
v.vhost.SetDirective("AuthUserFile", userFile)
|
||||
v.vhost.SetDirective("Require", "valid-user")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *baseVhost) Redirects() []types.Redirect {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
redirects, _ := parseRedirectFiles(vhostDir)
|
||||
return redirects
|
||||
}
|
||||
|
||||
func (v *baseVhost) SetRedirects(redirects []types.Redirect) error {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
return writeRedirectFiles(vhostDir, redirects)
|
||||
}
|
||||
|
||||
// ========== PHPVhost ==========
|
||||
|
||||
func (v *PHPVhost) PHP() int {
|
||||
// Apache 通常通过 FilesMatch 块配置 PHP
|
||||
// 或者通过 SetHandler 指令
|
||||
handler := v.vhost.GetDirectiveValue("SetHandler")
|
||||
@@ -540,7 +630,7 @@ func (v *Vhost) PHP() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *Vhost) SetPHP(version int) error {
|
||||
func (v *PHPVhost) SetPHP(version int) error {
|
||||
// 移除现有的 PHP 配置
|
||||
v.vhost.RemoveDirective("SetHandler")
|
||||
|
||||
@@ -577,78 +667,36 @@ func (v *Vhost) SetPHP(version int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== VhostAdvanced 接口实现 ==========
|
||||
// ========== ProxyVhost ==========
|
||||
|
||||
func (v *Vhost) RateLimit() *types.RateLimit {
|
||||
// Apache 使用 mod_ratelimit
|
||||
rate := v.vhost.GetDirectiveValue("SetOutputFilter")
|
||||
if rate != "RATE_LIMIT" {
|
||||
return nil
|
||||
}
|
||||
|
||||
rateLimit := &types.RateLimit{
|
||||
Options: make(map[string]string),
|
||||
}
|
||||
|
||||
// 获取速率限制值
|
||||
rateValue := v.vhost.GetDirectiveValue("SetEnv")
|
||||
if rateValue != "" {
|
||||
rateLimit.Rate = rateValue
|
||||
}
|
||||
|
||||
return rateLimit
|
||||
func (v *ProxyVhost) Proxies() []types.Proxy {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
proxies, _ := parseProxyFiles(vhostDir)
|
||||
return proxies
|
||||
}
|
||||
|
||||
func (v *Vhost) SetRateLimit(limit *types.RateLimit) error {
|
||||
if limit == nil {
|
||||
// 清除限速配置
|
||||
v.vhost.RemoveDirective("SetOutputFilter")
|
||||
v.vhost.RemoveDirectives("SetEnv")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置 mod_ratelimit
|
||||
v.vhost.SetDirective("SetOutputFilter", "RATE_LIMIT")
|
||||
if limit.Rate != "" {
|
||||
v.vhost.SetDirective("SetEnv", "rate-limit", limit.Rate)
|
||||
}
|
||||
|
||||
return nil
|
||||
func (v *ProxyVhost) SetProxies(proxies []types.Proxy) error {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
return writeProxyFiles(vhostDir, proxies)
|
||||
}
|
||||
|
||||
func (v *Vhost) BasicAuth() map[string]string {
|
||||
authType := v.vhost.GetDirectiveValue("AuthType")
|
||||
if authType == "" || !strings.EqualFold(authType, "Basic") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"realm": v.vhost.GetDirectiveValue("AuthName"),
|
||||
"user_file": v.vhost.GetDirectiveValue("AuthUserFile"),
|
||||
}
|
||||
func (v *ProxyVhost) ClearProxies() error {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
return clearProxyFiles(vhostDir)
|
||||
}
|
||||
|
||||
func (v *Vhost) SetBasicAuth(auth map[string]string) error {
|
||||
if auth == nil || len(auth) == 0 {
|
||||
// 清除基本认证配置
|
||||
v.vhost.RemoveDirective("AuthType")
|
||||
v.vhost.RemoveDirective("AuthName")
|
||||
v.vhost.RemoveDirective("AuthUserFile")
|
||||
v.vhost.RemoveDirective("Require")
|
||||
return nil
|
||||
}
|
||||
|
||||
realm := auth["realm"]
|
||||
userFile := auth["user_file"]
|
||||
|
||||
if realm == "" {
|
||||
realm = "Restricted"
|
||||
}
|
||||
|
||||
v.vhost.SetDirective("AuthType", "Basic")
|
||||
v.vhost.SetDirective("AuthName", fmt.Sprintf(`"%s"`, realm))
|
||||
v.vhost.SetDirective("AuthUserFile", userFile)
|
||||
v.vhost.SetDirective("Require", "valid-user")
|
||||
|
||||
return nil
|
||||
func (v *ProxyVhost) Upstreams() map[string]types.Upstream {
|
||||
globalDir := filepath.Join(v.configDir, "global")
|
||||
upstreams, _ := parseBalancerFiles(globalDir)
|
||||
return upstreams
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) SetUpstreams(upstreams map[string]types.Upstream) error {
|
||||
globalDir := filepath.Join(v.configDir, "global")
|
||||
return writeBalancerFiles(globalDir, upstreams)
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) ClearUpstreams() error {
|
||||
globalDir := filepath.Join(v.configDir, "global")
|
||||
return clearBalancerFiles(globalDir)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
type VhostTestSuite struct {
|
||||
suite.Suite
|
||||
vhost *Vhost
|
||||
vhost *PHPVhost
|
||||
configDir string
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ func (s *VhostTestSuite) SetupTest() {
|
||||
s.Require().NoError(err)
|
||||
s.configDir = configDir
|
||||
|
||||
// 创建 server.d 目录
|
||||
err = os.MkdirAll(filepath.Join(configDir, "server.d"), 0755)
|
||||
// 创建 vhost 目录
|
||||
err = os.MkdirAll(filepath.Join(configDir, "vhost"), 0755)
|
||||
s.Require().NoError(err)
|
||||
|
||||
vhost, err := NewVhost(configDir)
|
||||
vhost, err := NewPHPVhost(configDir)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(vhost)
|
||||
s.vhost = vhost
|
||||
@@ -45,13 +45,13 @@ func (s *VhostTestSuite) TearDownTest() {
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestNewVhost() {
|
||||
s.Equal(s.configDir, s.vhost.configDir)
|
||||
s.NotNil(s.vhost.config)
|
||||
s.NotNil(s.vhost.vhost)
|
||||
s.Equal(s.configDir, s.vhost.baseVhost.configDir)
|
||||
s.NotNil(s.vhost.baseVhost.config)
|
||||
s.NotNil(s.vhost.baseVhost.vhost)
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestEnable() {
|
||||
// 默认应该是启用状态(没有 00-disable.conf)
|
||||
// 默认应该是启用状态(没有 000-disable.conf)
|
||||
s.True(s.vhost.Enable())
|
||||
|
||||
// 禁用网站
|
||||
@@ -60,7 +60,7 @@ func (s *VhostTestSuite) TestEnable() {
|
||||
s.False(s.vhost.Enable())
|
||||
|
||||
// 验证禁用文件存在
|
||||
disableFile := filepath.Join(s.configDir, "server.d", DisableConfName)
|
||||
disableFile := filepath.Join(s.configDir, "vhost", DisableConfName)
|
||||
_, err = os.Stat(disableFile)
|
||||
s.NoError(err)
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *VhostTestSuite) TestDisableConfigContent() {
|
||||
s.NoError(err)
|
||||
|
||||
// 读取禁用配置内容
|
||||
disableFile := filepath.Join(s.configDir, "server.d", DisableConfName)
|
||||
disableFile := filepath.Join(s.configDir, "vhost", DisableConfName)
|
||||
content, err := os.ReadFile(disableFile)
|
||||
s.NoError(err)
|
||||
|
||||
@@ -381,7 +381,280 @@ func (s *VhostTestSuite) TestPHPFilesMatchBlock() {
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestDefaultVhostConfIncludesServerD() {
|
||||
// 验证默认配置包含 server.d 的 include
|
||||
s.Contains(DefaultVhostConf, "server.d")
|
||||
// 验证默认配置包含 vhost 的 include
|
||||
s.Contains(DefaultVhostConf, "vhost")
|
||||
s.Contains(DefaultVhostConf, "IncludeOptional")
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirects() {
|
||||
// 初始应该没有重定向
|
||||
s.Empty(s.vhost.Redirects())
|
||||
|
||||
// 设置重定向
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: "/old",
|
||||
To: "/new",
|
||||
StatusCode: 301,
|
||||
},
|
||||
{
|
||||
Type: types.RedirectTypeHost,
|
||||
From: "old.example.com",
|
||||
To: "https://new.example.com",
|
||||
KeepURI: true,
|
||||
StatusCode: 308,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 验证重定向文件已创建
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
s.NoError(err)
|
||||
|
||||
redirectCount := 0
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "1") && strings.HasSuffix(entry.Name(), "-redirect.conf") {
|
||||
redirectCount++
|
||||
}
|
||||
}
|
||||
s.Equal(2, redirectCount)
|
||||
|
||||
// 验证可以读取回来
|
||||
got := s.vhost.Redirects()
|
||||
s.Len(got, 2)
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirectURL() {
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: "/old-page",
|
||||
To: "/new-page",
|
||||
StatusCode: 301,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "100-redirect.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "Redirect 301")
|
||||
s.Contains(string(content), "/old-page")
|
||||
s.Contains(string(content), "/new-page")
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirectHost() {
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectTypeHost,
|
||||
From: "old.example.com",
|
||||
To: "https://new.example.com",
|
||||
KeepURI: true,
|
||||
StatusCode: 308,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "100-redirect.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "RewriteEngine")
|
||||
s.Contains(string(content), "RewriteCond")
|
||||
s.Contains(string(content), "old.example.com")
|
||||
s.Contains(string(content), "R=308")
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirect404() {
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectType404,
|
||||
To: "/custom-404.html",
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "100-redirect.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "ErrorDocument 404")
|
||||
s.Contains(string(content), "/custom-404.html")
|
||||
}
|
||||
|
||||
// ProxyVhost 测试套件
|
||||
type ProxyVhostTestSuite struct {
|
||||
suite.Suite
|
||||
vhost *ProxyVhost
|
||||
configDir string
|
||||
}
|
||||
|
||||
func TestProxyVhostTestSuite(t *testing.T) {
|
||||
suite.Run(t, &ProxyVhostTestSuite{})
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) SetupTest() {
|
||||
configDir, err := os.MkdirTemp("", "apache-proxy-test-*")
|
||||
s.Require().NoError(err)
|
||||
s.configDir = configDir
|
||||
|
||||
// 创建 vhost 和 global 目录
|
||||
s.NoError(os.MkdirAll(filepath.Join(configDir, "vhost"), 0755))
|
||||
s.NoError(os.MkdirAll(filepath.Join(configDir, "global"), 0755))
|
||||
|
||||
vhost, err := NewProxyVhost(configDir)
|
||||
s.Require().NoError(err)
|
||||
s.vhost = vhost
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TearDownTest() {
|
||||
if s.configDir != "" {
|
||||
s.NoError(os.RemoveAll(s.configDir))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestProxies() {
|
||||
// 初始应该没有代理配置
|
||||
s.Empty(s.vhost.Proxies())
|
||||
|
||||
// 设置代理配置
|
||||
proxies := []types.Proxy{
|
||||
{
|
||||
Location: "/",
|
||||
Pass: "http://backend:8080/",
|
||||
Host: "example.com",
|
||||
},
|
||||
{
|
||||
Location: "/api",
|
||||
Pass: "http://api-backend:8080/",
|
||||
Buffering: true,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
|
||||
// 验证代理文件已创建
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
s.NoError(err)
|
||||
|
||||
proxyCount := 0
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "2") && strings.HasSuffix(entry.Name(), "-proxy.conf") {
|
||||
proxyCount++
|
||||
}
|
||||
}
|
||||
s.Equal(2, proxyCount)
|
||||
|
||||
// 验证可以读取回来
|
||||
got := s.vhost.Proxies()
|
||||
s.Len(got, 2)
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestProxyConfig() {
|
||||
proxies := []types.Proxy{
|
||||
{
|
||||
Location: "/",
|
||||
Pass: "http://backend:8080/",
|
||||
Host: "example.com",
|
||||
Buffering: true,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "200-proxy.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "ProxyPass /")
|
||||
s.Contains(string(content), "ProxyPassReverse")
|
||||
s.Contains(string(content), "http://backend:8080/")
|
||||
s.Contains(string(content), "RequestHeader set Host")
|
||||
s.Contains(string(content), "example.com")
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestClearProxies() {
|
||||
proxies := []types.Proxy{
|
||||
{Location: "/", Pass: "http://backend/"},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
s.Len(s.vhost.Proxies(), 1)
|
||||
|
||||
s.NoError(s.vhost.ClearProxies())
|
||||
s.Empty(s.vhost.Proxies())
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestUpstreams() {
|
||||
// 初始应该没有上游服务器配置
|
||||
s.Empty(s.vhost.Upstreams())
|
||||
|
||||
// 设置上游服务器(Apache 使用 balancer)
|
||||
upstreams := map[string]types.Upstream{
|
||||
"backend": {
|
||||
Servers: map[string]string{
|
||||
"http://127.0.0.1:8080": "loadfactor=5",
|
||||
"http://127.0.0.1:8081": "loadfactor=3",
|
||||
},
|
||||
Algo: "least_conn",
|
||||
Keepalive: 32,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
|
||||
// 验证 balancer 文件已创建
|
||||
globalDir := filepath.Join(s.configDir, "global")
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(entries)
|
||||
|
||||
// 验证可以读取回来
|
||||
got := s.vhost.Upstreams()
|
||||
s.Len(got, 1)
|
||||
s.Contains(got, "backend")
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestBalancerConfig() {
|
||||
upstreams := map[string]types.Upstream{
|
||||
"mybackend": {
|
||||
Servers: map[string]string{
|
||||
"http://127.0.0.1:8080": "loadfactor=5",
|
||||
},
|
||||
Algo: "least_conn",
|
||||
Keepalive: 16,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
|
||||
// 读取配置文件内容
|
||||
globalDir := filepath.Join(s.configDir, "global")
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
s.NoError(err)
|
||||
s.Require().NotEmpty(entries)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(globalDir, entries[0].Name()))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "balancer://mybackend")
|
||||
s.Contains(string(content), "BalancerMember")
|
||||
s.Contains(string(content), "http://127.0.0.1:8080")
|
||||
s.Contains(string(content), "lbmethod=bybusyness") // least_conn 映射为 bybusyness
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestClearUpstreams() {
|
||||
upstreams := map[string]types.Upstream{
|
||||
"backend": {
|
||||
Servers: map[string]string{"http://127.0.0.1:8080": ""},
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
s.Len(s.vhost.Upstreams(), 1)
|
||||
|
||||
s.NoError(s.vhost.ClearUpstreams())
|
||||
s.Empty(s.vhost.Upstreams())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package nginx
|
||||
|
||||
// DisableConfName 禁用配置文件名
|
||||
const DisableConfName = "00-disable.conf"
|
||||
const DisableConfName = "000-disable.conf"
|
||||
|
||||
// DisableConfContent 禁用配置内容
|
||||
const DisableConfContent = `# 网站已停止
|
||||
@@ -10,7 +10,16 @@ location / {
|
||||
}
|
||||
`
|
||||
|
||||
const DefaultConf = `include /opt/ace/sites/default/config/http.d/*.conf;
|
||||
// 配置文件序号范围
|
||||
const (
|
||||
RedirectStartNum = 100 // 重定向配置起始序号 (100-199)
|
||||
RedirectEndNum = 199
|
||||
ProxyStartNum = 200 // 代理配置起始序号 (200-299)
|
||||
ProxyEndNum = 299
|
||||
UpstreamStartNum = 100 // 上游服务器配置起始序号
|
||||
)
|
||||
|
||||
const DefaultConf = `include /opt/ace/sites/default/config/global/*.conf;
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
@@ -19,7 +28,7 @@ server {
|
||||
# error page
|
||||
error_page 404 /404.html;
|
||||
# custom configs
|
||||
include /opt/ace/sites/default/config/server.d/*.conf;
|
||||
include /opt/ace/sites/default/config/vhost/*.conf;
|
||||
# browser cache
|
||||
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||
expires 30d;
|
||||
|
||||
@@ -214,10 +214,6 @@ func (p *Parser) parameters2Slices(parameters []config.Parameter) []string {
|
||||
|
||||
// Save 保存配置到文件
|
||||
func (p *Parser) Save() error {
|
||||
if p.cfgPath == "" {
|
||||
return fmt.Errorf("config file path is empty, cannot save")
|
||||
}
|
||||
|
||||
content := p.Dump()
|
||||
if err := os.WriteFile(p.cfgPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to save config file: %w", err)
|
||||
|
||||
@@ -96,7 +96,7 @@ func (s *NginxTestSuite) TestIncludes() {
|
||||
s.NoError(err)
|
||||
includes, comments, err := parser.GetIncludes()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"/opt/ace/sites/default/config/server.d/*.conf"}, includes)
|
||||
s.Equal([]string{"/opt/ace/sites/default/config/vhost/*.conf"}, includes)
|
||||
s.Equal([][]string{{"# custom configs"}}, comments)
|
||||
s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/default.conf"}, nil))
|
||||
includes, comments, err = parser.GetIncludes()
|
||||
|
||||
279
pkg/webserver/nginx/proxy.go
Normal file
279
pkg/webserver/nginx/proxy.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// proxyFilePattern 匹配代理配置文件名 (200-299)
|
||||
var proxyFilePattern = regexp.MustCompile(`^(\d{3})-proxy\.conf$`)
|
||||
|
||||
// parseProxyFiles 从 vhost 目录解析所有代理配置
|
||||
func parseProxyFiles(vhostDir string) ([]types.Proxy, error) {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var proxies []types.Proxy
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := proxyFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num < ProxyStartNum || num > ProxyEndNum {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
proxy, err := parseProxyFile(filePath)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if proxy != nil {
|
||||
proxies = append(proxies, *proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
// parseProxyFile 解析单个代理配置文件
|
||||
func parseProxyFile(filePath string) (*types.Proxy, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// 解析 location 块
|
||||
// location / {
|
||||
// proxy_pass http://backend;
|
||||
// ...
|
||||
// }
|
||||
locationPattern := regexp.MustCompile(`location\s+([^{]+)\{([^}]+(?:\{[^}]*}[^}]*)*)}`)
|
||||
matches := locationPattern.FindStringSubmatch(contentStr)
|
||||
if matches == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proxy := &types.Proxy{
|
||||
Location: strings.TrimSpace(matches[1]),
|
||||
Replaces: make(map[string]string),
|
||||
}
|
||||
|
||||
blockContent := matches[2]
|
||||
|
||||
// 解析 proxy_pass
|
||||
passPattern := regexp.MustCompile(`proxy_pass\s+([^;]+);`)
|
||||
if pm := passPattern.FindStringSubmatch(blockContent); pm != nil {
|
||||
proxy.Pass = strings.TrimSpace(pm[1])
|
||||
}
|
||||
|
||||
// 解析 proxy_set_header Host
|
||||
hostPattern := regexp.MustCompile(`proxy_set_header\s+Host\s+([^;]+);`)
|
||||
if hm := hostPattern.FindStringSubmatch(blockContent); hm != nil {
|
||||
host := strings.TrimSpace(hm[1])
|
||||
// 移除引号
|
||||
host = strings.Trim(host, `"'`)
|
||||
if host != "$host" && host != "$http_host" {
|
||||
proxy.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 proxy_ssl_name (SNI)
|
||||
sniPattern := regexp.MustCompile(`proxy_ssl_name\s+([^;]+);`)
|
||||
if sm := sniPattern.FindStringSubmatch(blockContent); sm != nil {
|
||||
proxy.SNI = strings.TrimSpace(sm[1])
|
||||
}
|
||||
|
||||
// 解析 proxy_buffering
|
||||
bufferingPattern := regexp.MustCompile(`proxy_buffering\s+(on|off);`)
|
||||
if bm := bufferingPattern.FindStringSubmatch(blockContent); bm != nil {
|
||||
proxy.Buffering = bm[1] == "on"
|
||||
}
|
||||
|
||||
// 解析 proxy_cache
|
||||
cachePattern := regexp.MustCompile(`proxy_cache\s+(\S+);`)
|
||||
if cm := cachePattern.FindStringSubmatch(blockContent); cm != nil && cm[1] != "off" {
|
||||
proxy.Cache = true
|
||||
}
|
||||
|
||||
// 解析 resolver
|
||||
resolverPattern := regexp.MustCompile(`resolver\s+([^;]+);`)
|
||||
if rm := resolverPattern.FindStringSubmatch(blockContent); rm != nil {
|
||||
parts := strings.Fields(rm[1])
|
||||
proxy.Resolver = parts
|
||||
proxy.AutoRefresh = true // 有 resolver 通常意味着需要自动刷新
|
||||
}
|
||||
|
||||
// 解析 resolver_timeout
|
||||
resolverTimeoutPattern := regexp.MustCompile(`resolver_timeout\s+(\d+)([smh]?);`)
|
||||
if rtm := resolverTimeoutPattern.FindStringSubmatch(blockContent); rtm != nil {
|
||||
value, _ := strconv.Atoi(rtm[1])
|
||||
unit := rtm[2]
|
||||
switch unit {
|
||||
case "m":
|
||||
proxy.ResolverTimeout = time.Duration(value) * time.Minute
|
||||
case "h":
|
||||
proxy.ResolverTimeout = time.Duration(value) * time.Hour
|
||||
default:
|
||||
proxy.ResolverTimeout = time.Duration(value) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 sub_filter (响应内容替换)
|
||||
subFilterPattern := regexp.MustCompile(`sub_filter\s+"([^"]+)"\s+"([^"]*)";`)
|
||||
subFilterMatches := subFilterPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
for _, sfm := range subFilterMatches {
|
||||
proxy.Replaces[sfm[1]] = sfm[2]
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// writeProxyFiles 将代理配置写入文件
|
||||
func writeProxyFiles(vhostDir string, proxies []types.Proxy) error {
|
||||
// 删除现有的代理配置文件 (200-299)
|
||||
if err := clearProxyFiles(vhostDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
for i, proxy := range proxies {
|
||||
num := ProxyStartNum + i
|
||||
if num > ProxyEndNum {
|
||||
return fmt.Errorf("proxy rules exceed limit (%d)", ProxyEndNum-ProxyStartNum+1)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%03d-proxy.conf", num)
|
||||
filePath := filepath.Join(vhostDir, fileName)
|
||||
|
||||
content := generateProxyConfig(proxy)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write proxy config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearProxyFiles 清除所有代理配置文件
|
||||
func clearProxyFiles(vhostDir string) error {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := proxyFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num >= ProxyStartNum && num <= ProxyEndNum {
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete proxy config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateProxyConfig 生成代理配置内容
|
||||
func generateProxyConfig(proxy types.Proxy) string {
|
||||
var sb strings.Builder
|
||||
|
||||
location := proxy.Location
|
||||
if location == "" {
|
||||
location = "/"
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass))
|
||||
sb.WriteString(fmt.Sprintf("location %s {\n", location))
|
||||
|
||||
// resolver 配置(如果启用自动刷新)
|
||||
if proxy.AutoRefresh && len(proxy.Resolver) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(proxy.Resolver, " ")))
|
||||
if proxy.ResolverTimeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver_timeout %ds;\n", int(proxy.ResolverTimeout.Seconds())))
|
||||
}
|
||||
// 使用变量实现动态解析
|
||||
sb.WriteString(fmt.Sprintf(" set $backend \"%s\";\n", proxy.Pass))
|
||||
sb.WriteString(" proxy_pass $backend;\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_pass %s;\n", proxy.Pass))
|
||||
}
|
||||
|
||||
// Host 头
|
||||
if proxy.Host != "" {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_set_header Host \"%s\";\n", proxy.Host))
|
||||
} else {
|
||||
sb.WriteString(" proxy_set_header Host $host;\n")
|
||||
}
|
||||
|
||||
// 标准代理头
|
||||
sb.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n")
|
||||
sb.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||||
sb.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n")
|
||||
|
||||
// SNI 配置
|
||||
if proxy.SNI != "" {
|
||||
sb.WriteString(" proxy_ssl_server_name on;\n")
|
||||
sb.WriteString(fmt.Sprintf(" proxy_ssl_name %s;\n", proxy.SNI))
|
||||
}
|
||||
|
||||
// Buffering 配置
|
||||
if proxy.Buffering {
|
||||
sb.WriteString(" proxy_buffering on;\n")
|
||||
} else {
|
||||
sb.WriteString(" proxy_buffering off;\n")
|
||||
}
|
||||
|
||||
// Cache 配置
|
||||
if proxy.Cache {
|
||||
sb.WriteString(" proxy_cache proxy_cache;\n")
|
||||
sb.WriteString(" proxy_cache_valid 200 302 10m;\n")
|
||||
sb.WriteString(" proxy_cache_valid 404 1m;\n")
|
||||
}
|
||||
|
||||
// 响应内容替换
|
||||
if len(proxy.Replaces) > 0 {
|
||||
sb.WriteString(" proxy_set_header Accept-Encoding \"\";\n")
|
||||
sb.WriteString(" sub_filter_once off;\n")
|
||||
for from, to := range proxy.Replaces {
|
||||
sb.WriteString(fmt.Sprintf(" sub_filter \"%s\" \"%s\";\n", from, to))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
211
pkg/webserver/nginx/redirect.go
Normal file
211
pkg/webserver/nginx/redirect.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// redirectFilePattern 匹配重定向配置文件名 (100-199)
|
||||
var redirectFilePattern = regexp.MustCompile(`^(\d{3})-redirect\.conf$`)
|
||||
|
||||
// parseRedirectFiles 从 vhost 目录解析所有重定向配置
|
||||
func parseRedirectFiles(vhostDir string) ([]types.Redirect, error) {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var redirects []types.Redirect
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := redirectFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num < RedirectStartNum || num > RedirectEndNum {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
redirect, err := parseRedirectFile(filePath)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if redirect != nil {
|
||||
redirects = append(redirects, *redirect)
|
||||
}
|
||||
}
|
||||
|
||||
return redirects, nil
|
||||
}
|
||||
|
||||
// parseRedirectFile 解析单个重定向配置文件
|
||||
func parseRedirectFile(filePath string) (*types.Redirect, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// 解析 URL 重定向: location = /old { return 308 /new; }
|
||||
urlPattern := regexp.MustCompile(`location\s*=\s*(\S+)\s*\{[^}]*return\s+(\d+)\s+([^;]+);`)
|
||||
if matches := urlPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
statusCode, _ := strconv.Atoi(matches[2])
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: matches[1],
|
||||
To: strings.TrimSpace(matches[3]),
|
||||
KeepURI: strings.Contains(matches[3], "$request_uri"),
|
||||
StatusCode: statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析 Host 重定向: if ($host = "old.example.com") { return 308 https://new.example.com$request_uri; }
|
||||
hostPattern := regexp.MustCompile(`if\s*\(\s*\$host\s*=\s*"?([^")\s]+)"?\s*\)\s*\{[^}]*return\s+(\d+)\s+([^;]+);`)
|
||||
if matches := hostPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
statusCode, _ := strconv.Atoi(matches[2])
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectTypeHost,
|
||||
From: matches[1],
|
||||
To: strings.TrimSpace(matches[3]),
|
||||
KeepURI: strings.Contains(matches[3], "$request_uri"),
|
||||
StatusCode: statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析 404 重定向: error_page 404 = @redirect_404; location @redirect_404 { return 308 /custom; }
|
||||
errorPattern := regexp.MustCompile(`error_page\s+404\s*=\s*@redirect_404;[^@]*location\s+@redirect_404\s*\{[^}]*return\s+(\d+)\s+([^;]+);`)
|
||||
if matches := errorPattern.FindStringSubmatch(contentStr); matches != nil {
|
||||
statusCode, _ := strconv.Atoi(matches[1])
|
||||
return &types.Redirect{
|
||||
Type: types.RedirectType404,
|
||||
From: "",
|
||||
To: strings.TrimSpace(matches[2]),
|
||||
KeepURI: strings.Contains(matches[2], "$request_uri"),
|
||||
StatusCode: statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// writeRedirectFiles 将重定向配置写入文件
|
||||
func writeRedirectFiles(vhostDir string, redirects []types.Redirect) error {
|
||||
// 删除现有的重定向配置文件 (100-199)
|
||||
if err := clearRedirectFiles(vhostDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
for i, redirect := range redirects {
|
||||
num := RedirectStartNum + i
|
||||
if num > RedirectEndNum {
|
||||
return fmt.Errorf("redirect rules exceed limit (%d)", RedirectEndNum-RedirectStartNum+1)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%03d-redirect.conf", num)
|
||||
filePath := filepath.Join(vhostDir, fileName)
|
||||
|
||||
content := generateRedirectConfig(redirect)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write redirect config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearRedirectFiles 清除所有重定向配置文件
|
||||
func clearRedirectFiles(vhostDir string) error {
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := redirectFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num >= RedirectStartNum && num <= RedirectEndNum {
|
||||
filePath := filepath.Join(vhostDir, entry.Name())
|
||||
if err = os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete redirect config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRedirectConfig 生成重定向配置内容
|
||||
func generateRedirectConfig(redirect types.Redirect) string {
|
||||
statusCode := redirect.StatusCode
|
||||
if statusCode == 0 {
|
||||
statusCode = 308 // 默认使用 308 永久重定向
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
switch redirect.Type {
|
||||
case types.RedirectTypeURL:
|
||||
// URL 重定向
|
||||
sb.WriteString(fmt.Sprintf("# URL redirect: %s -> %s\n", redirect.From, redirect.To))
|
||||
sb.WriteString(fmt.Sprintf("location = %s {\n", redirect.From))
|
||||
if redirect.KeepURI {
|
||||
sb.WriteString(fmt.Sprintf(" return %d %s$request_uri;\n", statusCode, redirect.To))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" return %d %s;\n", statusCode, redirect.To))
|
||||
}
|
||||
sb.WriteString("}\n")
|
||||
|
||||
case types.RedirectTypeHost:
|
||||
// Host 重定向
|
||||
sb.WriteString(fmt.Sprintf("# Host redirect: %s -> %s\n", redirect.From, redirect.To))
|
||||
sb.WriteString(fmt.Sprintf("if ($host = \"%s\") {\n", redirect.From))
|
||||
if redirect.KeepURI {
|
||||
sb.WriteString(fmt.Sprintf(" return %d %s$request_uri;\n", statusCode, redirect.To))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" return %d %s;\n", statusCode, redirect.To))
|
||||
}
|
||||
sb.WriteString("}\n")
|
||||
|
||||
case types.RedirectType404:
|
||||
// 404 重定向
|
||||
sb.WriteString(fmt.Sprintf("# 404 redirect -> %s\n", redirect.To))
|
||||
sb.WriteString("error_page 404 = @redirect_404;\n")
|
||||
sb.WriteString("location @redirect_404 {\n")
|
||||
if redirect.KeepURI {
|
||||
sb.WriteString(fmt.Sprintf(" return %d %s$request_uri;\n", statusCode, redirect.To))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" return %d %s;\n", statusCode, redirect.To))
|
||||
}
|
||||
sb.WriteString("}\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
4
pkg/webserver/nginx/testdata/http.conf
vendored
4
pkg/webserver/nginx/testdata/http.conf
vendored
@@ -1,4 +1,4 @@
|
||||
include /opt/ace/sites/default/config/http.d/*.conf;
|
||||
include /opt/ace/sites/default/config/global/*.conf;
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
@@ -7,7 +7,7 @@ server {
|
||||
# error page
|
||||
error_page 404 /404.html;
|
||||
# custom configs
|
||||
include /opt/ace/sites/default/config/server.d/*.conf;
|
||||
include /opt/ace/sites/default/config/vhost/*.conf;
|
||||
# browser cache
|
||||
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||
expires 30d;
|
||||
|
||||
4
pkg/webserver/nginx/testdata/https.conf
vendored
4
pkg/webserver/nginx/testdata/https.conf
vendored
@@ -1,4 +1,4 @@
|
||||
include /opt/ace/sites/default/config/http.d/*.conf;
|
||||
include /opt/ace/sites/default/config/global/*.conf;
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
@@ -15,7 +15,7 @@ server {
|
||||
# error page
|
||||
error_page 404 /404.html;
|
||||
# custom configs
|
||||
include /opt/ace/sites/default/config/server.d/*.conf;
|
||||
include /opt/ace/sites/default/config/vhost/*.conf;
|
||||
# browser cache
|
||||
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||
expires 30d;
|
||||
|
||||
202
pkg/webserver/nginx/upstream.go
Normal file
202
pkg/webserver/nginx/upstream.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// upstreamFilePattern 匹配 upstream 配置文件名 (100-XXX-name.conf)
|
||||
var upstreamFilePattern = regexp.MustCompile(`^(\d{3})-(.+)\.conf$`)
|
||||
|
||||
// parseUpstreamFiles 从 global 目录解析所有 upstream 配置
|
||||
func parseUpstreamFiles(globalDir string) (map[string]types.Upstream, error) {
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreams := make(map[string]types.Upstream)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := upstreamFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num < UpstreamStartNum {
|
||||
continue
|
||||
}
|
||||
|
||||
name := matches[2]
|
||||
filePath := filepath.Join(globalDir, entry.Name())
|
||||
upstream, err := parseUpstreamFile(filePath, name)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if upstream != nil {
|
||||
upstreams[name] = *upstream
|
||||
}
|
||||
}
|
||||
|
||||
return upstreams, nil
|
||||
}
|
||||
|
||||
// parseUpstreamFile 解析单个 upstream 配置文件
|
||||
func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// 解析 upstream 块
|
||||
// upstream backend {
|
||||
// least_conn;
|
||||
// server 127.0.0.1:8080 weight=5;
|
||||
// keepalive 32;
|
||||
// }
|
||||
upstreamPattern := regexp.MustCompile(`upstream\s+(\S+)\s*\{([^}]+)}`)
|
||||
matches := upstreamPattern.FindStringSubmatch(contentStr)
|
||||
if matches == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
name := matches[1]
|
||||
if expectedName != "" && name != expectedName {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
blockContent := matches[2]
|
||||
upstream := &types.Upstream{
|
||||
Servers: make(map[string]string),
|
||||
}
|
||||
|
||||
// 解析负载均衡算法
|
||||
algoPatterns := []string{"least_conn", "ip_hash", "hash", "random"}
|
||||
for _, algo := range algoPatterns {
|
||||
if regexp.MustCompile(`\b` + algo + `\b`).MatchString(blockContent) {
|
||||
upstream.Algo = algo
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 server 指令
|
||||
serverPattern := regexp.MustCompile(`server\s+(\S+)(?:\s+([^;]+))?;`)
|
||||
serverMatches := serverPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
for _, sm := range serverMatches {
|
||||
addr := sm[1]
|
||||
options := ""
|
||||
if len(sm) > 2 {
|
||||
options = strings.TrimSpace(sm[2])
|
||||
}
|
||||
upstream.Servers[addr] = options
|
||||
}
|
||||
|
||||
// 解析 keepalive 指令
|
||||
keepalivePattern := regexp.MustCompile(`keepalive\s+(\d+);`)
|
||||
if km := keepalivePattern.FindStringSubmatch(blockContent); km != nil {
|
||||
upstream.Keepalive, _ = strconv.Atoi(km[1])
|
||||
}
|
||||
|
||||
return upstream, nil
|
||||
}
|
||||
|
||||
// writeUpstreamFiles 将 upstream 配置写入文件
|
||||
func writeUpstreamFiles(globalDir string, upstreams map[string]types.Upstream) error {
|
||||
// 删除现有的 upstream 配置文件
|
||||
if err := clearUpstreamFiles(globalDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
num := UpstreamStartNum
|
||||
for name, upstream := range upstreams {
|
||||
fileName := fmt.Sprintf("%03d-%s.conf", num, name)
|
||||
filePath := filepath.Join(globalDir, fileName)
|
||||
|
||||
content := generateUpstreamConfig(name, upstream)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write upstream config: %w", err)
|
||||
}
|
||||
num++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearUpstreamFiles 清除所有 upstream 配置文件
|
||||
func clearUpstreamFiles(globalDir string) error {
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := upstreamFilePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
if num >= UpstreamStartNum {
|
||||
filePath := filepath.Join(globalDir, entry.Name())
|
||||
if err = os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete upstream config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateUpstreamConfig 生成 upstream 配置内容
|
||||
func generateUpstreamConfig(name string, upstream types.Upstream) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Upstream: %s\n", name))
|
||||
sb.WriteString(fmt.Sprintf("upstream %s {\n", name))
|
||||
|
||||
// 负载均衡算法
|
||||
if upstream.Algo != "" {
|
||||
sb.WriteString(fmt.Sprintf(" %s;\n", upstream.Algo))
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
for addr, options := range upstream.Servers {
|
||||
if options != "" {
|
||||
sb.WriteString(fmt.Sprintf(" server %s %s;\n", addr, options))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" server %s;\n", addr))
|
||||
}
|
||||
}
|
||||
|
||||
// keepalive 连接数
|
||||
if upstream.Keepalive > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" keepalive %d;\n", upstream.Keepalive))
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -10,16 +10,34 @@ import (
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// Vhost Nginx 虚拟主机实现
|
||||
type Vhost struct {
|
||||
// StaticVhost 纯静态虚拟主机
|
||||
type StaticVhost struct {
|
||||
*baseVhost
|
||||
}
|
||||
|
||||
// PHPVhost PHP 虚拟主机
|
||||
type PHPVhost struct {
|
||||
*baseVhost
|
||||
}
|
||||
|
||||
// ProxyVhost 反向代理虚拟主机
|
||||
type ProxyVhost struct {
|
||||
*baseVhost
|
||||
}
|
||||
|
||||
// baseVhost Nginx 虚拟主机基础实现
|
||||
type baseVhost struct {
|
||||
parser *Parser
|
||||
configDir string // 配置目录
|
||||
}
|
||||
|
||||
// NewVhost 创建 Nginx 虚拟主机实例
|
||||
// configDir: 配置目录路径
|
||||
func NewVhost(configDir string) (*Vhost, error) {
|
||||
v := &Vhost{
|
||||
// newBaseVhost 创建基础虚拟主机实例
|
||||
func newBaseVhost(configDir string) (*baseVhost, error) {
|
||||
if configDir == "" {
|
||||
return nil, fmt.Errorf("config directory is required")
|
||||
}
|
||||
|
||||
v := &baseVhost{
|
||||
configDir: configDir,
|
||||
}
|
||||
|
||||
@@ -27,14 +45,12 @@ func NewVhost(configDir string) (*Vhost, error) {
|
||||
var parser *Parser
|
||||
var err error
|
||||
|
||||
if v.configDir != "" {
|
||||
// 从配置目录加载主配置文件
|
||||
configFile := filepath.Join(v.configDir, "nginx.conf")
|
||||
if _, statErr := os.Stat(configFile); statErr == nil {
|
||||
parser, err = NewParserFromFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load nginx config: %w", err)
|
||||
}
|
||||
// 从配置目录加载主配置文件
|
||||
configFile := filepath.Join(v.configDir, "nginx.conf")
|
||||
if _, statErr := os.Stat(configFile); statErr == nil {
|
||||
parser, err = NewParserFromFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load nginx config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,27 +61,49 @@ func NewVhost(configDir string) (*Vhost, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load default config: %w", err)
|
||||
}
|
||||
// 如果有 configDir,设置配置文件路径
|
||||
if v.configDir != "" {
|
||||
parser.SetConfigPath(filepath.Join(v.configDir, "nginx.conf"))
|
||||
}
|
||||
parser.SetConfigPath(filepath.Join(v.configDir, "nginx.conf"))
|
||||
}
|
||||
|
||||
v.parser = parser
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ========== VhostCore 接口实现 ==========
|
||||
// NewStaticVhost 创建纯静态虚拟主机实例
|
||||
func NewStaticVhost(configDir string) (*StaticVhost, error) {
|
||||
base, err := newBaseVhost(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StaticVhost{baseVhost: base}, nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Enable() bool {
|
||||
// NewPHPVhost 创建 PHP 虚拟主机实例
|
||||
func NewPHPVhost(configDir string) (*PHPVhost, error) {
|
||||
base, err := newBaseVhost(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PHPVhost{baseVhost: base}, nil
|
||||
}
|
||||
|
||||
// NewProxyVhost 创建反向代理虚拟主机实例
|
||||
func NewProxyVhost(configDir string) (*ProxyVhost, error) {
|
||||
base, err := newBaseVhost(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ProxyVhost{baseVhost: base}, nil
|
||||
}
|
||||
|
||||
func (v *baseVhost) Enable() bool {
|
||||
// 检查禁用配置文件是否存在
|
||||
disableFile := filepath.Join(v.configDir, "server.d", DisableConfName)
|
||||
disableFile := filepath.Join(v.configDir, "vhost", DisableConfName)
|
||||
_, err := os.Stat(disableFile)
|
||||
return os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (v *Vhost) SetEnable(enable bool, _ ...string) error {
|
||||
serverDir := filepath.Join(v.configDir, "server.d")
|
||||
func (v *baseVhost) SetEnable(enable bool, _ ...string) error {
|
||||
serverDir := filepath.Join(v.configDir, "vhost")
|
||||
disableFile := filepath.Join(serverDir, DisableConfName)
|
||||
|
||||
if enable {
|
||||
@@ -77,12 +115,6 @@ func (v *Vhost) SetEnable(enable bool, _ ...string) error {
|
||||
}
|
||||
|
||||
// 禁用:创建禁用配置文件
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(serverDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// 写入禁用配置
|
||||
if err := os.WriteFile(disableFile, []byte(DisableConfContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write disable config: %w", err)
|
||||
}
|
||||
@@ -90,7 +122,7 @@ func (v *Vhost) SetEnable(enable bool, _ ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Listen() []types.Listen {
|
||||
func (v *baseVhost) Listen() []types.Listen {
|
||||
listens, err := v.parser.GetListen()
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -132,7 +164,7 @@ func (v *Vhost) Listen() []types.Listen {
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Vhost) SetListen(listens []types.Listen) error {
|
||||
func (v *baseVhost) SetListen(listens []types.Listen) error {
|
||||
// 将通用 Listen 转换为 Nginx 格式
|
||||
var nginxListens [][]string
|
||||
for _, l := range listens {
|
||||
@@ -163,7 +195,7 @@ func (v *Vhost) SetListen(listens []types.Listen) error {
|
||||
return v.parser.SetListen(nginxListens)
|
||||
}
|
||||
|
||||
func (v *Vhost) ServerName() []string {
|
||||
func (v *baseVhost) ServerName() []string {
|
||||
names, err := v.parser.GetServerName()
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -171,11 +203,11 @@ func (v *Vhost) ServerName() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
func (v *Vhost) SetServerName(serverName []string) error {
|
||||
func (v *baseVhost) SetServerName(serverName []string) error {
|
||||
return v.parser.SetServerName(serverName)
|
||||
}
|
||||
|
||||
func (v *Vhost) Index() []string {
|
||||
func (v *baseVhost) Index() []string {
|
||||
index, err := v.parser.GetIndex()
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -183,11 +215,11 @@ func (v *Vhost) Index() []string {
|
||||
return index
|
||||
}
|
||||
|
||||
func (v *Vhost) SetIndex(index []string) error {
|
||||
func (v *baseVhost) SetIndex(index []string) error {
|
||||
return v.parser.SetIndex(index)
|
||||
}
|
||||
|
||||
func (v *Vhost) Root() string {
|
||||
func (v *baseVhost) Root() string {
|
||||
root, err := v.parser.GetRoot()
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -195,11 +227,11 @@ func (v *Vhost) Root() string {
|
||||
return root
|
||||
}
|
||||
|
||||
func (v *Vhost) SetRoot(root string) error {
|
||||
func (v *baseVhost) SetRoot(root string) error {
|
||||
return v.parser.SetRoot(root)
|
||||
}
|
||||
|
||||
func (v *Vhost) Includes() []types.IncludeFile {
|
||||
func (v *baseVhost) Includes() []types.IncludeFile {
|
||||
includes, comments, err := v.parser.GetIncludes()
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -219,7 +251,7 @@ func (v *Vhost) Includes() []types.IncludeFile {
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Vhost) SetIncludes(includes []types.IncludeFile) error {
|
||||
func (v *baseVhost) SetIncludes(includes []types.IncludeFile) error {
|
||||
var paths []string
|
||||
var comments [][]string
|
||||
|
||||
@@ -231,7 +263,7 @@ func (v *Vhost) SetIncludes(includes []types.IncludeFile) error {
|
||||
return v.parser.SetIncludes(paths, comments)
|
||||
}
|
||||
|
||||
func (v *Vhost) AccessLog() string {
|
||||
func (v *baseVhost) AccessLog() string {
|
||||
log, err := v.parser.GetAccessLog()
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -239,11 +271,11 @@ func (v *Vhost) AccessLog() string {
|
||||
return log
|
||||
}
|
||||
|
||||
func (v *Vhost) SetAccessLog(accessLog string) error {
|
||||
func (v *baseVhost) SetAccessLog(accessLog string) error {
|
||||
return v.parser.SetAccessLog(accessLog)
|
||||
}
|
||||
|
||||
func (v *Vhost) ErrorLog() string {
|
||||
func (v *baseVhost) ErrorLog() string {
|
||||
log, err := v.parser.GetErrorLog()
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -251,48 +283,24 @@ func (v *Vhost) ErrorLog() string {
|
||||
return log
|
||||
}
|
||||
|
||||
func (v *Vhost) SetErrorLog(errorLog string) error {
|
||||
func (v *baseVhost) SetErrorLog(errorLog string) error {
|
||||
return v.parser.SetErrorLog(errorLog)
|
||||
}
|
||||
|
||||
func (v *Vhost) Save() error {
|
||||
func (v *baseVhost) Save() error {
|
||||
return v.parser.Save()
|
||||
}
|
||||
|
||||
func (v *Vhost) Reload() error {
|
||||
// 重载 Nginx 配置
|
||||
// 优先使用 openresty,如果不存在则使用 nginx
|
||||
cmds := []string{
|
||||
"/opt/ace/apps/openresty/bin/openresty -s reload",
|
||||
"/usr/sbin/nginx -s reload",
|
||||
"nginx -s reload",
|
||||
func (v *baseVhost) Reload() error {
|
||||
parts := strings.Fields("systemctl reload openresty")
|
||||
if err := exec.Command(parts[0], parts[1:]...).Run(); err != nil {
|
||||
return fmt.Errorf("failed to reload nginx config: %w", err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, cmd := range cmds {
|
||||
parts := strings.Fields(cmd)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查命令是否存在
|
||||
if _, err := os.Stat(parts[0]); err == nil {
|
||||
// 执行重载命令
|
||||
err := exec.Command(parts[0], parts[1:]...).Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("failed to reload nginx config: %w", lastErr)
|
||||
}
|
||||
return fmt.Errorf("nginx or openresty command not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) Reset() error {
|
||||
func (v *baseVhost) Reset() error {
|
||||
// 重置配置为默认值
|
||||
parser, err := NewParser("")
|
||||
if err != nil {
|
||||
@@ -308,13 +316,11 @@ func (v *Vhost) Reset() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== VhostSSL 接口实现 ==========
|
||||
|
||||
func (v *Vhost) HTTPS() bool {
|
||||
func (v *baseVhost) HTTPS() bool {
|
||||
return v.parser.GetHTTPS()
|
||||
}
|
||||
|
||||
func (v *Vhost) SSLConfig() *types.SSLConfig {
|
||||
func (v *baseVhost) SSLConfig() *types.SSLConfig {
|
||||
if !v.HTTPS() {
|
||||
return nil
|
||||
}
|
||||
@@ -329,7 +335,7 @@ func (v *Vhost) SSLConfig() *types.SSLConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Vhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
func (v *baseVhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("SSL config cannot be nil")
|
||||
}
|
||||
@@ -378,41 +384,11 @@ func (v *Vhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vhost) ClearHTTPS() error {
|
||||
func (v *baseVhost) ClearHTTPS() error {
|
||||
return v.parser.ClearHTTPS()
|
||||
}
|
||||
|
||||
// ========== VhostPHP 接口实现 ==========
|
||||
|
||||
func (v *Vhost) PHP() int {
|
||||
return v.parser.GetPHP()
|
||||
}
|
||||
|
||||
func (v *Vhost) SetPHP(version int) error {
|
||||
// 先移除所有 PHP 相关的 include
|
||||
includes := v.Includes()
|
||||
var newIncludes []types.IncludeFile
|
||||
for _, inc := range includes {
|
||||
// 过滤掉 enable-php-*.conf
|
||||
if !strings.HasPrefix(inc.Path, "enable-php-") || !strings.HasSuffix(inc.Path, ".conf") {
|
||||
newIncludes = append(newIncludes, inc)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果版本不为 0,添加新的 PHP include
|
||||
if version > 0 {
|
||||
newIncludes = append(newIncludes, types.IncludeFile{
|
||||
Path: fmt.Sprintf("enable-php-%d.conf", version),
|
||||
Comment: []string{fmt.Sprintf("# Enable PHP %d.%d", version/10, version%10)},
|
||||
})
|
||||
}
|
||||
|
||||
return v.SetIncludes(newIncludes)
|
||||
}
|
||||
|
||||
// ========== VhostAdvanced 接口实现 ==========
|
||||
|
||||
func (v *Vhost) RateLimit() *types.RateLimit {
|
||||
func (v *baseVhost) RateLimit() *types.RateLimit {
|
||||
rate := v.parser.GetLimitRate()
|
||||
limitConn := v.parser.GetLimitConn()
|
||||
|
||||
@@ -437,7 +413,7 @@ func (v *Vhost) RateLimit() *types.RateLimit {
|
||||
return rateLimit
|
||||
}
|
||||
|
||||
func (v *Vhost) SetRateLimit(limit *types.RateLimit) error {
|
||||
func (v *baseVhost) SetRateLimit(limit *types.RateLimit) error {
|
||||
if limit == nil {
|
||||
// 清除限流配置
|
||||
if err := v.parser.SetLimitRate(""); err != nil {
|
||||
@@ -460,7 +436,7 @@ func (v *Vhost) SetRateLimit(limit *types.RateLimit) error {
|
||||
return v.parser.SetLimitConn(limitConns)
|
||||
}
|
||||
|
||||
func (v *Vhost) BasicAuth() map[string]string {
|
||||
func (v *baseVhost) BasicAuth() map[string]string {
|
||||
realm, userFile := v.parser.GetBasicAuth()
|
||||
if realm == "" || userFile == "" {
|
||||
return nil
|
||||
@@ -474,7 +450,7 @@ func (v *Vhost) BasicAuth() map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Vhost) SetBasicAuth(auth map[string]string) error {
|
||||
func (v *baseVhost) SetBasicAuth(auth map[string]string) error {
|
||||
if auth == nil || len(auth) == 0 {
|
||||
// 清除基本认证配置
|
||||
return v.parser.SetBasicAuth("", "")
|
||||
@@ -489,3 +465,76 @@ func (v *Vhost) SetBasicAuth(auth map[string]string) error {
|
||||
|
||||
return v.parser.SetBasicAuth(realm, userFile)
|
||||
}
|
||||
|
||||
func (v *baseVhost) Redirects() []types.Redirect {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
redirects, _ := parseRedirectFiles(vhostDir)
|
||||
return redirects
|
||||
}
|
||||
|
||||
func (v *baseVhost) SetRedirects(redirects []types.Redirect) error {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
return writeRedirectFiles(vhostDir, redirects)
|
||||
}
|
||||
|
||||
// ========== PHPVhost ==========
|
||||
|
||||
func (v *PHPVhost) PHP() int {
|
||||
return v.parser.GetPHP()
|
||||
}
|
||||
|
||||
func (v *PHPVhost) SetPHP(version int) error {
|
||||
// 先移除所有 PHP 相关的 include
|
||||
includes := v.Includes()
|
||||
var newIncludes []types.IncludeFile
|
||||
for _, inc := range includes {
|
||||
// 过滤掉 enable-php-*.conf
|
||||
if !strings.HasPrefix(inc.Path, "enable-php-") || !strings.HasSuffix(inc.Path, ".conf") {
|
||||
newIncludes = append(newIncludes, inc)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果版本不为 0,添加新的 PHP include
|
||||
if version > 0 {
|
||||
newIncludes = append(newIncludes, types.IncludeFile{
|
||||
Path: fmt.Sprintf("enable-php-%d.conf", version),
|
||||
Comment: []string{fmt.Sprintf("# Enable PHP %d.%d", version/10, version%10)},
|
||||
})
|
||||
}
|
||||
|
||||
return v.SetIncludes(newIncludes)
|
||||
}
|
||||
|
||||
// ========== ProxyVhost ==========
|
||||
|
||||
func (v *ProxyVhost) Proxies() []types.Proxy {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
proxies, _ := parseProxyFiles(vhostDir)
|
||||
return proxies
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) SetProxies(proxies []types.Proxy) error {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
return writeProxyFiles(vhostDir, proxies)
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) ClearProxies() error {
|
||||
vhostDir := filepath.Join(v.configDir, "vhost")
|
||||
return clearProxyFiles(vhostDir)
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) Upstreams() map[string]types.Upstream {
|
||||
globalDir := filepath.Join(v.configDir, "global")
|
||||
upstreams, _ := parseUpstreamFiles(globalDir)
|
||||
return upstreams
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) SetUpstreams(upstreams map[string]types.Upstream) error {
|
||||
globalDir := filepath.Join(v.configDir, "global")
|
||||
return writeUpstreamFiles(globalDir, upstreams)
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) ClearUpstreams() error {
|
||||
globalDir := filepath.Join(v.configDir, "global")
|
||||
return clearUpstreamFiles(globalDir)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
type VhostTestSuite struct {
|
||||
suite.Suite
|
||||
vhost *Vhost
|
||||
vhost *PHPVhost
|
||||
configDir string
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ func (s *VhostTestSuite) SetupTest() {
|
||||
s.Require().NoError(err)
|
||||
s.configDir = configDir
|
||||
|
||||
// 创建 server.d 目录
|
||||
err = os.MkdirAll(filepath.Join(configDir, "server.d"), 0755)
|
||||
// 创建 vhost 目录
|
||||
err = os.MkdirAll(filepath.Join(configDir, "vhost"), 0755)
|
||||
s.Require().NoError(err)
|
||||
|
||||
vhost, err := NewVhost(configDir)
|
||||
vhost, err := NewPHPVhost(configDir)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(vhost)
|
||||
s.vhost = vhost
|
||||
@@ -45,12 +45,12 @@ func (s *VhostTestSuite) TearDownTest() {
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestNewVhost() {
|
||||
s.Equal(s.configDir, s.vhost.configDir)
|
||||
s.NotNil(s.vhost.parser)
|
||||
s.Equal(s.configDir, s.vhost.baseVhost.configDir)
|
||||
s.NotNil(s.vhost.baseVhost.parser)
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestEnable() {
|
||||
// 默认应该是启用状态(没有 00-disable.conf)
|
||||
// 默认应该是启用状态(没有 000-disable.conf)
|
||||
s.True(s.vhost.Enable())
|
||||
|
||||
// 禁用网站
|
||||
@@ -58,7 +58,7 @@ func (s *VhostTestSuite) TestEnable() {
|
||||
s.False(s.vhost.Enable())
|
||||
|
||||
// 验证禁用文件存在
|
||||
disableFile := filepath.Join(s.configDir, "server.d", DisableConfName)
|
||||
disableFile := filepath.Join(s.configDir, "vhost", DisableConfName)
|
||||
_, err := os.Stat(disableFile)
|
||||
s.NoError(err)
|
||||
|
||||
@@ -76,7 +76,7 @@ func (s *VhostTestSuite) TestDisableConfigContent() {
|
||||
s.NoError(s.vhost.SetEnable(false))
|
||||
|
||||
// 读取禁用配置内容
|
||||
disableFile := filepath.Join(s.configDir, "server.d", DisableConfName)
|
||||
disableFile := filepath.Join(s.configDir, "vhost", DisableConfName)
|
||||
content, err := os.ReadFile(disableFile)
|
||||
s.NoError(err)
|
||||
|
||||
@@ -343,7 +343,319 @@ func (s *VhostTestSuite) TestAltSvc() {
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestDefaultConfIncludesServerD() {
|
||||
// 验证默认配置包含 server.d 的 include
|
||||
s.Contains(DefaultConf, "server.d")
|
||||
// 验证默认配置包含 vhost 的 include
|
||||
s.Contains(DefaultConf, "vhost")
|
||||
s.Contains(DefaultConf, "include")
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirects() {
|
||||
// 初始应该没有重定向
|
||||
s.Empty(s.vhost.Redirects())
|
||||
|
||||
// 设置重定向
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: "/old",
|
||||
To: "/new",
|
||||
StatusCode: 301,
|
||||
},
|
||||
{
|
||||
Type: types.RedirectTypeHost,
|
||||
From: "old.example.com",
|
||||
To: "https://new.example.com",
|
||||
KeepURI: true,
|
||||
StatusCode: 308,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 验证重定向文件已创建
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
s.NoError(err)
|
||||
|
||||
redirectCount := 0
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "1") && strings.HasSuffix(entry.Name(), "-redirect.conf") {
|
||||
redirectCount++
|
||||
}
|
||||
}
|
||||
s.Equal(2, redirectCount)
|
||||
|
||||
// 验证可以读取回来
|
||||
got := s.vhost.Redirects()
|
||||
s.Len(got, 2)
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirectURL() {
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectTypeURL,
|
||||
From: "/old-page",
|
||||
To: "/new-page",
|
||||
StatusCode: 301,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "100-redirect.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "location = /old-page")
|
||||
s.Contains(string(content), "return 301")
|
||||
s.Contains(string(content), "/new-page")
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirectHost() {
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectTypeHost,
|
||||
From: "old.example.com",
|
||||
To: "https://new.example.com",
|
||||
KeepURI: true,
|
||||
StatusCode: 308,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "100-redirect.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "$host")
|
||||
s.Contains(string(content), "old.example.com")
|
||||
s.Contains(string(content), "return 308")
|
||||
s.Contains(string(content), "$request_uri")
|
||||
}
|
||||
|
||||
func (s *VhostTestSuite) TestRedirect404() {
|
||||
redirects := []types.Redirect{
|
||||
{
|
||||
Type: types.RedirectType404,
|
||||
To: "/custom-404.html",
|
||||
StatusCode: 308,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetRedirects(redirects))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "100-redirect.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "error_page 404")
|
||||
s.Contains(string(content), "@redirect_404")
|
||||
}
|
||||
|
||||
// ProxyVhost 测试套件
|
||||
type ProxyVhostTestSuite struct {
|
||||
suite.Suite
|
||||
vhost *ProxyVhost
|
||||
configDir string
|
||||
}
|
||||
|
||||
func TestProxyVhostTestSuite(t *testing.T) {
|
||||
suite.Run(t, &ProxyVhostTestSuite{})
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) SetupTest() {
|
||||
configDir, err := os.MkdirTemp("", "nginx-proxy-test-*")
|
||||
s.Require().NoError(err)
|
||||
s.configDir = configDir
|
||||
|
||||
// 创建 vhost 和 global 目录
|
||||
s.NoError(os.MkdirAll(filepath.Join(configDir, "vhost"), 0755))
|
||||
s.NoError(os.MkdirAll(filepath.Join(configDir, "global"), 0755))
|
||||
|
||||
vhost, err := NewProxyVhost(configDir)
|
||||
s.Require().NoError(err)
|
||||
s.vhost = vhost
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TearDownTest() {
|
||||
if s.configDir != "" {
|
||||
s.NoError(os.RemoveAll(s.configDir))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestProxies() {
|
||||
// 初始应该没有代理配置
|
||||
s.Empty(s.vhost.Proxies())
|
||||
|
||||
// 设置代理配置
|
||||
proxies := []types.Proxy{
|
||||
{
|
||||
Location: "/",
|
||||
Pass: "http://backend",
|
||||
Host: "example.com",
|
||||
},
|
||||
{
|
||||
Location: "/api",
|
||||
Pass: "http://api-backend:8080",
|
||||
Buffering: true,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
|
||||
// 验证代理文件已创建
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
entries, err := os.ReadDir(vhostDir)
|
||||
s.NoError(err)
|
||||
|
||||
proxyCount := 0
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "2") && strings.HasSuffix(entry.Name(), "-proxy.conf") {
|
||||
proxyCount++
|
||||
}
|
||||
}
|
||||
s.Equal(2, proxyCount)
|
||||
|
||||
// 验证可以读取回来
|
||||
got := s.vhost.Proxies()
|
||||
s.Len(got, 2)
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestProxyConfig() {
|
||||
proxies := []types.Proxy{
|
||||
{
|
||||
Location: "/",
|
||||
Pass: "http://backend",
|
||||
Host: "example.com",
|
||||
SNI: "example.com",
|
||||
Buffering: true,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
|
||||
// 读取配置文件内容
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "200-proxy.conf"))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "location /")
|
||||
s.Contains(string(content), "proxy_pass http://backend")
|
||||
s.Contains(string(content), "proxy_set_header Host")
|
||||
s.Contains(string(content), "example.com")
|
||||
s.Contains(string(content), "proxy_ssl_name")
|
||||
s.Contains(string(content), "proxy_buffering on")
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestClearProxies() {
|
||||
proxies := []types.Proxy{
|
||||
{Location: "/", Pass: "http://backend"},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
s.Len(s.vhost.Proxies(), 1)
|
||||
|
||||
s.NoError(s.vhost.ClearProxies())
|
||||
s.Empty(s.vhost.Proxies())
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestUpstreams() {
|
||||
// 初始应该没有上游服务器配置
|
||||
s.Empty(s.vhost.Upstreams())
|
||||
|
||||
// 设置上游服务器
|
||||
upstreams := map[string]types.Upstream{
|
||||
"backend": {
|
||||
Servers: map[string]string{
|
||||
"127.0.0.1:8080": "weight=5",
|
||||
"127.0.0.1:8081": "weight=3",
|
||||
},
|
||||
Algo: "least_conn",
|
||||
Keepalive: 32,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
|
||||
// 验证 upstream 文件已创建
|
||||
globalDir := filepath.Join(s.configDir, "global")
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(entries)
|
||||
|
||||
// 验证可以读取回来
|
||||
got := s.vhost.Upstreams()
|
||||
s.Len(got, 1)
|
||||
s.Contains(got, "backend")
|
||||
s.Equal("least_conn", got["backend"].Algo)
|
||||
s.Equal(32, got["backend"].Keepalive)
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestUpstreamConfig() {
|
||||
upstreams := map[string]types.Upstream{
|
||||
"mybackend": {
|
||||
Servers: map[string]string{
|
||||
"127.0.0.1:8080": "weight=5",
|
||||
},
|
||||
Algo: "ip_hash",
|
||||
Keepalive: 16,
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
|
||||
// 读取配置文件内容
|
||||
globalDir := filepath.Join(s.configDir, "global")
|
||||
entries, err := os.ReadDir(globalDir)
|
||||
s.NoError(err)
|
||||
s.Require().NotEmpty(entries)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(globalDir, entries[0].Name()))
|
||||
s.NoError(err)
|
||||
|
||||
s.Contains(string(content), "upstream mybackend")
|
||||
s.Contains(string(content), "ip_hash")
|
||||
s.Contains(string(content), "server 127.0.0.1:8080")
|
||||
s.Contains(string(content), "weight=5")
|
||||
s.Contains(string(content), "keepalive 16")
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestClearUpstreams() {
|
||||
upstreams := map[string]types.Upstream{
|
||||
"backend": {
|
||||
Servers: map[string]string{"127.0.0.1:8080": ""},
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
s.Len(s.vhost.Upstreams(), 1)
|
||||
|
||||
s.NoError(s.vhost.ClearUpstreams())
|
||||
s.Empty(s.vhost.Upstreams())
|
||||
}
|
||||
|
||||
func (s *ProxyVhostTestSuite) TestProxyWithUpstream() {
|
||||
// 先创建 upstream
|
||||
upstreams := map[string]types.Upstream{
|
||||
"api-servers": {
|
||||
Servers: map[string]string{
|
||||
"127.0.0.1:3000": "",
|
||||
"127.0.0.1:3001": "",
|
||||
},
|
||||
Algo: "least_conn",
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetUpstreams(upstreams))
|
||||
|
||||
// 然后创建引用 upstream 的 proxy
|
||||
proxies := []types.Proxy{
|
||||
{
|
||||
Location: "/api",
|
||||
Pass: "http://api-servers",
|
||||
},
|
||||
}
|
||||
s.NoError(s.vhost.SetProxies(proxies))
|
||||
|
||||
// 验证两者都存在
|
||||
s.Len(s.vhost.Upstreams(), 1)
|
||||
s.Len(s.vhost.Proxies(), 1)
|
||||
|
||||
// 验证 proxy 配置中引用了 upstream
|
||||
vhostDir := filepath.Join(s.configDir, "vhost")
|
||||
content, err := os.ReadFile(filepath.Join(vhostDir, "200-proxy.conf"))
|
||||
s.NoError(err)
|
||||
s.Contains(string(content), "http://api-servers")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
@@ -17,7 +18,6 @@ type Proxy struct {
|
||||
|
||||
// Upstream 上游服务器配置
|
||||
type Upstream struct {
|
||||
Name string // 上游名称,如: "backend"
|
||||
Servers map[string]string // 上游服务器及权重,如: map["server1"] = "weight=5"
|
||||
Algo string // 负载均衡算法,如: "least_conn", "ip_hash"
|
||||
Keepalive int // 保持连接数,如: 32
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
package types
|
||||
|
||||
// VhostType 虚拟主机类型
|
||||
type VhostType string
|
||||
|
||||
const (
|
||||
VhostTypeStatic VhostType = "static"
|
||||
VhostTypePHP VhostType = "php"
|
||||
VhostTypeProxy VhostType = "proxy"
|
||||
)
|
||||
|
||||
// Vhost 虚拟主机完整接口
|
||||
// Vhost 虚拟主机通用接口
|
||||
type Vhost interface {
|
||||
VhostCore
|
||||
VhostSSL
|
||||
VhostPHP
|
||||
VhostAdvanced
|
||||
}
|
||||
// ========== 核心方法 ==========
|
||||
|
||||
// VhostCore 核心接口
|
||||
type VhostCore interface {
|
||||
// Enable 取启用状态
|
||||
Enable() bool
|
||||
// SetEnable 设置启用状态及停止页路径
|
||||
@@ -65,10 +50,9 @@ type VhostCore interface {
|
||||
Reload() error
|
||||
// Reset 重置配置为默认值
|
||||
Reset() error
|
||||
}
|
||||
|
||||
// VhostSSL SSL/TLS 相关接口
|
||||
type VhostSSL interface {
|
||||
// ========== SSL/TLS 方法 ==========
|
||||
|
||||
// HTTPS 取 HTTPS 启用状态
|
||||
HTTPS() bool
|
||||
// SSLConfig 取 SSL 配置
|
||||
@@ -77,6 +61,38 @@ type VhostSSL interface {
|
||||
SetSSLConfig(cfg *SSLConfig) error
|
||||
// ClearHTTPS 清除 HTTPS 配置
|
||||
ClearHTTPS() error
|
||||
|
||||
// ========== 高级功能方法 ==========
|
||||
|
||||
// RateLimit 取限流限速配置
|
||||
RateLimit() *RateLimit
|
||||
// SetRateLimit 设置限流限速配置
|
||||
SetRateLimit(limit *RateLimit) error
|
||||
|
||||
// BasicAuth 取基本认证配置
|
||||
BasicAuth() map[string]string
|
||||
// SetBasicAuth 设置基本认证
|
||||
SetBasicAuth(auth map[string]string) error
|
||||
}
|
||||
|
||||
// StaticVhost 纯静态虚拟主机接口
|
||||
type StaticVhost interface {
|
||||
Vhost
|
||||
VhostRedirect
|
||||
}
|
||||
|
||||
// PHPVhost PHP 虚拟主机接口
|
||||
type PHPVhost interface {
|
||||
Vhost
|
||||
VhostPHP
|
||||
VhostRedirect
|
||||
}
|
||||
|
||||
// ProxyVhost 反向代理虚拟主机接口
|
||||
type ProxyVhost interface {
|
||||
Vhost
|
||||
VhostRedirect
|
||||
VhostProxyConfig
|
||||
}
|
||||
|
||||
// VhostPHP PHP 相关接口
|
||||
@@ -87,17 +103,29 @@ type VhostPHP interface {
|
||||
SetPHP(version int) error
|
||||
}
|
||||
|
||||
// VhostAdvanced 高级功能接口
|
||||
type VhostAdvanced interface {
|
||||
// RateLimit 取限流限速配置
|
||||
RateLimit() *RateLimit
|
||||
// SetRateLimit 设置限流限速配置
|
||||
SetRateLimit(limit *RateLimit) error
|
||||
// VhostRedirect 重定向相关接口
|
||||
type VhostRedirect interface {
|
||||
// Redirects 取所有重定向配置
|
||||
Redirects() []Redirect
|
||||
// SetRedirects 设置重定向
|
||||
SetRedirects(redirects []Redirect) error
|
||||
}
|
||||
|
||||
// BasicAuth 取基本认证配置
|
||||
BasicAuth() map[string]string
|
||||
// SetBasicAuth 设置基本认证
|
||||
SetBasicAuth(auth map[string]string) error
|
||||
// VhostProxyConfig 反向代理相关接口
|
||||
type VhostProxyConfig interface {
|
||||
// Proxies 取所有反向代理配置
|
||||
Proxies() []Proxy
|
||||
// SetProxies 设置反向代理配置
|
||||
SetProxies(proxies []Proxy) error
|
||||
// ClearProxies 清除所有反向代理配置
|
||||
ClearProxies() error
|
||||
|
||||
// Upstreams 取上游服务器配置
|
||||
Upstreams() map[string]Upstream
|
||||
// SetUpstreams 设置上游服务器配置
|
||||
SetUpstreams(upstreams map[string]Upstream) error
|
||||
// ClearUpstreams 清除所有上游服务器配置
|
||||
ClearUpstreams() error
|
||||
}
|
||||
|
||||
// Listen 监听配置
|
||||
|
||||
@@ -8,13 +8,37 @@ import (
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
// NewVhost 创建虚拟主机管理实例
|
||||
func NewVhost(serverType Type, configDir string) (types.Vhost, error) {
|
||||
// NewStaticVhost 创建纯静态虚拟主机实例
|
||||
func NewStaticVhost(serverType Type, configDir string) (types.StaticVhost, error) {
|
||||
switch serverType {
|
||||
case TypeNginx:
|
||||
return nginx.NewVhost(configDir)
|
||||
return nginx.NewStaticVhost(configDir)
|
||||
case TypeApache:
|
||||
return apache.NewVhost(configDir)
|
||||
return apache.NewStaticVhost(configDir)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported server type: %s", serverType)
|
||||
}
|
||||
}
|
||||
|
||||
// NewPHPVhost 创建 PHP 虚拟主机实例
|
||||
func NewPHPVhost(serverType Type, configDir string) (types.PHPVhost, error) {
|
||||
switch serverType {
|
||||
case TypeNginx:
|
||||
return nginx.NewPHPVhost(configDir)
|
||||
case TypeApache:
|
||||
return apache.NewPHPVhost(configDir)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported server type: %s", serverType)
|
||||
}
|
||||
}
|
||||
|
||||
// NewProxyVhost 创建反向代理虚拟主机实例
|
||||
func NewProxyVhost(serverType Type, configDir string) (types.ProxyVhost, error) {
|
||||
switch serverType {
|
||||
case TypeNginx:
|
||||
return nginx.NewProxyVhost(configDir)
|
||||
case TypeApache:
|
||||
return apache.NewProxyVhost(configDir)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported server type: %s", serverType)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user