package apache
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"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 从 site 目录解析所有代理配置
func parseProxyFiles(siteDir string) ([]types.Proxy, error) {
entries, err := os.ReadDir(siteDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
proxies := make([]types.Proxy, 0)
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(siteDir, 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{
Resolver: []string{},
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
_ = regexp.MustCompile(`ProxyPreserveHost\s+On`).MatchString(contentStr)
// 解析 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
}
// 解析 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(siteDir string, proxies []types.Proxy) error {
// 删除现有的代理配置文件 (200-299)
if err := clearProxyFiles(siteDir); 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(siteDir, fileName)
content := generateProxyConfig(proxy)
if err := os.WriteFile(filePath, []byte(content), 0600); err != nil {
return fmt.Errorf("failed to write proxy config: %w", err)
}
}
return nil
}
// clearProxyFiles 清除所有代理配置文件
func clearProxyFiles(siteDir string) error {
entries, err := os.ReadDir(siteDir)
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(siteDir, 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 = "/"
}
// 将 Nginx 风格的 location 转换为 Apache 格式
// ^~ / -> /
// ~ ^/api -> /api (正则匹配需要使用 ProxyPassMatch)
location = strings.TrimPrefix(location, "^~ ")
location = strings.TrimPrefix(location, "~ ")
location = strings.TrimPrefix(location, "= ")
// 如果 location 以 ^ 开头(正则),去掉它
location = strings.TrimPrefix(location, "^")
// 确保 location 以 / 开头
if !strings.HasPrefix(location, "/") {
location = "/" + location
}
sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass))
// 启用代理模块
sb.WriteString("\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")
}
// Cache 配置
if proxy.Cache {
sb.WriteString(" \n")
sb.WriteString(fmt.Sprintf(" CacheEnable disk %s\n", location))
sb.WriteString(" CacheDefaultExpire 600\n")
sb.WriteString(" \n")
}
// 响应内容替换
if len(proxy.Replaces) > 0 {
sb.WriteString(" \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(" \n")
}
sb.WriteString("\n")
return sb.String()
}
// parseBalancerFiles 从 shared 目录解析所有负载均衡配置(Apache 的 upstream 等价物)
func parseBalancerFiles(sharedDir string) ([]types.Upstream, error) {
entries, err := os.ReadDir(sharedDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
upstreams := make([]types.Upstream, 0)
for _, entry := range entries {
if entry.IsDir() {
continue
}
matches := balancerFilePattern.FindStringSubmatch(entry.Name())
if matches == nil {
continue
}
filePath := filepath.Join(sharedDir, entry.Name())
upstream, err := parseBalancerFile(filePath, matches[2])
if err != nil {
continue // 跳过解析失败的文件
}
if upstream != nil {
upstreams = append(upstreams, *upstream)
}
}
return upstreams, nil
}
// parseBalancerFile 解析单个负载均衡配置文件
func parseBalancerFile(filePath string, name string) (*types.Upstream, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
contentStr := string(content)
upstream := &types.Upstream{
Name: name,
Servers: make(map[string]string),
Resolver: []string{},
}
// 解析 块
//
// BalancerMember http://127.0.0.1:8080 loadfactor=5
// BalancerMember http://127.0.0.1:8081 loadfactor=3
// ProxySet lbmethod=byrequests
//
// 解析 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(sharedDir string, upstreams []types.Upstream) error {
// 删除现有的负载均衡配置文件
if err := clearBalancerFiles(sharedDir); err != nil {
return err
}
// 写入新的配置文件,保持顺序
for i, upstream := range upstreams {
num := 100 + i
fileName := fmt.Sprintf("%03d-balancer-%s.conf", num, upstream.Name)
filePath := filepath.Join(sharedDir, fileName)
content := generateBalancerConfig(upstream)
if err := os.WriteFile(filePath, []byte(content), 0600); err != nil {
return fmt.Errorf("failed to write balancer config: %w", err)
}
}
return nil
}
// clearBalancerFiles 清除所有负载均衡配置文件
func clearBalancerFiles(sharedDir string) error {
entries, err := os.ReadDir(sharedDir)
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(sharedDir, 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(upstream types.Upstream) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("# Load balancer: %s\n", upstream.Name))
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf(" \n", upstream.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(" \n")
sb.WriteString("\n")
return sb.String()
}