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

feat: 配置解析器优化

This commit is contained in:
2025-12-01 16:21:27 +08:00
parent b4bd840781
commit bd453daed9
18 changed files with 2440 additions and 340 deletions

View File

@@ -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

View 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()
}

View 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()
}

View File

@@ -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)
}

View File

@@ -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())
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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()

View 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()
}

View 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()
}

View File

@@ -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;

View File

@@ -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;

View 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()
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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 监听配置

View File

@@ -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)
}