2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 03:07:20 +08:00
Files
panel/pkg/webserver/apache/vhost.go
2026-01-29 08:14:02 +08:00

739 lines
18 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"
"os"
"path/filepath"
"strings"
"github.com/acepanel/panel/pkg/webserver/types"
"github.com/samber/lo"
)
// 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 // 配置目录
siteName string // 网站名
}
// newBaseVhost 创建基础虚拟主机实例
func newBaseVhost(configDir string) (*baseVhost, error) {
if configDir == "" {
return nil, fmt.Errorf("config directory is required")
}
v := &baseVhost{
configDir: configDir,
siteName: filepath.Base(filepath.Dir(configDir)),
}
// 加载配置
var config *Config
var err error
// 从配置目录加载主配置文件
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)
}
}
// 如果没有配置文件,使用默认配置
if config == nil {
defaultConf := strings.ReplaceAll(DefaultVhostConf, "/opt/ace/sites/default", fmt.Sprintf("/opt/ace/sites/%s", v.siteName))
config, err = ParseString(defaultConf)
if err != nil {
return nil, fmt.Errorf("failed to parse default config: %w", err)
}
}
v.config = config
// 获取第一个虚拟主机
if len(config.VirtualHosts) > 0 {
v.vhost = config.VirtualHosts[0]
} else {
// 创建默认虚拟主机
v.vhost = config.AddVirtualHost("*:80")
}
return v, nil
}
// NewStaticVhost 创建纯静态虚拟主机实例
func NewStaticVhost(configDir string) (*StaticVhost, error) {
base, err := newBaseVhost(configDir)
if err != nil {
return nil, err
}
return &StaticVhost{baseVhost: base}, nil
}
// 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 {
// 检查是否存在禁用配置文件
disableConf := filepath.Join(v.configDir, "site", "00-disable.conf")
_, err := os.Stat(disableConf)
return os.IsNotExist(err)
}
func (v *baseVhost) SetEnable(enable bool) error {
disableConf := filepath.Join(v.configDir, "site", "00-disable.conf")
if enable {
// 启用:删除禁用配置文件
if err := os.Remove(disableConf); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove disable config: %w", err)
}
} else {
// 禁用:所有请求重写到禁用页面
content := fmt.Sprintf(`# Auto-generated by AcePanel. DO NOT EDIT MANUALLY!
RewriteEngine On
RewriteRule ^ %s [L]
`, DisablePage)
if err := os.WriteFile(disableConf, []byte(content), 0600); err != nil {
return fmt.Errorf("failed to write disable config: %w", err)
}
}
return nil
}
func (v *baseVhost) Listen() []types.Listen {
var result []types.Listen
// Apache 的监听配置通常在 VirtualHost 的参数中
// 例如: <VirtualHost *:80> 或 <VirtualHost 192.168.1.1:443>
for _, arg := range v.vhost.Args {
listen := types.Listen{Address: arg, Args: []string{}}
result = append(result, listen)
}
return result
}
func (v *baseVhost) SetListen(listens []types.Listen) error {
var args []string
for _, l := range listens {
addr := l.Address
// 如果只是端口号,添加 *: 前缀
if !strings.Contains(addr, ":") {
addr = "*:" + addr
}
args = append(args, addr)
}
v.vhost.Args = args
return nil
}
func (v *baseVhost) ServerName() []string {
names := make([]string, 0)
// 获取 ServerName
serverName := v.vhost.GetDirectiveValue("ServerName")
if serverName != "" {
names = append(names, serverName)
}
// 获取 ServerAlias可能有多个值
aliases := v.vhost.GetDirectives("ServerAlias")
for _, alias := range aliases {
names = append(names, alias.Args...)
}
return names
}
func (v *baseVhost) SetServerName(serverName []string) error {
if len(serverName) == 0 {
return nil
}
// 设置主域名
v.vhost.SetDirective("ServerName", serverName[0])
// 删除现有的 ServerAlias
v.vhost.RemoveDirectives("ServerAlias")
// 设置别名
if len(serverName) > 1 {
v.vhost.AddDirective("ServerAlias", serverName[1:]...)
}
return nil
}
func (v *baseVhost) Index() []string {
values := v.vhost.GetDirectiveValues("DirectoryIndex")
if values != nil {
return values
}
return []string{}
}
func (v *baseVhost) SetIndex(index []string) error {
if len(index) == 0 {
v.vhost.RemoveDirective("DirectoryIndex")
return nil
}
v.vhost.SetDirective("DirectoryIndex", index...)
return nil
}
func (v *baseVhost) Root() string {
return v.vhost.GetDirectiveValue("DocumentRoot")
}
func (v *baseVhost) SetRoot(root string) error {
v.vhost.SetDirective("DocumentRoot", root)
// 同时更新 Directory 块
dirBlock := v.vhost.GetBlock("Directory")
if dirBlock != nil {
// 更新现有的 Directory 块路径
dirBlock.Args = []string{root}
} else {
// 添加新的 Directory 块
block := v.vhost.AddBlock("Directory", root)
if block.Block != nil {
block.Block.Directives = append(block.Block.Directives,
&Directive{Name: "Options", Args: []string{"-Indexes", "+FollowSymLinks"}},
&Directive{Name: "AllowOverride", Args: []string{"All"}},
&Directive{Name: "Require", Args: []string{"all", "granted"}},
)
}
}
return nil
}
func (v *baseVhost) Includes() []types.IncludeFile {
var result []types.IncludeFile
// 获取所有 Include 和 IncludeOptional 指令
for _, dir := range v.vhost.GetDirectives("Include") {
if len(dir.Args) > 0 {
result = append(result, types.IncludeFile{
Path: dir.Args[0],
})
}
}
for _, dir := range v.vhost.GetDirectives("IncludeOptional") {
if len(dir.Args) > 0 {
result = append(result, types.IncludeFile{
Path: dir.Args[0],
})
}
}
return result
}
func (v *baseVhost) SetIncludes(includes []types.IncludeFile) error {
// 删除现有的 Include 指令
v.vhost.RemoveDirectives("Include")
v.vhost.RemoveDirectives("IncludeOptional")
// 添加新的 Include 指令
for _, inc := range includes {
v.vhost.AddDirective("Include", inc.Path)
}
return nil
}
func (v *baseVhost) AccessLog() string {
content := v.Config("020-access-log.conf", "site")
if content == "" {
return ""
}
var result string
_, err := fmt.Sscanf(content, "CustomLog %s combined", &result)
if err != nil {
return ""
}
return result
}
func (v *baseVhost) SetAccessLog(accessLog string) error {
if accessLog == "" {
return v.RemoveConfig("020-access-log.conf", "site")
}
return v.SetConfig("020-access-log.conf", "site", fmt.Sprintf("CustomLog %s combined\n", accessLog))
}
func (v *baseVhost) ErrorLog() string {
content := v.Config("020-error-log.conf", "site")
if content == "" {
return ""
}
var result string
_, err := fmt.Sscanf(content, "ErrorLog %s", &result)
if err != nil {
return ""
}
return result
}
func (v *baseVhost) SetErrorLog(errorLog string) error {
if errorLog == "" {
return v.RemoveConfig("020-error-log.conf", "site")
}
return v.SetConfig("020-error-log.conf", "site", fmt.Sprintf("ErrorLog %s\n", errorLog))
}
func (v *baseVhost) Save() error {
configFile := filepath.Join(v.configDir, "apache.conf")
content := v.config.Export()
if err := os.WriteFile(configFile, []byte(content), 0600); err != nil {
return fmt.Errorf("failed to save config file: %w", err)
}
return nil
}
func (v *baseVhost) Reset() error {
// 重置配置为默认值
defaultConf := strings.ReplaceAll(DefaultVhostConf, "/opt/ace/sites/default", fmt.Sprintf("/opt/ace/sites/%s", v.siteName))
config, err := ParseString(defaultConf)
if err != nil {
return fmt.Errorf("failed to reset config: %w", err)
}
v.config = config
if len(config.VirtualHosts) > 0 {
v.vhost = config.VirtualHosts[0]
}
return nil
}
func (v *baseVhost) Config(name string, typ string) string {
conf := filepath.Join(v.configDir, typ, name)
content, err := os.ReadFile(conf)
if err != nil {
return ""
}
return strings.TrimSpace(string(content))
}
func (v *baseVhost) SetConfig(name string, typ string, content string) error {
conf := filepath.Join(v.configDir, typ, name)
if err := os.WriteFile(conf, []byte(content), 0600); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
func (v *baseVhost) RemoveConfig(name string, typ string) error {
conf := filepath.Join(v.configDir, typ, name)
if err := os.Remove(conf); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove config file: %w", err)
}
return nil
}
func (v *baseVhost) SSL() bool {
// 检查是否有 SSL 相关配置
return v.vhost.HasDirective("SSLEngine") &&
strings.EqualFold(v.vhost.GetDirectiveValue("SSLEngine"), "on")
}
func (v *baseVhost) SSLConfig() *types.SSLConfig {
if !v.SSL() {
return nil
}
config := &types.SSLConfig{
Cert: v.vhost.GetDirectiveValue("SSLCertificateFile"),
Key: v.vhost.GetDirectiveValue("SSLCertificateKeyFile"),
}
// 获取协议
protocols := v.vhost.GetDirectiveValues("SSLProtocol")
if protocols != nil {
config.Protocols = protocols
}
// 获取加密套件
config.Ciphers = v.vhost.GetDirectiveValue("SSLCipherSuite")
// 检查 HSTS
headers := v.vhost.GetDirectives("Header")
for _, h := range headers {
if len(h.Args) >= 3 && strings.Contains(strings.Join(h.Args, " "), "Strict-Transport-Security") {
config.HSTS = true
break
}
}
// 检查 OCSP
config.OCSP = strings.EqualFold(v.vhost.GetDirectiveValue("SSLUseStapling"), "on")
// 检查 HTTP 重定向(通常在 HTTP 虚拟主机中配置)
redirects := v.vhost.GetDirectives("RewriteRule")
for _, r := range redirects {
if len(r.Args) >= 2 && strings.Contains(strings.Join(r.Args, " "), "https://") {
config.HTTPRedirect = true
break
}
}
return config
}
func (v *baseVhost) SetSSLConfig(cfg *types.SSLConfig) error {
if cfg == nil {
return fmt.Errorf("SSL config cannot be nil")
}
// 启用 SSL
v.vhost.SetDirective("SSLEngine", "on")
// 设置证书
if cfg.Cert != "" {
v.vhost.SetDirective("SSLCertificateFile", cfg.Cert)
}
if cfg.Key != "" {
v.vhost.SetDirective("SSLCertificateKeyFile", cfg.Key)
}
// 设置协议
if len(cfg.Protocols) > 0 {
v.vhost.SetDirective("SSLProtocol", cfg.Protocols...)
} else {
v.vhost.SetDirective("SSLProtocol", "all", "-SSLv2", "-SSLv3", "-TLSv1", "-TLSv1.1")
}
// 设置加密套件
if cfg.Ciphers != "" {
v.vhost.SetDirective("SSLCipherSuite", cfg.Ciphers)
} else {
v.vhost.SetDirective("SSLCipherSuite", "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384")
}
// 设置 HSTS
if cfg.HSTS {
// 只移除现有的 HSTS Header保留其他 Header
newDirectives := make([]*Directive, 0, len(v.vhost.Directives))
for _, dir := range v.vhost.Directives {
if strings.EqualFold(dir.Name, "Header") {
if len(dir.Args) >= 3 && strings.Contains(strings.Join(dir.Args, " "), "Strict-Transport-Security") {
continue
}
}
newDirectives = append(newDirectives, dir)
}
v.vhost.Directives = newDirectives
v.vhost.AddDirective("Header", "always", "set", "Strict-Transport-Security", `"max-age=31536000"`)
}
// 设置 OCSP
if cfg.OCSP {
v.vhost.SetDirective("SSLUseStapling", "on")
}
// 设置 HTTP 重定向(需要 mod_rewrite
if cfg.HTTPRedirect {
v.vhost.SetDirective("RewriteEngine", "on")
v.vhost.AddDirective("RewriteCond", "%{HTTPS}", "off")
v.vhost.AddDirective("RewriteRule", "^(.*)$", "https://%{HTTP_HOST}%{REQUEST_URI}", "[R=301,L]")
}
// 更新监听端口为 443
hasSSLPort := false
for _, arg := range v.vhost.Args {
if strings.Contains(arg, ":443") {
hasSSLPort = true
break
}
}
if !hasSSLPort {
v.vhost.Args = append(v.vhost.Args, "*:443")
}
return nil
}
func (v *baseVhost) ClearSSL() error {
// 移除 SSL 相关指令
v.vhost.RemoveDirective("SSLEngine")
v.vhost.RemoveDirective("SSLCertificateFile")
v.vhost.RemoveDirective("SSLCertificateKeyFile")
v.vhost.RemoveDirective("SSLProtocol")
v.vhost.RemoveDirective("SSLCipherSuite")
v.vhost.RemoveDirective("SSLUseStapling")
// 只移除 HSTS 相关的 Header 指令
newDirectives := make([]*Directive, 0, len(v.vhost.Directives))
for _, dir := range v.vhost.Directives {
if strings.EqualFold(dir.Name, "Header") {
// 检查是否是 HSTS Header
if len(dir.Args) >= 3 && strings.Contains(strings.Join(dir.Args, " "), "Strict-Transport-Security") {
continue // 跳过 HSTS Header
}
}
newDirectives = append(newDirectives, dir)
}
v.vhost.Directives = newDirectives
// 移除重定向规则
v.vhost.RemoveDirective("RewriteEngine")
v.vhost.RemoveDirectives("RewriteCond")
v.vhost.RemoveDirectives("RewriteRule")
// 更新监听端口,移除 443
var newArgs []string
for _, arg := range v.vhost.Args {
if !strings.Contains(arg, ":443") {
newArgs = append(newArgs, arg)
}
}
if len(newArgs) == 0 {
newArgs = []string{"*:80"}
}
v.vhost.Args = newArgs
return nil
}
func (v *baseVhost) RateLimit() *types.RateLimit {
// Apache 使用 mod_ratelimit
rate := v.vhost.GetDirectiveValue("SetOutputFilter")
if rate != "RATE_LIMIT" {
return nil
}
rateLimit := &types.RateLimit{}
// 获取速率限制值 (SetEnv rate-limit 512)
args := v.vhost.GetDirectiveValues("SetEnv")
if len(args) >= 2 && args[0] == "rate-limit" {
_, _ = fmt.Sscanf(args[1], "%d", &rateLimit.Rate)
}
return rateLimit
}
func (v *baseVhost) SetRateLimit(limit *types.RateLimit) error {
// Apache mod_ratelimit 只支持流量限制,不支持并发连接限制
if limit.Rate > 0 {
v.vhost.SetDirective("SetOutputFilter", "RATE_LIMIT")
v.vhost.SetDirective("SetEnv", "rate-limit", fmt.Sprintf("%d", limit.Rate))
} else {
v.vhost.RemoveDirective("SetOutputFilter")
v.vhost.RemoveDirectives("SetEnv")
}
return nil
}
func (v *baseVhost) ClearRateLimit() error {
v.vhost.RemoveDirective("SetOutputFilter")
v.vhost.RemoveDirectives("SetEnv")
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 {
v.vhost.SetDirective("AuthType", "Basic")
v.vhost.SetDirective("AuthName", fmt.Sprintf(`"%s"`, lo.If(auth["realm"] != "", auth["realm"]).Else("Restricted")))
v.vhost.SetDirective("AuthUserFile", auth["user_file"])
v.vhost.SetDirective("Require", "valid-user")
return nil
}
func (v *baseVhost) ClearBasicAuth() error {
v.vhost.RemoveDirective("AuthType")
v.vhost.RemoveDirective("AuthName")
v.vhost.RemoveDirective("AuthUserFile")
v.vhost.RemoveDirective("Require")
return nil
}
func (v *baseVhost) RealIP() *types.RealIP {
// Apache 使用 mod_remoteip
// RemoteIPHeader X-Forwarded-For
// RemoteIPTrustedProxy 127.0.0.1
header := v.vhost.GetDirectiveValue("RemoteIPHeader")
if header == "" {
return nil
}
var from []string
for _, dir := range v.vhost.GetDirectives("RemoteIPTrustedProxy") {
if len(dir.Args) > 0 {
from = append(from, dir.Args[0])
}
}
return &types.RealIP{
From: from,
Header: header,
}
}
func (v *baseVhost) SetRealIP(realIP *types.RealIP) error {
// 清除现有配置
v.vhost.RemoveDirective("RemoteIPHeader")
v.vhost.RemoveDirectives("RemoteIPTrustedProxy")
if realIP == nil || (len(realIP.From) == 0 && realIP.Header == "") {
return nil
}
// 设置 RemoteIPHeader
if realIP.Header != "" {
v.vhost.SetDirective("RemoteIPHeader", realIP.Header)
}
// 设置 RemoteIPTrustedProxy
for _, ip := range realIP.From {
if ip != "" {
v.vhost.AddDirective("RemoteIPTrustedProxy", ip)
}
}
return nil
}
func (v *baseVhost) ClearRealIP() error {
v.vhost.RemoveDirective("RemoteIPHeader")
v.vhost.RemoveDirectives("RemoteIPTrustedProxy")
return nil
}
func (v *baseVhost) Redirects() []types.Redirect {
siteDir := filepath.Join(v.configDir, "site")
redirects, _ := parseRedirectFiles(siteDir)
return redirects
}
func (v *baseVhost) SetRedirects(redirects []types.Redirect) error {
siteDir := filepath.Join(v.configDir, "site")
return writeRedirectFiles(siteDir, redirects)
}
// ========== PHPVhost ==========
func (v *PHPVhost) PHP() uint {
content := v.Config("010-php.conf", "site")
if content == "" {
return 0
}
// 从配置内容中提取版本号
// 格式: proxy:unix:/tmp/php-cgi-84.sock|fcgi://localhost/
idx := strings.Index(content, "php-cgi-")
if idx == -1 {
return 0
}
var result uint
_, err := fmt.Sscanf(content[idx:], "php-cgi-%d.sock", &result)
if err != nil {
return 0
}
return result
}
func (v *PHPVhost) SetPHP(version uint) error {
if version == 0 {
return v.RemoveConfig("010-php.conf", "site")
}
// 生成 PHP-FPM 配置
// sock 路径格式: unix:/tmp/php-cgi-84.sock
content := fmt.Sprintf(`# Auto-generated by AcePanel. DO NOT EDIT MANUALLY!
<FilesMatch \.php$>
SetHandler "proxy:unix:/tmp/php-cgi-%d.sock|fcgi://localhost/"
</FilesMatch>
`, version)
return v.SetConfig("010-php.conf", "site", content)
}
// ========== ProxyVhost ==========
func (v *ProxyVhost) Proxies() []types.Proxy {
siteDir := filepath.Join(v.configDir, "site")
proxies, _ := parseProxyFiles(siteDir)
return proxies
}
func (v *ProxyVhost) SetProxies(proxies []types.Proxy) error {
siteDir := filepath.Join(v.configDir, "site")
return writeProxyFiles(siteDir, proxies)
}
func (v *ProxyVhost) ClearProxies() error {
siteDir := filepath.Join(v.configDir, "site")
return clearProxyFiles(siteDir)
}
func (v *ProxyVhost) Upstreams() []types.Upstream {
sharedDir := filepath.Join(v.configDir, "shared")
upstreams, _ := parseBalancerFiles(sharedDir)
return upstreams
}
func (v *ProxyVhost) SetUpstreams(upstreams []types.Upstream) error {
sharedDir := filepath.Join(v.configDir, "shared")
return writeBalancerFiles(sharedDir, upstreams)
}
func (v *ProxyVhost) ClearUpstreams() error {
sharedDir := filepath.Join(v.configDir, "shared")
return clearBalancerFiles(sharedDir)
}