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 @@ + + + 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