2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 14:57:16 +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())
}