Files
panel/pkg/webserver/apache/proxy.go
T
耗子andClaude Opus 5 2ecc124e6c docs: 清掉复述代码的注释
删的是只把标识符名翻译一遍的注释和 ===== 分隔线,
带条件、原因、格式示例的都留着。顺带把 Apache 的全局监听文件命名为 listen.conf。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 00:50:28 +08:00

820 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package apache
import (
"fmt"
"maps"
"net/url"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/acepanel/panel/v3/pkg/webserver/conf"
"github.com/acepanel/panel/v3/pkg/webserver/types"
)
var durationPattern = regexp.MustCompile(`^(\d+)([smhd]?)$`)
// parseDurationToSeconds 将时长字符串转换为秒数,支持 "10s" "5m" "1h" "1d"
func parseDurationToSeconds(duration string) int {
duration = strings.TrimSpace(duration)
if duration == "" {
return 600 // 默认 10 分钟
}
matches := durationPattern.FindStringSubmatch(duration)
if matches == nil {
return 600
}
value, _ := strconv.Atoi(matches[1])
switch matches[2] {
case "m":
return value * 60
case "h":
return value * 3600
case "d":
return value * 86400
default:
return value
}
}
// proxyFilePattern 匹配代理配置文件名 (200-299)
var proxyFilePattern = regexp.MustCompile(`^(\d{3})-proxy\.conf$`)
var balancerFilePattern = regexp.MustCompile(`^(\d{3})-balancer-(.+)\.conf$`)
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
}
proxy, err := parseProxyFile(filepath.Join(siteDir, entry.Name()))
if err != nil {
continue // 跳过解析失败的文件
}
if proxy != nil {
proxies = append(proxies, *proxy)
}
}
return proxies, nil
}
func parseProxyFile(filePath string) (*types.Proxy, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
cfg, err := ParseFragment(string(content))
if err != nil {
return nil, err
}
proxy := &types.Proxy{
Resolver: []string{},
Headers: make(map[string]string),
Replaces: make(map[string]string),
}
// 取写入时留下的原值
if blk := cfg.GetBlock("IfModule", "mod_proxy.c"); blk != nil {
proxy.Location = blk.Meta("location")
proxy.Pass = blk.Meta("pass")
proxy.SNI = blk.Meta("sni")
}
if proxy.Location == "" {
if d := cfg.FindOne("IfModule.ProxyPass"); d != nil && len(d.Args) >= 2 {
proxy.Location = d.Arg(0)
proxy.Pass = d.Arg(1)
}
}
if d := cfg.FindOne("IfModule.LimitRequestBody"); d != nil {
proxy.ClientMaxBodySize, _ = strconv.ParseInt(d.Arg(0), 10, 64)
}
if blk := cfg.FindOne("IfModule.Location"); blk != nil {
proxy.AccessControl = parseAccessControl(blk)
proxy.ResponseHeaders = parseResponseHeaders(blk)
if d := blk.Get("LimitRequestBody"); d != nil {
proxy.ClientMaxBodySize, _ = strconv.ParseInt(d.Arg(0), 10, 64)
}
}
if d := cfg.FindOne("IfModule.SSLProxyVerify"); d != nil && strings.EqualFold(d.Arg(0), "require") {
proxy.SSLBackend = &types.SSLBackendConfig{Verify: true}
if ca := cfg.FindOne("IfModule.SSLProxyCACertificateFile"); ca != nil {
proxy.SSLBackend.TrustedCertificate = ca.Arg(0)
}
}
proxy.Timeout, proxy.Retry = parseProxyParams(cfg.FindOne("IfModule.ProxyPass"), cfg.FindOne("IfModule.ProxyPassMatch"))
// RequestHeader set Host "host"(mod_proxy 直接子级)
for _, d := range cfg.Find("IfModule.RequestHeader") {
vals := d.Values()
if len(vals) >= 3 && strings.EqualFold(vals[0], "set") && vals[1] == "Host" {
proxy.Host = vals[2]
}
}
if d := cfg.FindOne("IfModule.ProxyPreserveHost"); d != nil && strings.EqualFold(d.Arg(0), "on") {
proxy.Host = "$host"
}
if cfg.FindOne("IfModule.ProxyIOBufferSize") != nil {
proxy.Buffering = true
}
// cache(mod_cache 子块,含 CacheEnable)
for _, blk := range cfg.FindBlocks("IfModule.IfModule") {
if blk.Has("CacheEnable") {
proxy.Cache = parseCacheBlock(blk)
break
}
}
// 自定义请求头(mod_headers 子块,排除 Host)
for _, d := range cfg.Find("IfModule.IfModule.RequestHeader") {
vals := d.Values()
if len(vals) >= 3 && strings.EqualFold(vals[0], "set") && vals[1] != "Host" {
proxy.Headers[vals[1]] = parseHeaderValue(vals[2])
}
}
for _, d := range cfg.Find("IfModule.IfModule.Substitute") {
if len(d.Args) >= 1 {
if from, to, ok := parseSubstitute(d.Args[0].Value); ok {
proxy.Replaces[from] = to
}
}
}
return proxy, nil
}
func parseCacheBlock(blk *conf.Directive) *types.CacheConfig {
cache := &types.CacheConfig{
Valid: make(map[string]string),
NoCacheConditions: []string{},
UseStale: []string{},
Methods: []string{},
}
if d := blk.Get("CacheDefaultExpire"); d != nil && len(d.Args) > 0 {
seconds, _ := strconv.Atoi(d.Args[0].Value)
if minutes := seconds / 60; minutes > 0 {
cache.Valid["any"] = fmt.Sprintf("%dm", minutes)
} else {
cache.Valid["any"] = fmt.Sprintf("%ds", seconds)
}
}
return cache
}
// parseSubstitute 解析 Substitute 规则 s|from|to|flags,分隔符取规则第二个字符
func parseSubstitute(rule string) (from, to string, ok bool) {
if len(rule) < 2 || rule[0] != 's' {
return "", "", false
}
parts := strings.Split(rule[2:], string(rule[1]))
if len(parts) >= 2 {
return parts[0], parts[1], true
}
return "", "", false
}
func writeProxyFiles(siteDir string, proxies []types.Proxy, upstreams []string) error {
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)
}
filePath := filepath.Join(siteDir, fmt.Sprintf("%03d-proxy.conf", num))
if err := os.WriteFile(filePath, []byte(generateProxyConfig(proxy, upstreams)), 0600); err != nil {
return fmt.Errorf("failed to write proxy config: %w", err)
}
}
return nil
}
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 {
if err := os.Remove(filepath.Join(siteDir, entry.Name())); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete proxy config: %w", err)
}
}
}
return nil
}
// location 描述 nginx 的 location 在 Apache 下的表达方式。
// Apache 的 <LocationMatch> 里写 ProxyPass 语法能过但运行时不代理,正则只能走 ProxyPassMatch
type location struct {
regex bool // 用 ProxyPassMatch 而不是 ProxyPass
pattern string // 前缀路径或正则
path string // 用于 ProxyPassReverse 与 <Location> 的路径前缀
}
func parseLocation(loc string) location {
kind, rest := types.ParseLocation(loc)
switch kind {
case types.LocationExact:
path := types.NormalizePath(rest)
return location{regex: true, pattern: "^" + regexp.QuoteMeta(path) + "$", path: path}
case types.LocationRegexAny:
// PCRE 的内联标志,ProxyPassMatch 本身区分大小写
return location{regex: true, pattern: "(?i)" + rest, path: "/"}
case types.LocationRegex:
return location{regex: true, pattern: rest, path: "/"}
default:
path := types.NormalizePath(rest)
return location{pattern: path, path: path}
}
}
// proxyTarget 代理目标。nginx 的 proxy_pass 不带路径时保留完整 URI,带路径时替换掉 location 前缀,
// 而 Apache 的 ProxyPass 总是替换前缀,所以不带路径的要把 location 拼回去;
// ProxyPassMatch 会把整个 URI 附到目标后面,目标只能写到端口
func proxyTarget(pass string, loc location) string {
origin, path := splitPass(pass)
if loc.regex {
return origin
}
target := pass
if path == "" {
target = origin + loc.path
}
// 两边的尾斜杠必须一致,否则深层路径会 500
if strings.HasSuffix(loc.pattern, "/") {
return strings.TrimSuffix(target, "/") + "/"
}
return strings.TrimSuffix(target, "/")
}
// headerValue 含变量的头值要写成 expr 形式,Apache 才会求值
func headerValue(value string) string {
native := types.ApacheVars.ToNative(value)
if native == value {
return value
}
return "expr=" + native
}
func parseHeaderValue(value string) string {
expr, ok := strings.CutPrefix(value, "expr=")
if !ok {
return value
}
return types.ApacheVars.ToNginx(expr)
}
func splitPass(pass string) (origin, path string) {
u, err := url.Parse(pass)
if err != nil || u.Host == "" {
return pass, ""
}
return u.Scheme + "://" + u.Host, u.Path
}
// balancerPass 上游名换成 Apache 的 balancer 地址,照搬 nginx 的 http://<上游名> 会被当真实主机解析
func balancerPass(pass string, upstreams []string) string {
u, err := url.Parse(pass)
if err != nil || !slices.Contains(upstreams, u.Hostname()) {
return pass
}
return "balancer://" + u.Hostname() + u.Path
}
func proxyParams(p types.Proxy) []string {
var params []string
if p.Timeout != nil {
if p.Timeout.Connect > 0 {
params = append(params, "connectiontimeout="+strconv.Itoa(int(p.Timeout.Connect.Seconds())))
}
if p.Timeout.Read > 0 {
params = append(params, "timeout="+strconv.Itoa(int(p.Timeout.Read.Seconds())))
}
}
if p.Retry != nil && p.Retry.Timeout > 0 {
params = append(params, "retry="+strconv.Itoa(int(p.Retry.Timeout.Seconds())))
}
// 2.4.47 起协议升级由 mod_proxy_http 接管,但要显式开启,否则 WebSocket 被当普通请求
params = append(params, "upgrade=websocket")
return params
}
func parseProxyParams(dirs ...*conf.Directive) (*types.TimeoutConfig, *types.RetryConfig) {
timeout := &types.TimeoutConfig{}
var retry *types.RetryConfig
for _, d := range dirs {
for _, a := range d.ArgsFrom(2) {
key, value, found := strings.Cut(a, "=")
seconds, err := strconv.Atoi(value)
if !found || err != nil {
continue
}
switch key {
case "connectiontimeout":
timeout.Connect = time.Duration(seconds) * time.Second
case "timeout":
timeout.Read = time.Duration(seconds) * time.Second
case "retry":
retry = &types.RetryConfig{Timeout: time.Duration(seconds) * time.Second}
}
}
}
if *timeout == (types.TimeoutConfig{}) {
return nil, retry
}
return timeout, retry
}
// accessControlNodes IP 访问控制,deny 要包在 RequireAll 里才是"默认放行、排除名单"的语义
func accessControlNodes(ac *types.AccessControlConfig) []conf.Node {
if len(ac.Deny) == 0 {
return []conf.Node{conf.Dir("Require", append([]string{"ip"}, ac.Allow...)...)}
}
allow := conf.Dir("Require", "all", "granted")
if len(ac.Allow) > 0 {
allow = conf.Dir("Require", append([]string{"ip"}, ac.Allow...)...)
}
return []conf.Node{conf.Blk("RequireAll").Append(
allow,
conf.Dir("Require", append([]string{"not", "ip"}, ac.Deny...)...),
)}
}
func parseAccessControl(blk *conf.Directive) *types.AccessControlConfig {
ac := &types.AccessControlConfig{Allow: []string{}, Deny: []string{}}
collect := func(b *conf.Directive) {
for _, d := range b.GetAll("Require") {
switch {
case d.Arg(0) == "not" && d.Arg(1) == "ip":
ac.Deny = append(ac.Deny, d.ArgsFrom(2)...)
case d.Arg(0) == "ip":
ac.Allow = append(ac.Allow, d.ArgsFrom(1)...)
}
}
}
collect(blk)
for _, all := range blk.Blocks("RequireAll") {
collect(all)
}
if len(ac.Allow) == 0 && len(ac.Deny) == 0 {
return nil
}
return ac
}
func parseResponseHeaders(blk *conf.Directive) *types.ResponseHeaderConfig {
headers := &types.ResponseHeaderConfig{Hide: []string{}, Add: make(map[string]string)}
for _, d := range blk.GetAll("Header") {
args := d.Values()
if len(args) > 0 && strings.EqualFold(args[0], "always") {
args = args[1:]
}
switch {
case len(args) >= 2 && strings.EqualFold(args[0], "unset"):
headers.Hide = append(headers.Hide, args[1])
case len(args) >= 3 && strings.EqualFold(args[0], "set"):
headers.Add[args[1]] = parseHeaderValue(args[2])
}
}
if len(headers.Hide) == 0 && len(headers.Add) == 0 {
return nil
}
return headers
}
func generateProxyConfig(proxy types.Proxy, upstreams []string) string {
loc := parseLocation(proxy.Location)
pass := balancerPass(proxy.Pass, upstreams)
target := proxyTarget(pass, loc)
inner := conf.Blk("IfModule", "mod_proxy.c")
// 匹配类型与目标路径在 Apache 语法里不可逆,留下原值供回读
inner.AddMeta("location", proxy.Location)
inner.AddMeta("pass", proxy.Pass)
// Apache 不支持自定义 SNI,记下来供换回其它服务器时使用
if proxy.SNI != "" {
inner.AddMeta("sni", proxy.SNI)
}
args := []string{loc.pattern, target}
// balancer 目标只认 balancer 参数,worker 参数得写在 BalancerMember 上
if !strings.HasPrefix(target, "balancer://") {
args = append(args, proxyParams(proxy)...)
}
if loc.regex {
inner.Append(conf.Dir("ProxyPassMatch", args...))
} else {
inner.Append(conf.Dir("ProxyPass", args...))
}
inner.Append(conf.Dir("ProxyPassReverse", loc.path, target))
switch host := strings.TrimSpace(proxy.Host); host {
case "$host":
inner.Append(conf.Dir("ProxyPreserveHost", "On"))
case "", "$proxy_host":
default:
inner.Append(conf.Dir("RequestHeader", "set", "Host", host))
}
// mod_proxy 自带 X-Forwarded-For/Host/Server,这两个要自己补
inner.Append(
conf.Dir("RequestHeader", "set", "X-Real-IP", "expr=%{REMOTE_ADDR}"),
conf.Dir("RequestHeader", "set", "X-Forwarded-Proto", "expr=%{REQUEST_SCHEME}"),
)
inner.Append(sslProxyNodes(proxy, target)...)
if proxy.Buffering {
inner.Append(conf.Dir("ProxyIOBufferSize", "65536"))
}
if proxy.Cache != nil {
inner.Append(cacheNode(proxy.Cache, loc.path))
}
if len(proxy.Headers) > 0 {
headers := conf.Blk("IfModule", "mod_headers.c")
for _, name := range slices.Sorted(maps.Keys(proxy.Headers)) {
headers.Append(conf.Dir("RequestHeader", "set", name, headerValue(proxy.Headers[name])))
}
inner.Append(headers)
}
if len(proxy.Replaces) > 0 {
inner.Append(substituteNode(proxy.Replaces))
}
// 这几项是按路径生效的,必须落在容器块里
if scoped := scopedNodes(proxy); len(scoped) > 0 {
inner.Append(conf.Blk("Location", loc.path).Append(scoped...))
}
cfg := &conf.Config{}
cfg.Append(
conf.Cmt("Auto-generated by AcePanel. DO NOT EDIT MANUALLY!"),
conf.Cmt(fmt.Sprintf("Reverse proxy: %s -> %s", proxy.Location, proxy.Pass)),
inner,
)
return Export(cfg) + "\n"
}
func sslProxyNodes(proxy types.Proxy, target string) []conf.Node {
if !strings.HasPrefix(target, "https://") {
return nil
}
nodes := []conf.Node{conf.Dir("SSLProxyEngine", "On")}
if proxy.SSLBackend != nil && proxy.SSLBackend.Verify {
nodes = append(nodes, conf.Dir("SSLProxyVerify", "require"))
if proxy.SSLBackend.TrustedCertificate != "" {
nodes = append(nodes, conf.Dir("SSLProxyCACertificateFile", proxy.SSLBackend.TrustedCertificate))
}
return nodes
}
return append(nodes,
conf.Dir("SSLProxyVerify", "none"),
conf.Dir("SSLProxyCheckPeerCN", "off"),
conf.Dir("SSLProxyCheckPeerName", "off"),
)
}
func cacheNode(cache *types.CacheConfig, path string) *conf.Directive {
expire := 600
for _, duration := range cache.Valid {
expire = parseDurationToSeconds(duration)
break
}
return conf.Blk("IfModule", "mod_cache.c").Append(
conf.Dir("CacheEnable", "disk", path),
conf.Dir("CacheDefaultExpire", strconv.Itoa(expire)),
)
}
func substituteNode(replaces map[string]string) *conf.Directive {
sub := conf.Blk("IfModule", "mod_substitute.c").Append(
conf.Dir("AddOutputFilterByType", "SUBSTITUTE", "text/html", "text/plain", "text/xml"),
)
for _, from := range slices.Sorted(maps.Keys(replaces)) {
// 用 | 作为分隔符以支持含 / 的内容,强制双引号
sub.Append(&conf.Directive{Name: "Substitute", Args: []conf.Arg{dquote(fmt.Sprintf("s|%s|%s|n", from, replaces[from]))}})
}
return sub
}
func scopedNodes(p types.Proxy) []conf.Node {
var nodes []conf.Node
if p.AccessControl != nil && (len(p.AccessControl.Allow) > 0 || len(p.AccessControl.Deny) > 0) {
nodes = append(nodes, accessControlNodes(p.AccessControl)...)
}
if p.ResponseHeaders != nil {
for _, name := range p.ResponseHeaders.Hide {
nodes = append(nodes, conf.Dir("Header", "always", "unset", name))
}
for _, name := range slices.Sorted(maps.Keys(p.ResponseHeaders.Add)) {
nodes = append(nodes, conf.Dir("Header", "always", "set", name, headerValue(p.ResponseHeaders.Add[name])))
}
}
if p.ClientMaxBodySize > 0 {
nodes = append(nodes, conf.Dir("LimitRequestBody", strconv.FormatInt(p.ClientMaxBodySize, 10)))
}
return nodes
}
// 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
}
upstream, err := parseBalancerFile(filepath.Join(sharedDir, entry.Name()), matches[2])
if err != nil {
continue // 跳过解析失败的文件
}
if upstream != nil {
upstreams = append(upstreams, *upstream)
}
}
return upstreams, nil
}
func parseBalancerFile(filePath string, name string) (*types.Upstream, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
cfg, err := ParseFragment(string(content))
if err != nil {
return nil, err
}
upstream := &types.Upstream{
Name: name,
Servers: make(map[string]string),
Resolver: []string{},
}
for _, d := range cfg.Find("IfModule.Proxy.BalancerMember") {
if len(d.Args) == 0 {
continue
}
addr, options := parseMember(d.Values())
upstream.Servers[addr] = options
}
// ProxySet lbmethod=xxx / max=N
for _, d := range cfg.Find("IfModule.Proxy.ProxySet") {
for _, a := range d.Args {
k, v, found := strings.Cut(a.Value, "=")
if !found {
continue
}
switch k {
case "lbmethod":
upstream.Algo = algoFromLBMethod(v)
case "max":
upstream.Keepalive, _ = strconv.Atoi(v)
}
}
}
// 降级前的原值优先
if blk := cfg.FindBlocks("IfModule.Proxy"); len(blk) > 0 {
if algo := blk[0].Meta("algo"); algo != "" {
upstream.Algo = types.NormalizeAlgo(algo)
}
}
return upstream, nil
}
// lbMethod 规范算法名转 Apache 的 lbmethod,Apache 只有按请求数、按流量、按繁忙度三种,其余表达不了
func lbMethod(algo string) (string, bool) {
switch algo {
case types.AlgoRoundRobin:
return "byrequests", true
case types.AlgoLeastConn:
return "bybusyness", true
default:
return "byrequests", false
}
}
func algoFromLBMethod(method string) string {
if method == "bybusyness" {
return types.AlgoLeastConn
}
return types.AlgoRoundRobin
}
// balancerMember 上游地址与参数转 Apache 语法:地址必须带协议前缀,
// 参数名与 nginx 完全不同,照搬过去会让 Apache 以 "unknown Worker parameter" 拒载
func balancerMember(addr, options string) []string {
args := []string{balancerAddr(addr), "upgrade=websocket"}
for _, opt := range strings.Fields(options) {
key, value, _ := strings.Cut(opt, "=")
switch key {
case "weight":
args = append(args, "loadfactor="+value)
case "fail_timeout":
args = append(args, "retry="+strings.TrimSuffix(value, "s"))
case "backup":
args = append(args, "status=+H")
case "down":
args = append(args, "status=+D")
}
}
return args
}
func balancerAddr(addr string) string {
switch {
case strings.Contains(addr, "://"):
return addr
case strings.HasPrefix(addr, "unix:"):
return addr + "|http://localhost"
default:
return "http://" + addr
}
}
// parseMember 还原 BalancerMember 为面板的地址与 nginx 风格参数
func parseMember(args []string) (string, string) {
addr := args[0]
if unix, ok := strings.CutSuffix(addr, "|http://localhost"); ok {
addr = unix
} else {
addr = strings.TrimPrefix(addr, "http://")
}
var options []string
for _, a := range args[1:] {
if a == "upgrade=websocket" {
continue
}
key, value, _ := strings.Cut(a, "=")
switch key {
case "loadfactor":
options = append(options, "weight="+value)
case "retry":
options = append(options, "fail_timeout="+value+"s")
case "status":
switch value {
case "+H":
options = append(options, "backup")
case "+D":
options = append(options, "down")
}
}
}
return addr, strings.Join(options, " ")
}
// balancerNames 上游名从文件名就能取到,不必读内容
func balancerNames(sharedDir string) []string {
entries, err := os.ReadDir(sharedDir)
if err != nil {
return nil
}
names := make([]string, 0, len(entries))
for _, entry := range entries {
if matches := balancerFilePattern.FindStringSubmatch(entry.Name()); matches != nil {
names = append(names, matches[2])
}
}
return names
}
func writeBalancerFiles(sharedDir string, upstreams []types.Upstream) error {
if err := clearBalancerFiles(sharedDir); err != nil {
return err
}
for i, upstream := range upstreams {
num := 100 + i
filePath := filepath.Join(sharedDir, fmt.Sprintf("%03d-balancer-%s.conf", num, upstream.Name))
if err := os.WriteFile(filePath, []byte(generateBalancerConfig(upstream)), 0600); err != nil {
return fmt.Errorf("failed to write balancer config: %w", err)
}
}
return nil
}
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()) {
if err := os.Remove(filepath.Join(sharedDir, entry.Name())); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete balancer config: %w", err)
}
}
}
return nil
}
func generateBalancerConfig(upstream types.Upstream) string {
proxy := conf.Blk("Proxy", "balancer://"+upstream.Name)
for _, addr := range slices.Sorted(maps.Keys(upstream.Servers)) {
proxy.Append(conf.Dir("BalancerMember", balancerMember(addr, upstream.Servers[addr])...))
}
algo := types.NormalizeAlgo(upstream.Algo)
method, ok := lbMethod(algo)
if !ok {
// 降级后靠注释保留原值,换回其它服务器时不丢
proxy.AddMeta("algo", algo)
}
proxy.Append(conf.Dir("ProxySet", "lbmethod="+method))
if upstream.Keepalive > 0 {
proxy.Append(conf.Dir("ProxySet", "max="+strconv.Itoa(upstream.Keepalive)))
}
cfg := &conf.Config{}
cfg.Append(
conf.Cmt("Auto-generated by AcePanel. DO NOT EDIT MANUALLY!"),
conf.Cmt("Load balancer: "+upstream.Name),
conf.Blk("IfModule", "mod_proxy_balancer.c").Append(proxy),
)
return Export(cfg) + "\n"
}