diff --git a/internal/data/container.go b/internal/data/container.go index 4c20065c..5d4e7af1 100644 --- a/internal/data/container.go +++ b/internal/data/container.go @@ -107,7 +107,6 @@ func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { } defer func(out client.ImagePullResponse) { _ = out.Close() }(out) - // TODO 实现流式显示拉取进度 if err = out.Wait(ctx); err != nil { return "", err } diff --git a/internal/data/container_image.go b/internal/data/container_image.go index d453b030..3a5aa18f 100644 --- a/internal/data/container_image.go +++ b/internal/data/container_image.go @@ -106,7 +106,6 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { } defer func(out client.ImagePullResponse) { _ = out.Close() }(out) - // TODO 实现流式显示拉取进度 return out.Wait(context.Background()) } diff --git a/internal/data/website.go b/internal/data/website.go index 4edf0db4..34ec951a 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -81,21 +81,35 @@ func (r *websiteRepo) GetRewrites() (map[string]string, error) { } func (r *websiteRepo) UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error { - if err := io.Write(filepath.Join(app.Root, "server/nginx/html/index.html"), req.Index, 0644); err != nil { + webServer, err := r.setting.Get(biz.SettingKeyWebserver) + if err != nil { return err } - if err := io.Write(filepath.Join(app.Root, "server/nginx/html/stop.html"), req.Stop, 0644); err != nil { + var htmlPath string + switch webServer { + case "nginx": + htmlPath = filepath.Join(app.Root, "server/nginx/html") + case "apache": + htmlPath = filepath.Join(app.Root, "server/apache/htdocs") + default: + htmlPath = filepath.Join(app.Root, "server/nginx/html") + } + + if err = io.Write(filepath.Join(htmlPath, "index.html"), req.Index, 0644); err != nil { + return err + } + if err = io.Write(filepath.Join(htmlPath, "stop.html"), req.Stop, 0644); err != nil { return err } if req.NotFound != "" { - if err := io.Write(filepath.Join(app.Root, "server/nginx/html/404.html"), req.NotFound, 0644); err != nil { + if err = io.Write(filepath.Join(htmlPath, "404.html"), req.NotFound, 0644); err != nil { return err } } - if err := r.setting.SetSlice(biz.SettingKeyWebsiteTLSVersions, req.TLSVersions); err != nil { + if err = r.setting.SetSlice(biz.SettingKeyWebsiteTLSVersions, req.TLSVersions); err != nil { return err } - if err := r.setting.Set(biz.SettingKeyWebsiteCipherSuites, req.CipherSuites); err != nil { + if err = r.setting.Set(biz.SettingKeyWebsiteCipherSuites, req.CipherSuites); err != nil { return err } @@ -245,6 +259,11 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (* Remark: req.Remark, } + webServer, err := r.setting.Get(biz.SettingKeyWebserver) + if err != nil { + return nil, err + } + vhost, err := r.getVhost(w) if err != nil { return nil, err @@ -290,8 +309,14 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (* return nil, err } // 404 页面 - // TODO 需要兼容 Apache - if err = vhost.SetConfig("010-error-404.conf", "site", `error_page 404 /404.html;`); err != nil { + var errorPageConfig string + switch webServer { + case "nginx": + errorPageConfig = `error_page 404 /404.html;` + case "apache": + errorPageConfig = `ErrorDocument 404 /404.html` + } + if err = vhost.SetConfig("010-error-404.conf", "site", errorPageConfig); err != nil { return nil, err } @@ -315,8 +340,10 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (* if err = phpVhost.SetConfig("010-rewrite.conf", "site", ""); err != nil { return nil, err } - // TODO 需要兼容 Apache - if err = phpVhost.SetConfig("010-cache.conf", "site", `# browser cache + var cacheConfig string + switch webServer { + case "nginx": + cacheConfig = `# browser cache location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ { expires 30d; access_log /dev/null; @@ -331,7 +358,38 @@ location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ { location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) { return 404; } -`); err != nil { +` + case "apache": + cacheConfig = `# browser cache + + ExpiresActive On + ExpiresByType image/bmp "access plus 30 days" + ExpiresByType image/jpeg "access plus 30 days" + ExpiresByType image/png "access plus 30 days" + ExpiresByType image/gif "access plus 30 days" + ExpiresByType image/svg+xml "access plus 30 days" + ExpiresByType image/x-icon "access plus 30 days" + ExpiresByType image/tiff "access plus 30 days" + ExpiresByType image/webp "access plus 30 days" + ExpiresByType image/avif "access plus 30 days" + ExpiresByType image/heif "access plus 30 days" + ExpiresByType image/heic "access plus 30 days" + ExpiresByType image/jxl "access plus 30 days" + ExpiresByType text/css "access plus 6 hours" + ExpiresByType application/javascript "access plus 6 hours" + ExpiresByType font/ttf "access plus 6 hours" + ExpiresByType font/otf "access plus 6 hours" + ExpiresByType font/woff "access plus 6 hours" + ExpiresByType font/woff2 "access plus 6 hours" + ExpiresByType application/vnd.ms-fontobject "access plus 6 hours" + +# deny sensitive files + + Require all denied + +` + } + if err = phpVhost.SetConfig("010-cache.conf", "site", cacheConfig); err != nil { return nil, err } } @@ -358,9 +416,15 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) { var notFound []byte // 如果存在自定义 404 页面,则使用自定义的 - // TODO 需要兼容 Apache - if io.Exists(filepath.Join(app.Root, "server/nginx/html/404.html")) { - notFound, _ = os.ReadFile(filepath.Join(app.Root, "server/nginx/html/404.html")) + var custom404Path string + switch webServer { + case "nginx": + custom404Path = filepath.Join(app.Root, "server/nginx/html/404.html") + case "apache": + custom404Path = filepath.Join(app.Root, "server/apache/htdocs/404.html") + } + if io.Exists(custom404Path) { + notFound, _ = os.ReadFile(custom404Path) } else { switch app.Locale { case "zh_CN": diff --git a/internal/service/cli.go b/internal/service/cli.go index 1393e1ab..9575fb69 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -140,7 +140,6 @@ func (s *CliService) Fix(ctx context.Context, cmd *cli.Command) error { } func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error { - // TODO 未来加权限设置之后这里需要优化 user := new(biz.User) if err := s.db.First(user).Error; err != nil { return errors.New(s.t.Get("Failed to get user info: %v", err)) diff --git a/internal/service/website.go b/internal/service/website.go index dc9e877f..3f98fc17 100644 --- a/internal/service/website.go +++ b/internal/service/website.go @@ -35,9 +35,20 @@ func (s *WebsiteService) GetRewrites(w http.ResponseWriter, r *http.Request) { } func (s *WebsiteService) GetDefaultConfig(w http.ResponseWriter, r *http.Request) { - index, _ := io.Read(filepath.Join(app.Root, "server/nginx/html/index.html")) - stop, _ := io.Read(filepath.Join(app.Root, "server/nginx/html/stop.html")) - notFound, _ := io.Read(filepath.Join(app.Root, "server/nginx/html/404.html")) + webServer, _ := s.settingRepo.Get(biz.SettingKeyWebserver) + var htmlPath string + switch webServer { + case "nginx": + htmlPath = filepath.Join(app.Root, "server/nginx/html") + case "apache": + htmlPath = filepath.Join(app.Root, "server/apache/htdocs") + default: + htmlPath = filepath.Join(app.Root, "server/nginx/html") + } + + index, _ := io.Read(filepath.Join(htmlPath, "index.html")) + stop, _ := io.Read(filepath.Join(htmlPath, "stop.html")) + notFound, _ := io.Read(filepath.Join(htmlPath, "404.html")) tlsVersions, _ := s.settingRepo.GetSlice(biz.SettingKeyWebsiteTLSVersions) cipherSuites, _ := s.settingRepo.Get(biz.SettingKeyWebsiteCipherSuites) diff --git a/pkg/webserver/apache/data.go b/pkg/webserver/apache/data.go index 412ba6f4..b74346a2 100644 --- a/pkg/webserver/apache/data.go +++ b/pkg/webserver/apache/data.go @@ -165,8 +165,8 @@ var order = map[string]int{ "Proxy": 1311, "ProxyMatch": 1312, - "Include": 1400, - "IncludeOptional": 1401, + "Include": 1290, + "IncludeOptional": 1291, "ErrorLog": 1500, "CustomLog": 1501, diff --git a/pkg/webserver/apache/parser.go b/pkg/webserver/apache/parser.go index 7a2d8ec1..438864d8 100644 --- a/pkg/webserver/apache/parser.go +++ b/pkg/webserver/apache/parser.go @@ -205,11 +205,11 @@ func (p *Parser) parseVirtualHost(token Token) (*VirtualHost, error) { return nil, err } - // 检查是否是Include类指令 + // 检查是否是 Include 类指令 if p.options.ProcessIncludes && (strings.EqualFold(directive.Name, "Include") || strings.EqualFold(directive.Name, "IncludeOptional")) { includeConfig, err := p.processInclude(directive) if err != nil { - // 对于IncludeOptional,如果文件不存在则忽略错误 + // 对于 IncludeOptional,如果文件不存在则忽略错误 if strings.EqualFold(directive.Name, "IncludeOptional") && os.IsNotExist(err) { continue } @@ -228,6 +228,21 @@ func (p *Parser) parseVirtualHost(token Token) (*VirtualHost, error) { } else { vhost.Directives = append(vhost.Directives, directive) } + } else if nextToken.Type == BLOCKDIRECTIVE { + // 处理虚拟主机内的块指令(如 Directory, Location 等) + block, err := p.parseBlockDirective(nextToken) + if err != nil { + return nil, err + } + // 将块指令作为带 Block 的 Directive 添加到虚拟主机 + directive := &Directive{ + Name: block.Type, + Args: block.Args, + Line: block.Line, + Column: block.Column, + Block: block, + } + vhost.Directives = append(vhost.Directives, directive) } else if nextToken.Type == COMMENT { // 收集虚拟主机内的注释 comment := &Comment{ diff --git a/pkg/webserver/apache/proxy.go b/pkg/webserver/apache/proxy.go index 5e4d5efa..bdd0d67e 100644 --- a/pkg/webserver/apache/proxy.go +++ b/pkg/webserver/apache/proxy.go @@ -28,7 +28,7 @@ func parseProxyFiles(siteDir string) ([]types.Proxy, error) { return nil, err } - var proxies []types.Proxy + proxies := make([]types.Proxy, 0) for _, entry := range entries { if entry.IsDir() { continue @@ -66,6 +66,7 @@ func parseProxyFile(filePath string) (*types.Proxy, error) { contentStr := string(content) proxy := &types.Proxy{ + Resolver: []string{}, Replaces: make(map[string]string), } @@ -188,6 +189,18 @@ func generateProxyConfig(proxy types.Proxy) string { 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)) @@ -261,7 +274,7 @@ func parseBalancerFiles(sharedDir string) ([]types.Upstream, error) { return nil, err } - var upstreams []types.Upstream + upstreams := make([]types.Upstream, 0) for _, entry := range entries { if entry.IsDir() { continue @@ -294,8 +307,9 @@ func parseBalancerFile(filePath string, name string) (*types.Upstream, error) { contentStr := string(content) upstream := &types.Upstream{ - Name: name, - Servers: make(map[string]string), + Name: name, + Servers: make(map[string]string), + Resolver: []string{}, } // 解析 块 diff --git a/pkg/webserver/apache/vhost.go b/pkg/webserver/apache/vhost.go index 15e7e9bc..8a03cce3 100644 --- a/pkg/webserver/apache/vhost.go +++ b/pkg/webserver/apache/vhost.go @@ -641,14 +641,14 @@ func (v *PHPVhost) PHP() uint { } // 从配置内容中提取版本号 - // 格式: proxy:unix:/tmp/php-cgi-84.sock|fcgi://localhost - idx := strings.Index(content, "php-cgi-") + // 格式: Include /opt/ace/server/apache/conf/extra/enable-php-85.conf + idx := strings.Index(content, "enable-php-") if idx == -1 { return 0 } var result uint - _, err := fmt.Sscanf(content[idx:], "php-cgi-%d.sock", &result) + _, err := fmt.Sscanf(content[idx:], "enable-php-%d.conf", &result) if err != nil { return 0 } @@ -660,12 +660,9 @@ func (v *PHPVhost) SetPHP(version uint) error { return v.RemoveConfig("010-php.conf", "site") } - // 生成 PHP-FPM 配置 - // sock 路径格式: unix:/tmp/php-cgi-84.sock - content := fmt.Sprintf(` - SetHandler "proxy:unix:/tmp/php-cgi-%d.sock|fcgi://localhost" - -`, version) + // 生成 PHP-FPM 配置,直接 Include Apache 的 PHP-FPM 配置文件 + // 配置文件路径: /opt/ace/server/apache/conf/extra/enable-php-85.conf + content := fmt.Sprintf("Include conf/extra/enable-php-%d.conf\n", version) return v.SetConfig("010-php.conf", "site", content) }