2026-09-23 09:40:59 +08:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"io/fs"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/fonu/fonu/internal/auth"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func NewRouter(deps Deps) http.Handler {
|
|
|
|
|
logsHandler := NewLogsHandler(deps.Config)
|
|
|
|
|
r := &Router{
|
2026-09-25 13:05:58 +08:00
|
|
|
authHandler: NewAuthHandler(deps.Auth, deps.Notify),
|
|
|
|
|
proxyHandler: NewProxyHandler(deps.Config, deps.Proxy, logsHandler, deps.Traffic),
|
|
|
|
|
nginxHandler: NewNginxHandler(deps.Proxy),
|
|
|
|
|
statusHandler: NewStatusHandler(deps.Config, deps.Proxy, deps.DDNS, deps.ACME, deps.StartedAt, deps.Config.NginxPIDFile),
|
|
|
|
|
ddnsHandler: NewDDNSHandler(deps.DDNS),
|
|
|
|
|
certHandler: NewCertHandler(deps.ACME),
|
|
|
|
|
settingsHandler: NewSettingsHandler(deps.Settings, deps.Proxy, deps.Notify),
|
|
|
|
|
chinaCIDRHandler: NewChinaCIDRHandler(deps.ChinaCIDR),
|
|
|
|
|
logsHandler: logsHandler,
|
|
|
|
|
backupHandler: NewBackupHandler(deps.Backup, deps.Settings),
|
|
|
|
|
statusPageHandler: NewStatusPageHandler(deps.Settings, deps.FRPMulti),
|
|
|
|
|
metricsHandler: NewMetricsHandler(deps.Metrics),
|
|
|
|
|
discoveryHandler: NewDiscoveryHandler(deps.Discovery),
|
|
|
|
|
frpmultiHandler: NewFRPMultiHandler(deps.FRPMulti),
|
2026-09-27 18:43:34 +08:00
|
|
|
updateHandler: NewUpdateHandler(deps.Updater),
|
2026-09-25 13:05:58 +08:00
|
|
|
authSvc: deps.Auth,
|
|
|
|
|
staticFS: deps.StaticFS,
|
2026-09-23 09:40:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
|
|
|
|
mux.HandleFunc("GET /api/version", Version(deps.Config))
|
2026-09-25 13:05:58 +08:00
|
|
|
// 唯一一个匿名只读接口:默认关闭、可设访问码、按 IP 限流
|
|
|
|
|
mux.HandleFunc("GET /api/status-page", r.statusPageHandler.Get)
|
2026-09-23 09:40:59 +08:00
|
|
|
mux.HandleFunc("GET /api/auth/status", r.authHandler.Status)
|
|
|
|
|
mux.HandleFunc("POST /api/auth/login", r.authHandler.Login)
|
|
|
|
|
mux.HandleFunc("POST /api/auth/logout", r.authHandler.Logout)
|
|
|
|
|
|
|
|
|
|
protect := func(pattern string, handler http.HandlerFunc) {
|
|
|
|
|
mux.Handle(pattern, SessionMiddleware(deps.Auth)(RequireAuth(deps.Auth)(http.HandlerFunc(handler))))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protect("POST /api/auth/password", r.authHandler.ChangePassword)
|
2026-09-24 20:50:17 +08:00
|
|
|
protect("GET /api/settings/tokens", r.authHandler.ListTokens)
|
|
|
|
|
protect("POST /api/settings/tokens", r.authHandler.CreateToken)
|
|
|
|
|
protect("DELETE /api/settings/tokens/{id}", r.authHandler.DeleteToken)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("GET /api/status", r.statusHandler.Get)
|
|
|
|
|
protect("GET /api/proxies", r.proxyHandler.List)
|
|
|
|
|
protect("GET /api/proxies/traffic", r.proxyHandler.Traffic)
|
|
|
|
|
protect("POST /api/proxies", r.proxyHandler.Create)
|
|
|
|
|
protect("PUT /api/proxies/reorder", r.proxyHandler.Reorder)
|
|
|
|
|
protect("PUT /api/proxies/{id}", r.proxyHandler.Update)
|
|
|
|
|
protect("DELETE /api/proxies/{id}", r.proxyHandler.Delete)
|
|
|
|
|
protect("GET /api/proxies/{id}/logs/stream", r.proxyHandler.StreamLogs)
|
|
|
|
|
protect("GET /api/proxies/{id}/clients", r.proxyHandler.Clients)
|
|
|
|
|
protect("GET /api/proxies/{id}/nginx", r.nginxHandler.GetRule)
|
|
|
|
|
protect("PUT /api/proxies/{id}/nginx", r.nginxHandler.PutRule)
|
|
|
|
|
protect("POST /api/proxies/{id}/nginx/rollback", r.nginxHandler.RollbackRule)
|
|
|
|
|
protect("GET /api/settings/nginx/global", r.nginxHandler.GetGlobal)
|
|
|
|
|
protect("PUT /api/settings/nginx/global", r.nginxHandler.PutGlobal)
|
|
|
|
|
protect("POST /api/settings/nginx/global/rollback", r.nginxHandler.RollbackGlobal)
|
2026-09-24 20:50:17 +08:00
|
|
|
protect("GET /api/settings/nginx/config/versions", r.nginxHandler.GetConfigVersions)
|
|
|
|
|
protect("GET /api/settings/nginx/config/versions/{name}", r.nginxHandler.GetConfigVersion)
|
|
|
|
|
protect("POST /api/settings/nginx/config/versions/{name}/rollback", r.nginxHandler.RollbackConfigVersion)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("GET /api/ddns", r.ddnsHandler.List)
|
|
|
|
|
protect("POST /api/ddns", r.ddnsHandler.Create)
|
|
|
|
|
protect("PUT /api/ddns/{id}", r.ddnsHandler.Update)
|
|
|
|
|
protect("DELETE /api/ddns/{id}", r.ddnsHandler.Delete)
|
|
|
|
|
protect("POST /api/ddns/test", r.ddnsHandler.Test)
|
|
|
|
|
protect("POST /api/ddns/update", r.ddnsHandler.UpdateAll)
|
|
|
|
|
protect("POST /api/ddns/{id}/update", r.ddnsHandler.UpdateOne)
|
|
|
|
|
protect("GET /api/certificates", r.certHandler.List)
|
|
|
|
|
protect("GET /api/certificates/ca-options", r.certHandler.Options)
|
|
|
|
|
protect("POST /api/certificates/apply", r.certHandler.Apply)
|
|
|
|
|
protect("GET /api/certificates/jobs/{id}/stream", r.certHandler.ApplyJobStream)
|
|
|
|
|
protect("POST /api/certificates/import", r.certHandler.Import)
|
|
|
|
|
protect("POST /api/certificates/renew", r.certHandler.Renew)
|
|
|
|
|
protect("GET /api/certificates/{domain}/download", r.certHandler.Download)
|
|
|
|
|
protect("DELETE /api/certificates/{domain}", r.certHandler.Delete)
|
|
|
|
|
notifyHandler := NewNotifyHandler(deps.Notify)
|
|
|
|
|
protect("GET /api/settings", r.settingsHandler.Get)
|
|
|
|
|
protect("PUT /api/settings", r.settingsHandler.Put)
|
|
|
|
|
protect("POST /api/settings/notify/test", notifyHandler.Test)
|
|
|
|
|
if deps.ChinaCIDR != nil {
|
|
|
|
|
protect("GET /api/settings/china-cidr", r.chinaCIDRHandler.Status)
|
|
|
|
|
protect("POST /api/settings/china-cidr/refresh", r.chinaCIDRHandler.Refresh)
|
|
|
|
|
}
|
|
|
|
|
protect("GET /api/logs/access", r.logsHandler.Access)
|
|
|
|
|
protect("GET /api/logs/error", r.logsHandler.Error)
|
|
|
|
|
protect("GET /api/logs/system", r.logsHandler.System)
|
2026-09-25 13:05:58 +08:00
|
|
|
protect("GET /api/metrics/history", r.metricsHandler.History)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("GET /api/logs/stream", r.logsHandler.Stream)
|
2026-09-27 18:43:34 +08:00
|
|
|
protect("GET /api/system/update/status", r.updateHandler.Status)
|
|
|
|
|
protect("POST /api/system/update/apply", r.updateHandler.Apply)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("GET /api/backup/export", r.backupHandler.Export)
|
|
|
|
|
protect("POST /api/backup/restore", r.backupHandler.Restore)
|
2026-09-25 13:05:58 +08:00
|
|
|
protect("GET /api/backup/archives", r.backupHandler.ListArchives)
|
|
|
|
|
protect("POST /api/backup/archives", r.backupHandler.CreateArchive)
|
|
|
|
|
protect("GET /api/backup/archives/{name}", r.backupHandler.DownloadArchive)
|
|
|
|
|
protect("POST /api/backup/archives/{name}/restore", r.backupHandler.RestoreArchive)
|
|
|
|
|
protect("DELETE /api/backup/archives/{name}", r.backupHandler.DeleteArchive)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("GET /api/discovery/scan", r.discoveryHandler.Scan)
|
|
|
|
|
if deps.FRPMulti != nil {
|
|
|
|
|
frpHandler := r.frpmultiHandler
|
|
|
|
|
protect("GET /api/frpmulti/status", frpHandler.Status)
|
|
|
|
|
protect("GET /api/frpmulti/runtime", frpHandler.Runtime)
|
|
|
|
|
protect("PUT /api/frpmulti/runtime/download-proxy", frpHandler.SetDownloadProxy)
|
|
|
|
|
protect("GET /api/frpmulti/releases", frpHandler.Releases)
|
|
|
|
|
protect("GET /api/frpmulti/binaries", frpHandler.Binaries)
|
|
|
|
|
protect("GET /api/frpmulti/download", frpHandler.DownloadStatus)
|
|
|
|
|
protect("POST /api/frpmulti/releases/{version}/download", frpHandler.DownloadRelease)
|
|
|
|
|
protect("POST /api/frpmulti/binaries/{version}/activate", frpHandler.ActivateBinary)
|
|
|
|
|
protect("DELETE /api/frpmulti/binaries/{version}", frpHandler.DeleteBinary)
|
|
|
|
|
protect("GET /api/frpmulti/servers", frpHandler.ListServers)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/detail", frpHandler.ServerDetail)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/diagnostics", frpHandler.ServerDiagnostics)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/frps-config", frpHandler.FRPSServerConfig)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/start", frpHandler.StartServer)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/stop", frpHandler.StopServer)
|
|
|
|
|
protect("POST /api/frpmulti/servers", frpHandler.CreateServer)
|
|
|
|
|
protect("PUT /api/frpmulti/servers/{id}", frpHandler.UpdateServer)
|
|
|
|
|
protect("DELETE /api/frpmulti/servers/{id}", frpHandler.DeleteServer)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/test", frpHandler.TestServer)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/probe", frpHandler.AgentProbe)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/install", frpHandler.AgentInstall)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/uninstall", frpHandler.AgentUninstall)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/ssh-diagnose", frpHandler.AgentSSHDiagnose)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/test", frpHandler.AgentTest)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/status", frpHandler.AgentStatus)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/routes", frpHandler.AgentRoutes)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/routes/health", frpHandler.AgentRouteHealth)
|
2026-09-23 19:10:43 +08:00
|
|
|
protect("GET /api/frpmulti/routes/summary", frpHandler.AgentRouteHealthSummary)
|
2026-09-25 13:05:58 +08:00
|
|
|
protect("GET /api/frpmulti/routes/uptime", frpHandler.RouteUptime)
|
2026-09-24 09:20:07 +08:00
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/routes/drift", frpHandler.AgentRouteDrift)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/routes/redeploy", frpHandler.AgentRedeployRoutes)
|
2026-09-24 10:15:11 +08:00
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/metrics", frpHandler.AgentMetrics)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/logs/nginx", frpHandler.AgentNginxLogs)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/frps-config", frpHandler.AgentFRPSConfig)
|
2026-09-24 10:49:28 +08:00
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/certs", frpHandler.AgentCerts)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/nginx/reload", frpHandler.AgentNginxReload)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/nginx/install", frpHandler.AgentInstallNginx)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/conf", frpHandler.AgentRouteConf)
|
2026-09-24 20:50:17 +08:00
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/versions", frpHandler.AgentRouteVersions)
|
|
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/versions/{name}", frpHandler.AgentRouteVersion)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/routes/{proxyID}/versions/{name}/rollback", frpHandler.AgentRollbackRouteVersion)
|
2026-09-23 09:40:59 +08:00
|
|
|
protect("GET /api/frpmulti/servers/{id}/agent/route-candidates", frpHandler.AgentRouteCandidates)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/routes/{proxyID}", frpHandler.DeployAgentRoute)
|
|
|
|
|
protect("DELETE /api/frpmulti/servers/{id}/agent/routes/{proxyID}", frpHandler.RemoveAgentRoute)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/frps-install", frpHandler.InstallFRPSOnAgent)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/frps-action", frpHandler.AgentFRPSAction)
|
|
|
|
|
protect("POST /api/frpmulti/servers/{id}/agent/frps-config", frpHandler.PushFRPSConfigToAgent)
|
|
|
|
|
protect("GET /api/frpmulti/proxies", frpHandler.ListProxies)
|
|
|
|
|
protect("GET /api/frpmulti/proxies/traffic", frpHandler.ListProxiesTraffic)
|
|
|
|
|
protect("POST /api/frpmulti/proxies", frpHandler.CreateProxy)
|
|
|
|
|
protect("PUT /api/frpmulti/proxies/{id}", frpHandler.UpdateProxy)
|
|
|
|
|
protect("DELETE /api/frpmulti/proxies/{id}", frpHandler.DeleteProxy)
|
|
|
|
|
protect("GET /api/frpmulti/proxies/{id}/logs", frpHandler.ProxyLogs)
|
|
|
|
|
protect("POST /api/frpmulti/start", frpHandler.Start)
|
|
|
|
|
protect("POST /api/frpmulti/stop", frpHandler.Stop)
|
|
|
|
|
protect("POST /api/frpmulti/reload", frpHandler.Reload)
|
|
|
|
|
protect("GET /api/frpmulti/logs", frpHandler.Logs)
|
|
|
|
|
protect("GET /api/frpmulti/config/export", frpHandler.ExportConfig)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if deps.StaticFS != nil {
|
|
|
|
|
mux.Handle("/", r.spaHandler())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger := deps.Logger
|
|
|
|
|
if logger == nil {
|
|
|
|
|
logger = slog.Default()
|
|
|
|
|
}
|
|
|
|
|
return loggingMiddleware(logger, SessionMiddleware(deps.Auth)(mux))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Router struct {
|
2026-09-25 13:05:58 +08:00
|
|
|
authHandler *AuthHandler
|
|
|
|
|
proxyHandler *ProxyHandler
|
|
|
|
|
nginxHandler *NginxHandler
|
|
|
|
|
statusHandler *StatusHandler
|
|
|
|
|
ddnsHandler *DDNSHandler
|
|
|
|
|
certHandler *CertHandler
|
|
|
|
|
settingsHandler *SettingsHandler
|
|
|
|
|
chinaCIDRHandler *ChinaCIDRHandler
|
|
|
|
|
logsHandler *LogsHandler
|
|
|
|
|
backupHandler *BackupHandler
|
|
|
|
|
statusPageHandler *StatusPageHandler
|
|
|
|
|
metricsHandler *MetricsHandler
|
|
|
|
|
discoveryHandler *DiscoveryHandler
|
|
|
|
|
frpmultiHandler *FRPMultiHandler
|
2026-09-27 18:43:34 +08:00
|
|
|
updateHandler *UpdateHandler
|
2026-09-25 13:05:58 +08:00
|
|
|
authSvc *auth.Service
|
|
|
|
|
staticFS fs.FS
|
2026-09-23 09:40:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Router) spaHandler() http.Handler {
|
|
|
|
|
fileServer := http.FileServer(http.FS(r.staticFS))
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
if strings.HasPrefix(req.URL.Path, "/api/") {
|
|
|
|
|
http.NotFound(w, req)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path := strings.TrimPrefix(req.URL.Path, "/")
|
|
|
|
|
if path == "" {
|
|
|
|
|
path = "index.html"
|
|
|
|
|
}
|
|
|
|
|
if _, err := fs.Stat(r.staticFS, path); err != nil {
|
|
|
|
|
// SPA fallback: unknown routes serve index.html
|
|
|
|
|
req.URL.Path = "/index.html"
|
|
|
|
|
}
|
|
|
|
|
fileServer.ServeHTTP(w, req)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type statusRecorder struct {
|
|
|
|
|
http.ResponseWriter
|
|
|
|
|
status int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *statusRecorder) WriteHeader(code int) {
|
|
|
|
|
w.status = code
|
|
|
|
|
w.ResponseWriter.WriteHeader(code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *statusRecorder) Flush() {
|
|
|
|
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
|
|
|
|
f.Flush()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
start := time.Now()
|
|
|
|
|
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
|
|
|
|
next.ServeHTTP(rec, r)
|
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/api/") &&
|
|
|
|
|
!strings.Contains(r.URL.Path, "/logs/stream") &&
|
|
|
|
|
!strings.Contains(r.URL.Path, "/certificates/jobs/") &&
|
|
|
|
|
rec.status >= 400 {
|
|
|
|
|
level := slog.LevelWarn
|
|
|
|
|
msg := "api request completed with client error"
|
|
|
|
|
if rec.status >= 500 {
|
|
|
|
|
level = slog.LevelError
|
|
|
|
|
msg = "api request failed"
|
|
|
|
|
}
|
|
|
|
|
logger.Log(r.Context(), level, msg,
|
|
|
|
|
"module", "HTTP",
|
|
|
|
|
"method", r.Method,
|
|
|
|
|
"path", r.URL.Path,
|
|
|
|
|
"status", rec.status,
|
|
|
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|