235 lines
5.9 KiB
Go
235 lines
5.9 KiB
Go
package remnawave
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type CheckItem struct {
|
|
Name string
|
|
OK bool
|
|
Skipped bool
|
|
Status int
|
|
Detail string
|
|
}
|
|
|
|
type HealthReport struct {
|
|
PanelURL string
|
|
Checks []CheckItem
|
|
Users int
|
|
Nodes int
|
|
AllOK bool
|
|
Hint string
|
|
}
|
|
|
|
func (c *Client) FullCheck(ctx context.Context, subscriptionURL string) HealthReport {
|
|
report := HealthReport{
|
|
PanelURL: c.panelURL,
|
|
}
|
|
|
|
probes := []struct {
|
|
name string
|
|
path string
|
|
isWeb bool
|
|
}{
|
|
{"Панель (веб)", "/", true},
|
|
{"API (статистика)", "/api/system/stats/recap", false},
|
|
{"API (пользователи)", "/api/users", false},
|
|
{"API (ноды)", "/api/nodes", false},
|
|
{"Подписка (настройки)", "/api/subscription-settings", false},
|
|
{"Подписка (API список)", "/api/subscriptions", false},
|
|
}
|
|
|
|
apiFailures := 0
|
|
webOK := false
|
|
|
|
for _, p := range probes {
|
|
var item CheckItem
|
|
if p.isWeb {
|
|
item = c.probeWeb(ctx, p.name, p.path)
|
|
if item.OK {
|
|
webOK = true
|
|
}
|
|
} else {
|
|
item = c.probeAPI(ctx, p.name, p.path)
|
|
if !item.OK {
|
|
apiFailures++
|
|
}
|
|
}
|
|
report.Checks = append(report.Checks, item)
|
|
|
|
if p.path == "/api/users" && item.OK {
|
|
if n, err := c.countFromEndpoint(ctx, "/api/users", "users"); err == nil {
|
|
report.Users = n
|
|
}
|
|
}
|
|
if p.path == "/api/nodes" && item.OK {
|
|
if n, err := c.countFromEndpoint(ctx, "/api/nodes", "nodes"); err == nil {
|
|
report.Nodes = n
|
|
}
|
|
}
|
|
}
|
|
|
|
subURL := strings.TrimRight(strings.TrimSpace(subscriptionURL), "/")
|
|
if subURL != "" {
|
|
report.Checks = append(report.Checks, c.probePublic(ctx, "Страница подписки", subURL))
|
|
} else {
|
|
report.Checks = append(report.Checks, CheckItem{
|
|
Name: "Страница подписки",
|
|
OK: true,
|
|
Skipped: true,
|
|
Detail: "опционально, не задана",
|
|
})
|
|
}
|
|
|
|
if webOK && apiFailures >= 4 {
|
|
report.Hint = "Веб открывается, но /api/* недоступен (502). " +
|
|
"По документации Remnawave API вызывается на REMNAWAVE_PANEL_URL (https://panel.example.com/api/...), " +
|
|
"а не на домене sub.*. Укажите URL админ-панели и токен REMNAWAVE_API_TOKEN. " +
|
|
"Док: https://docs.rw/docs/install/subscription-page/bundled"
|
|
}
|
|
|
|
report.AllOK = true
|
|
for _, ch := range report.Checks {
|
|
if !ch.OK && !ch.Skipped {
|
|
report.AllOK = false
|
|
break
|
|
}
|
|
}
|
|
return report
|
|
}
|
|
|
|
func (c *Client) probeWeb(ctx context.Context, name, path string) CheckItem {
|
|
item := CheckItem{Name: name}
|
|
resp, body, err := c.getPublic(ctx, path)
|
|
if err != nil {
|
|
item.Detail = err.Error()
|
|
return item
|
|
}
|
|
return finishProbe(&item, resp, body)
|
|
}
|
|
|
|
func (c *Client) probeAPI(ctx context.Context, name, path string) CheckItem {
|
|
item := CheckItem{Name: name}
|
|
resp, body, err := c.get(ctx, path)
|
|
if err != nil {
|
|
item.Detail = err.Error()
|
|
return item
|
|
}
|
|
return finishProbe(&item, resp, body)
|
|
}
|
|
|
|
func finishProbe(item *CheckItem, resp *http.Response, body []byte) CheckItem {
|
|
item.Status = resp.StatusCode
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
item.OK = true
|
|
item.Detail = "OK"
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
item.Detail = "неверный REMNAWAVE_API_TOKEN или CADDY_AUTH_API_TOKEN"
|
|
default:
|
|
item.Detail = statusHint(resp.StatusCode)
|
|
if item.Detail == "" {
|
|
if s := trimBody(body, 80); s != "" {
|
|
item.Detail = s
|
|
} else {
|
|
item.Detail = http.StatusText(resp.StatusCode)
|
|
}
|
|
}
|
|
}
|
|
return *item
|
|
}
|
|
|
|
func statusHint(code int) string {
|
|
switch code {
|
|
case http.StatusBadGateway:
|
|
return "Bad Gateway — прокси не достучался до API панели"
|
|
case http.StatusServiceUnavailable:
|
|
return "Service Unavailable — бэкенд панели не запущен"
|
|
case http.StatusNotFound:
|
|
return "Not Found — путь /api не проксируется на панель"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (c *Client) probePublic(ctx context.Context, name, url string) CheckItem {
|
|
item := CheckItem{Name: name}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
item.Detail = err.Error()
|
|
return item
|
|
}
|
|
req.Header.Set("Accept", "text/html,application/json,*/*")
|
|
if c.caddyToken != "" {
|
|
req.Header.Set("X-Api-Key", c.caddyToken)
|
|
}
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
item.Detail = fmt.Sprintf("нет связи: %v", err)
|
|
return item
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
item.Status = resp.StatusCode
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
|
item.OK = true
|
|
item.Detail = "OK"
|
|
} else {
|
|
item.Detail = statusHint(resp.StatusCode)
|
|
if item.Detail == "" {
|
|
item.Detail = http.StatusText(resp.StatusCode)
|
|
}
|
|
}
|
|
return item
|
|
}
|
|
|
|
// FormatReport — обычный текст (без Markdown), чтобы URL и имена env отображались корректно.
|
|
func FormatReport(r HealthReport, panelName string) string {
|
|
var b strings.Builder
|
|
|
|
if panelName != "" {
|
|
b.WriteString(fmt.Sprintf("Панель: %s\n", panelName))
|
|
}
|
|
b.WriteString(fmt.Sprintf("REMNAWAVE_PANEL_URL: %s\n", r.PanelURL))
|
|
b.WriteString(fmt.Sprintf("API: %s/api/...\n\n", r.PanelURL))
|
|
|
|
for _, ch := range r.Checks {
|
|
mark := "❌"
|
|
switch {
|
|
case ch.Skipped:
|
|
mark = "○"
|
|
case ch.OK:
|
|
mark = "✅"
|
|
}
|
|
line := fmt.Sprintf("%s %s", mark, ch.Name)
|
|
if ch.Status > 0 {
|
|
line += fmt.Sprintf(" — HTTP %d", ch.Status)
|
|
}
|
|
if ch.Detail != "" {
|
|
line += ": " + ch.Detail
|
|
}
|
|
b.WriteString(line + "\n")
|
|
}
|
|
|
|
if r.Users > 0 || r.Nodes > 0 {
|
|
b.WriteString(fmt.Sprintf("\nПользователей: %d\nНод: %d", r.Users, r.Nodes))
|
|
}
|
|
|
|
if r.Hint != "" {
|
|
b.WriteString("\n\n💡 " + r.Hint)
|
|
}
|
|
|
|
if r.AllOK {
|
|
b.WriteString("\n\nВсе обязательные проверки пройдены.")
|
|
} else {
|
|
b.WriteString("\n\nЕсть ошибки — см. подсказку выше.")
|
|
}
|
|
return b.String()
|
|
}
|