mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 14:57:16 +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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user