This commit is contained in:
@@ -101,6 +101,10 @@ func (h *CloudflareHandler) Restart(w http.ResponseWriter, r *http.Request) {
|
||||
h.tunnelAction(w, r, h.svc.Restart)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) RefreshCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
h.tunnelAction(w, r, h.svc.RefreshCredentials)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) tunnelAction(w http.ResponseWriter, r *http.Request, fn func(context.Context, int64) (cloudflared.Tunnel, error)) {
|
||||
id, err := parseID(r.PathValue("id"))
|
||||
if err != nil {
|
||||
@@ -158,6 +162,102 @@ func (h *CloudflareHandler) Config(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"content": content})
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) Routes(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "隧道 ID 无效")
|
||||
return
|
||||
}
|
||||
routes, err := h.svc.Routes(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, routes)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) ReplaceRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "隧道 ID 无效")
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Routes []cloudflared.RouteInput `json:"routes"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
||||
return
|
||||
}
|
||||
routes, err := h.svc.ReplaceRoutes(r.Context(), id, request.Routes)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, routes)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) Drift(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "隧道 ID 无效")
|
||||
return
|
||||
}
|
||||
result, err := h.svc.Drift(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) SyncRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "隧道 ID 无效")
|
||||
return
|
||||
}
|
||||
if err := h.svc.PushTunnel(r.Context(), id); err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) Diagnostics(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "隧道 ID 无效")
|
||||
return
|
||||
}
|
||||
result, err := h.svc.Diagnostics(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) Preflight(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, h.svc.Preflight(r.Context()))
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) TestRoute(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Service string `json:"service"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
||||
return
|
||||
}
|
||||
result, err := h.svc.TestService(r.Context(), request.Service)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) BinaryInfo(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"version": h.svc.BinaryVersion(),
|
||||
@@ -225,13 +325,32 @@ func (h *CloudflareHandler) SetEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *CloudflareHandler) DownloadBinary(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Mirror string `json:"mirror"`
|
||||
Mirror string `json:"mirror"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&request); err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
||||
return
|
||||
}
|
||||
version, err := h.svc.DownloadBinary(r.Context(), request.Mirror)
|
||||
version, err := h.svc.DownloadBinaryVersion(r.Context(), request.Mirror, request.Version)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "version": version})
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) LatestBinary(w http.ResponseWriter, r *http.Request) {
|
||||
version, err := h.svc.LatestBinaryVersion(r.Context())
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"version": version})
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) RollbackBinary(w http.ResponseWriter, r *http.Request) {
|
||||
version, err := h.svc.RollbackBinary()
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
@@ -248,6 +367,25 @@ func (h *CloudflareHandler) IngressCandidates(w http.ResponseWriter, r *http.Req
|
||||
writeJSON(w, http.StatusOK, map[string]any{"hostnames": hosts})
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) Zones(w http.ResponseWriter, r *http.Request) {
|
||||
zones, err := h.svc.Zones(r.Context())
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"zones": zones})
|
||||
}
|
||||
|
||||
func (h *CloudflareHandler) AccessStatus(w http.ResponseWriter, r *http.Request) {
|
||||
hostname := strings.TrimSpace(r.URL.Query().Get("hostname"))
|
||||
enabled, name, err := h.svc.AccessStatus(r.Context(), hostname)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"enabled": enabled, "name": name})
|
||||
}
|
||||
|
||||
func normalizeCloudflareNetwork(network cloudflared.NetworkSettings) cloudflared.NetworkSettings {
|
||||
defaults := cloudflared.DefaultNetworkSettings()
|
||||
if strings.TrimSpace(network.TransportProtocol) == "" {
|
||||
|
||||
@@ -36,6 +36,8 @@ type proxyRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Remark *string `json:"remark"` // deprecated alias for name
|
||||
Security *securityRequest `json:"security,omitempty"`
|
||||
Exits []string `json:"exits"`
|
||||
CFTunnelID *int64 `json:"cf_tunnel_id"`
|
||||
}
|
||||
|
||||
type proxyReorderRequest struct {
|
||||
@@ -80,6 +82,8 @@ func (h *ProxyHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
Enabled: boolDefault(req.Enabled, true),
|
||||
Name: name,
|
||||
Security: req.securityInput(),
|
||||
Exits: req.Exits,
|
||||
CFTunnelID: int64Default(req.CFTunnelID, 0),
|
||||
}
|
||||
|
||||
rule, err := h.svc.Create(r.Context(), in)
|
||||
@@ -135,6 +139,12 @@ func (h *ProxyHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
sec := req.securityInput()
|
||||
in.Security = &sec
|
||||
}
|
||||
if req.Exits != nil {
|
||||
in.Exits = &req.Exits
|
||||
}
|
||||
if req.CFTunnelID != nil {
|
||||
in.CFTunnelID = req.CFTunnelID
|
||||
}
|
||||
|
||||
rule, err := h.svc.Update(r.Context(), id, in)
|
||||
if err != nil {
|
||||
@@ -258,3 +268,10 @@ func boolDefault(v *bool, fallback bool) bool {
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func int64Default(v *int64, fallback int64) int64 {
|
||||
if v == nil {
|
||||
return fallback
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
+14
-2
@@ -28,14 +28,14 @@ func newRouterWithSpecs(deps Deps) (http.Handler, []routeSpec) {
|
||||
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),
|
||||
statusHandler: NewStatusHandler(deps.Config, deps.Proxy, deps.DDNS, deps.ACME, deps.Cloudflare, 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),
|
||||
statusPageHandler: NewStatusPageHandler(deps.Settings, deps.FRPMulti, deps.Cloudflare),
|
||||
metricsHandler: NewMetricsHandler(deps.Metrics),
|
||||
discoveryHandler: NewDiscoveryHandler(deps.Discovery),
|
||||
frpmultiHandler: NewFRPMultiHandler(deps.FRPMulti, deps.Auth),
|
||||
@@ -201,16 +201,28 @@ func newRouterWithSpecs(deps Deps) (http.Handler, []routeSpec) {
|
||||
protect("POST /api/cloudflare/tunnels/{id}/start", cf.Start)
|
||||
protect("POST /api/cloudflare/tunnels/{id}/stop", cf.Stop)
|
||||
protect("POST /api/cloudflare/tunnels/{id}/restart", cf.Restart)
|
||||
protect("POST /api/cloudflare/tunnels/{id}/credentials/refresh", cf.RefreshCredentials)
|
||||
protect("GET /api/cloudflare/tunnels/{id}/status", cf.Status)
|
||||
protect("GET /api/cloudflare/tunnels/{id}/logs", cf.Logs)
|
||||
protect("GET /api/cloudflare/tunnels/{id}/config", cf.Config)
|
||||
protect("GET /api/cloudflare/tunnels/{id}/routes", cf.Routes)
|
||||
protect("PUT /api/cloudflare/tunnels/{id}/routes", cf.ReplaceRoutes)
|
||||
protect("GET /api/cloudflare/tunnels/{id}/drift", cf.Drift)
|
||||
protect("POST /api/cloudflare/tunnels/{id}/sync", cf.SyncRoutes)
|
||||
protect("GET /api/cloudflare/tunnels/{id}/diagnostics", cf.Diagnostics)
|
||||
protect("GET /api/cloudflare/binary", cf.BinaryInfo)
|
||||
protect("POST /api/cloudflare/binary/download", cf.DownloadBinary)
|
||||
protect("GET /api/cloudflare/binary/latest", cf.LatestBinary)
|
||||
protect("POST /api/cloudflare/binary/rollback", cf.RollbackBinary)
|
||||
protect("GET /api/cloudflare/settings", cf.AppSettings)
|
||||
protect("PUT /api/cloudflare/settings", cf.SaveAppSettings)
|
||||
protect("POST /api/cloudflare/settings/test", cf.TestToken)
|
||||
protect("POST /api/cloudflare/enabled", cf.SetEnabled)
|
||||
protect("GET /api/cloudflare/ingress-candidates", cf.IngressCandidates)
|
||||
protect("GET /api/cloudflare/zones", cf.Zones)
|
||||
protect("GET /api/cloudflare/access-status", cf.AccessStatus)
|
||||
protect("GET /api/cloudflare/preflight", cf.Preflight)
|
||||
protect("POST /api/cloudflare/routes/test", cf.TestRoute)
|
||||
}
|
||||
|
||||
if deps.StaticFS != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/fonu/fonu/internal/acme"
|
||||
certstore "github.com/fonu/fonu/internal/certificate"
|
||||
"github.com/fonu/fonu/internal/cloudflared"
|
||||
"github.com/fonu/fonu/internal/config"
|
||||
"github.com/fonu/fonu/internal/ddns"
|
||||
"github.com/fonu/fonu/internal/logstore"
|
||||
@@ -22,27 +23,29 @@ import (
|
||||
)
|
||||
|
||||
type StatusHandler struct {
|
||||
cfg config.Config
|
||||
proxySvc *service.ProxyService
|
||||
ddnsSvc *ddns.Service
|
||||
acmeSvc *acme.Service
|
||||
startedAt string
|
||||
nginxPIDFile string
|
||||
cpuMu sync.Mutex
|
||||
cpuSample sysinfo.CPUSample
|
||||
cfg config.Config
|
||||
proxySvc *service.ProxyService
|
||||
ddnsSvc *ddns.Service
|
||||
acmeSvc *acme.Service
|
||||
cloudflareSvc *cloudflared.Service
|
||||
startedAt string
|
||||
nginxPIDFile string
|
||||
cpuMu sync.Mutex
|
||||
cpuSample sysinfo.CPUSample
|
||||
}
|
||||
|
||||
func NewStatusHandler(cfg config.Config, proxySvc *service.ProxyService, ddnsSvc *ddns.Service, acmeSvc *acme.Service, startedAt, nginxPIDFile string) *StatusHandler {
|
||||
func NewStatusHandler(cfg config.Config, proxySvc *service.ProxyService, ddnsSvc *ddns.Service, acmeSvc *acme.Service, cloudflareSvc *cloudflared.Service, startedAt, nginxPIDFile string) *StatusHandler {
|
||||
// 先采一次 CPU 计数器,使首次请求就能算出占用率(否则只能返回 0)
|
||||
_, sample := sysinfo.Read(sysinfo.CPUSample{})
|
||||
return &StatusHandler{
|
||||
cfg: cfg,
|
||||
proxySvc: proxySvc,
|
||||
ddnsSvc: ddnsSvc,
|
||||
acmeSvc: acmeSvc,
|
||||
startedAt: startedAt,
|
||||
nginxPIDFile: nginxPIDFile,
|
||||
cpuSample: sample,
|
||||
cfg: cfg,
|
||||
proxySvc: proxySvc,
|
||||
ddnsSvc: ddnsSvc,
|
||||
acmeSvc: acmeSvc,
|
||||
cloudflareSvc: cloudflareSvc,
|
||||
startedAt: startedAt,
|
||||
nginxPIDFile: nginxPIDFile,
|
||||
cpuSample: sample,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +74,10 @@ type statusResponse struct {
|
||||
MemTotalBytes uint64 `json:"mem_total_bytes"`
|
||||
DiskUsedBytes uint64 `json:"disk_used_bytes"`
|
||||
DiskTotalBytes uint64 `json:"disk_total_bytes"`
|
||||
CloudflareCount int `json:"cloudflare_count"`
|
||||
CloudflareOnline int `json:"cloudflare_online"`
|
||||
CloudflareStatus string `json:"cloudflare_status"`
|
||||
CloudflareMessage string `json:"cloudflare_message,omitempty"`
|
||||
StartedAt string `json:"started_at"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
}
|
||||
@@ -97,6 +104,10 @@ func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
resources, cpuSample := sysinfo.Read(h.currentCPUSample())
|
||||
h.setCPUSample(cpuSample)
|
||||
cfSummary := cloudflared.TunnelSummary{Status: "none", Message: "未创建隧道"}
|
||||
if h.cloudflareSvc != nil {
|
||||
cfSummary = h.cloudflareSvc.Summary(r.Context())
|
||||
}
|
||||
|
||||
accessLog := filepath.Join(h.cfg.LogsDir(), "access.log")
|
||||
total, errors, avgMs := logstore.CountTodayAccess(accessLog)
|
||||
@@ -131,6 +142,10 @@ func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
MemTotalBytes: resources.MemTotalBytes,
|
||||
DiskUsedBytes: resources.DiskUsedBytes,
|
||||
DiskTotalBytes: resources.DiskTotalBytes,
|
||||
CloudflareCount: cfSummary.Count,
|
||||
CloudflareOnline: cfSummary.Online,
|
||||
CloudflareStatus: cfSummary.Status,
|
||||
CloudflareMessage: cfSummary.Message,
|
||||
StartedAt: h.startedAt,
|
||||
UptimeSeconds: int64(uptime),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fonu/fonu/internal/cloudflared"
|
||||
"github.com/fonu/fonu/internal/frp"
|
||||
"github.com/fonu/fonu/internal/notify"
|
||||
"github.com/fonu/fonu/internal/settings"
|
||||
@@ -26,25 +28,37 @@ const (
|
||||
// 刻意只暴露域名与可用率:不带内网地址、端口、证书路径、服务端地址——
|
||||
// 状态页是要挂到公网上的东西,泄露拓扑就等于把内网结构摊开给别人看。
|
||||
type StatusPageHandler struct {
|
||||
settings *settings.Store
|
||||
frp *frp.Service
|
||||
limiter *statusPageLimiter
|
||||
settings *settings.Store
|
||||
frp *frp.Service
|
||||
cloudflare *cloudflared.Service
|
||||
limiter *statusPageLimiter
|
||||
}
|
||||
|
||||
func NewStatusPageHandler(settingsStore *settings.Store, frpSvc *frp.Service) *StatusPageHandler {
|
||||
func NewStatusPageHandler(settingsStore *settings.Store, frpSvc *frp.Service, cloudflareSvc *cloudflared.Service) *StatusPageHandler {
|
||||
return &StatusPageHandler{
|
||||
settings: settingsStore,
|
||||
frp: frpSvc,
|
||||
limiter: newStatusPageLimiter(),
|
||||
settings: settingsStore,
|
||||
frp: frpSvc,
|
||||
cloudflare: cloudflareSvc,
|
||||
limiter: newStatusPageLimiter(),
|
||||
}
|
||||
}
|
||||
|
||||
type statusPageRoute struct {
|
||||
Domain string `json:"domain"`
|
||||
State string `json:"state"`
|
||||
Uptime float64 `json:"uptime_pct"`
|
||||
DownMinutes int `json:"down_minutes"`
|
||||
UnknownMinutes int `json:"unknown_minutes"`
|
||||
Domain string `json:"domain"`
|
||||
State string `json:"state"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Uptime float64 `json:"uptime_pct"`
|
||||
DownMinutes int `json:"down_minutes"`
|
||||
UnknownMinutes int `json:"unknown_minutes"`
|
||||
Incidents []statusPageIncident `json:"incidents"`
|
||||
}
|
||||
|
||||
type statusPageIncident struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Minutes int `json:"minutes"`
|
||||
}
|
||||
|
||||
type statusPagePayload struct {
|
||||
@@ -70,7 +84,7 @@ func (h *StatusPageHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
enabled, _ := h.settings.GetBool(r.Context(), settings.KeyStatusPageEnabled)
|
||||
if !enabled || h.frp == nil {
|
||||
if !enabled || (h.frp == nil && h.cloudflare == nil) {
|
||||
writeError(r, w, http.StatusNotFound, "状态页未开启")
|
||||
return
|
||||
}
|
||||
@@ -84,17 +98,30 @@ func (h *StatusPageHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := h.frp.RouteUptime(r.Context(), 24*time.Hour)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusInternalServerError, "读取状态失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := statusPagePayload{
|
||||
Title: "Fonu 服务状态",
|
||||
CodeRequired: codeRequired,
|
||||
CheckedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Routes: make([]statusPageRoute, 0, len(stats)),
|
||||
Routes: make([]statusPageRoute, 0),
|
||||
}
|
||||
if err := appendFRPStatus(&payload, h.frp, r.Context()); err != nil {
|
||||
writeError(r, w, http.StatusInternalServerError, "读取状态失败", err)
|
||||
return
|
||||
}
|
||||
if err := appendCloudflareStatus(&payload, h.cloudflare, r.Context()); err != nil {
|
||||
writeError(r, w, http.StatusInternalServerError, "读取状态失败", err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func appendFRPStatus(payload *statusPagePayload, svc *frp.Service, ctx context.Context) error {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
stats, err := svc.RouteUptime(ctx, 24*time.Hour)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stat := range stats {
|
||||
switch stat.State {
|
||||
@@ -108,12 +135,64 @@ func (h *StatusPageHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
payload.Routes = append(payload.Routes, statusPageRoute{
|
||||
Domain: stat.Domain,
|
||||
State: stat.State,
|
||||
Source: "frp",
|
||||
Uptime: stat.Uptime,
|
||||
DownMinutes: stat.DownMinutes,
|
||||
UnknownMinutes: stat.UnknownMinutes,
|
||||
Incidents: mapFRPIncidents(stat.Incidents),
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendCloudflareStatus(payload *statusPagePayload, svc *cloudflared.Service, ctx context.Context) error {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
stats, err := svc.HealthUptime(ctx, 24*time.Hour)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stat := range stats {
|
||||
switch stat.State {
|
||||
case cloudflared.HealthUp:
|
||||
payload.Up++
|
||||
case cloudflared.HealthDown:
|
||||
payload.Down++
|
||||
default:
|
||||
payload.Unknown++
|
||||
}
|
||||
payload.Routes = append(payload.Routes, statusPageRoute{
|
||||
Domain: stat.Domain,
|
||||
State: stat.State,
|
||||
Source: "cloudflare",
|
||||
Uptime: stat.Uptime,
|
||||
DownMinutes: stat.DownMinutes,
|
||||
UnknownMinutes: stat.UnknownMinutes,
|
||||
Incidents: mapCloudflareIncidents(stat.Incidents),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapFRPIncidents(items []frp.RouteHealthIncident) []statusPageIncident {
|
||||
out := make([]statusPageIncident, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, statusPageIncident{
|
||||
From: item.From, To: item.To, Reason: item.Reason, Detail: item.Detail, Minutes: item.Minutes,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mapCloudflareIncidents(items []cloudflared.HealthIncident) []statusPageIncident {
|
||||
out := make([]statusPageIncident, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, statusPageIncident{
|
||||
From: item.From, To: item.To, Reason: item.Reason, Detail: item.Detail, Minutes: item.Minutes,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hashStatusPageCode(code string) string {
|
||||
|
||||
Reference in New Issue
Block a user