diff --git a/cmd/ace/wire_gen.go b/cmd/ace/wire_gen.go
index ee02fc9c..2c2957b2 100644
--- a/cmd/ace/wire_gen.go
+++ b/cmd/ace/wire_gen.go
@@ -8,6 +8,7 @@ package main
import (
"github.com/acepanel/panel/internal/app"
+ "github.com/acepanel/panel/internal/apps/apache"
"github.com/acepanel/panel/internal/apps/codeserver"
"github.com/acepanel/panel/internal/apps/docker"
"github.com/acepanel/panel/internal/apps/fail2ban"
@@ -127,6 +128,7 @@ func initWeb() (*app.Web, error) {
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
webHookRepo := data.NewWebHookRepo(locale, db, logger)
webHookService := service.NewWebHookService(webHookRepo)
+ apacheApp := apache.NewApp(locale)
codeserverApp := codeserver.NewApp()
dockerApp := docker.NewApp()
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
@@ -147,7 +149,7 @@ func initWeb() (*app.Web, error) {
rsyncApp := rsync.NewApp(locale)
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
- loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
+ loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, loader)
wsService := service.NewWsService(locale, config, logger, sshRepo)
ws := route.NewWs(wsService)
diff --git a/cmd/cli/wire_gen.go b/cmd/cli/wire_gen.go
index 536c465a..9bbf8ebc 100644
--- a/cmd/cli/wire_gen.go
+++ b/cmd/cli/wire_gen.go
@@ -8,6 +8,7 @@ package main
import (
"github.com/acepanel/panel/internal/app"
+ "github.com/acepanel/panel/internal/apps/apache"
"github.com/acepanel/panel/internal/apps/codeserver"
"github.com/acepanel/panel/internal/apps/docker"
"github.com/acepanel/panel/internal/apps/fail2ban"
@@ -72,6 +73,7 @@ func initCli() (*app.Cli, error) {
cli := route.NewCli(locale, cliService)
command := bootstrap.NewCli(locale, cli)
gormigrate := bootstrap.NewMigrate(db)
+ apacheApp := apache.NewApp(locale)
codeserverApp := codeserver.NewApp()
dockerApp := docker.NewApp()
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
@@ -92,7 +94,7 @@ func initCli() (*app.Cli, error) {
rsyncApp := rsync.NewApp(locale)
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
- loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
+ loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
appCli := app.NewCli(command, gormigrate, loader)
return appCli, nil
}
diff --git a/internal/apps/apache/app.go b/internal/apps/apache/app.go
new file mode 100644
index 00000000..81181331
--- /dev/null
+++ b/internal/apps/apache/app.go
@@ -0,0 +1,157 @@
+package apache
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/leonelquinteros/gotext"
+ "github.com/spf13/cast"
+
+ "github.com/acepanel/panel/internal/app"
+ "github.com/acepanel/panel/internal/service"
+ "github.com/acepanel/panel/pkg/io"
+ "github.com/acepanel/panel/pkg/shell"
+ "github.com/acepanel/panel/pkg/systemctl"
+ "github.com/acepanel/panel/pkg/tools"
+ "github.com/acepanel/panel/pkg/types"
+)
+
+type App struct {
+ t *gotext.Locale
+}
+
+func NewApp(t *gotext.Locale) *App {
+ return &App{
+ t: t,
+ }
+}
+
+func (s *App) Route(r chi.Router) {
+ r.Get("/load", s.Load)
+ r.Get("/config", s.GetConfig)
+ r.Post("/config", s.SaveConfig)
+ r.Get("/error_log", s.ErrorLog)
+ r.Post("/clear_error_log", s.ClearErrorLog)
+}
+
+func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
+ config, err := io.Read(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root))
+ if err != nil {
+ service.Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ service.Success(w, config)
+}
+
+func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) {
+ req, err := service.Bind[UpdateConfig](r)
+ if err != nil {
+ service.Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = io.Write(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root), req.Config, 0600); err != nil {
+ service.Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ if err = systemctl.Reload("apache"); err != nil {
+ _, err = shell.Execf("%s/server/apache/bin/apachectl configtest", app.Root)
+ service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload apache: %v", err))
+ return
+ }
+
+ service.Success(w, nil)
+}
+
+func (s *App) ErrorLog(w http.ResponseWriter, r *http.Request) {
+ service.Success(w, fmt.Sprintf("%s/%s", app.Root, "server/apache/logs/error_log"))
+}
+
+func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
+ if _, err := shell.Execf("cat /dev/null > %s/%s", app.Root, "server/apache/logs/error_log"); err != nil {
+ service.Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ service.Success(w, nil)
+}
+
+func (s *App) Load(w http.ResponseWriter, r *http.Request) {
+ status, err := shell.Execf("curl -s http://127.0.0.1/server_status?auto 2>/dev/null || true")
+ if err != nil {
+ service.Success(w, []types.NV{})
+ return
+ }
+
+ var data []types.NV
+
+ workers, err := shell.Execf("ps aux | grep httpd | grep -v grep | wc -l")
+ if err != nil {
+ service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get apache workers: %v", err))
+ return
+ }
+ data = append(data, types.NV{
+ Name: s.t.Get("Workers"),
+ Value: workers,
+ })
+
+ out, err := shell.Execf("ps aux | grep httpd | grep -v grep | awk '{memsum+=$6};END {print memsum}'")
+ if err != nil {
+ service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get apache workers: %v", err))
+ return
+ }
+ mem := tools.FormatBytes(cast.ToFloat64(out))
+ data = append(data, types.NV{
+ Name: s.t.Get("Memory"),
+ Value: mem,
+ })
+
+ // Parse server-status output
+ if match := regexp.MustCompile(`Total Accesses:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
+ data = append(data, types.NV{
+ Name: s.t.Get("Total Accesses"),
+ Value: match[1],
+ })
+ }
+
+ if match := regexp.MustCompile(`Total kBytes:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
+ data = append(data, types.NV{
+ Name: s.t.Get("Total Traffic"),
+ Value: tools.FormatBytes(cast.ToFloat64(match[1]) * 1024),
+ })
+ }
+
+ if match := regexp.MustCompile(`BusyWorkers:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
+ data = append(data, types.NV{
+ Name: s.t.Get("Busy Workers"),
+ Value: match[1],
+ })
+ }
+
+ if match := regexp.MustCompile(`IdleWorkers:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
+ data = append(data, types.NV{
+ Name: s.t.Get("Idle Workers"),
+ Value: match[1],
+ })
+ }
+
+ if match := regexp.MustCompile(`ReqPerSec:\s*([\d.]+)`).FindStringSubmatch(status); len(match) == 2 {
+ data = append(data, types.NV{
+ Name: s.t.Get("Requests/sec"),
+ Value: match[1],
+ })
+ }
+
+ if match := regexp.MustCompile(`BytesPerSec:\s*([\d.]+)`).FindStringSubmatch(status); len(match) == 2 {
+ data = append(data, types.NV{
+ Name: s.t.Get("Bytes/sec"),
+ Value: tools.FormatBytes(cast.ToFloat64(match[1])),
+ })
+ }
+
+ service.Success(w, data)
+}
diff --git a/internal/apps/apache/request.go b/internal/apps/apache/request.go
new file mode 100644
index 00000000..61829ed1
--- /dev/null
+++ b/internal/apps/apache/request.go
@@ -0,0 +1,5 @@
+package apache
+
+type UpdateConfig struct {
+ Config string `form:"config" json:"config" validate:"required"`
+}
diff --git a/internal/apps/apps.go b/internal/apps/apps.go
index cae4d8cd..5ae1430f 100644
--- a/internal/apps/apps.go
+++ b/internal/apps/apps.go
@@ -3,6 +3,7 @@ package apps
import (
"github.com/google/wire"
+ "github.com/acepanel/panel/internal/apps/apache"
"github.com/acepanel/panel/internal/apps/codeserver"
"github.com/acepanel/panel/internal/apps/docker"
"github.com/acepanel/panel/internal/apps/fail2ban"
@@ -26,6 +27,7 @@ import (
)
var ProviderSet = wire.NewSet(
+ apache.NewApp,
codeserver.NewApp,
docker.NewApp,
fail2ban.NewApp,
diff --git a/internal/bootstrap/apps.go b/internal/bootstrap/apps.go
index b504d8d0..bae60b97 100644
--- a/internal/bootstrap/apps.go
+++ b/internal/bootstrap/apps.go
@@ -1,6 +1,7 @@
package bootstrap
import (
+ "github.com/acepanel/panel/internal/apps/apache"
"github.com/acepanel/panel/internal/apps/codeserver"
"github.com/acepanel/panel/internal/apps/docker"
"github.com/acepanel/panel/internal/apps/fail2ban"
@@ -25,6 +26,7 @@ import (
)
func NewLoader(
+ apache *apache.App,
codeserver *codeserver.App,
docker *docker.App,
fail2ban *fail2ban.App,
@@ -47,6 +49,6 @@ func NewLoader(
supervisor *supervisor.App,
) *apploader.Loader {
loader := new(apploader.Loader)
- loader.Add(codeserver, docker, fail2ban, frp, gitea, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
+ loader.Add(apache, codeserver, docker, fail2ban, frp, gitea, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
return loader
}
diff --git a/pkg/webserver/apache/vhost.go b/pkg/webserver/apache/vhost.go
index af6a7abb..15e7e9bc 100644
--- a/pkg/webserver/apache/vhost.go
+++ b/pkg/webserver/apache/vhost.go
@@ -165,7 +165,12 @@ func (v *baseVhost) Listen() []types.Listen {
func (v *baseVhost) SetListen(listens []types.Listen) error {
var args []string
for _, l := range listens {
- args = append(args, l.Address)
+ addr := l.Address
+ // 如果只是端口号,添加 *: 前缀
+ if !strings.Contains(addr, ":") {
+ addr = "*:" + addr
+ }
+ args = append(args, addr)
}
v.vhost.Args = args
return nil
diff --git a/web/src/api/apps/apache/index.ts b/web/src/api/apps/apache/index.ts
new file mode 100644
index 00000000..8427bb01
--- /dev/null
+++ b/web/src/api/apps/apache/index.ts
@@ -0,0 +1,14 @@
+import { http } from '@/utils'
+
+export default {
+ // 负载状态
+ load: (): any => http.Get('/apps/apache/load'),
+ // 获取配置
+ config: (): any => http.Get('/apps/apache/config'),
+ // 保存配置
+ saveConfig: (config: string): any => http.Post('/apps/apache/config', { config }),
+ // 获取错误日志
+ errorLog: (): any => http.Get('/apps/apache/error_log'),
+ // 清空错误日志
+ clearErrorLog: (): any => http.Post('/apps/apache/clear_error_log')
+}
diff --git a/web/src/views/apps/apache/IndexView.vue b/web/src/views/apps/apache/IndexView.vue
new file mode 100644
index 00000000..3ed74d92
--- /dev/null
+++ b/web/src/views/apps/apache/IndexView.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $gettext(
+ 'This modifies the %{name} main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!',
+ { name: 'Apache' }
+ )
+ }}
+
+
+
+
+ {{ $gettext('Save') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Clear Log') }}
+
+
+
+
+
+
+
+
diff --git a/web/src/views/apps/apache/route.ts b/web/src/views/apps/apache/route.ts
new file mode 100644
index 00000000..fe5cc075
--- /dev/null
+++ b/web/src/views/apps/apache/route.ts
@@ -0,0 +1,22 @@
+import type { RouteType } from '~/types/router'
+
+const Layout = () => import('@/layout/IndexView.vue')
+
+export default {
+ name: 'apache',
+ path: '/apps/apache',
+ component: Layout,
+ isHidden: true,
+ children: [
+ {
+ name: 'apps-apache-index',
+ path: '',
+ component: () => import('./IndexView.vue'),
+ meta: {
+ title: 'Apache',
+ role: ['admin'],
+ requireAuth: true
+ }
+ }
+ ]
+} as RouteType