fc3a66dc83
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI(runs-on: ubuntu-latest) 与开源文档;镜像发布至 git.rc707blog.top Container Registry。 Co-authored-by: Cursor <cursoragent@cursor.com>
171 lines
3.8 KiB
Go
171 lines
3.8 KiB
Go
package health
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/prism/proxy/internal/cache"
|
|
"github.com/prism/proxy/internal/store"
|
|
)
|
|
|
|
type ReloadFunc func(ctx context.Context) error
|
|
|
|
type Checker struct {
|
|
store *store.Store
|
|
cache *cache.Cache
|
|
apiBase string
|
|
secret string
|
|
client *http.Client
|
|
testing atomic.Bool
|
|
}
|
|
|
|
func NewChecker(s *store.Store, c *cache.Cache, apiBase, secret string) *Checker {
|
|
return &Checker{
|
|
store: s,
|
|
cache: c,
|
|
apiBase: stringsTrimRight(apiBase, "/"),
|
|
secret: secret,
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func stringsTrimRight(s, cut string) string {
|
|
for len(s) > 0 && stringsHasSuffix(s, cut) {
|
|
s = s[:len(s)-len(cut)]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func stringsHasSuffix(s, suffix string) bool {
|
|
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
|
}
|
|
|
|
func (h *Checker) TestNode(ctx context.Context, nodeID int64, reload ReloadFunc) (int, error) {
|
|
node, err := h.store.GetProxyNode(ctx, nodeID)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
latency, err := h.testRuntimeName(node.RuntimeName)
|
|
if isNotFound(err) && reload != nil {
|
|
if reloadErr := reload(ctx); reloadErr == nil {
|
|
latency, err = h.testRuntimeName(node.RuntimeName)
|
|
}
|
|
}
|
|
if isNotFound(err) {
|
|
err = fmt.Errorf("proxy %q not loaded in engine; reload engine in settings", node.RuntimeName)
|
|
}
|
|
alive := err == nil && latency >= 0
|
|
lat := latency
|
|
if !alive {
|
|
lat = -1
|
|
}
|
|
_ = h.store.UpdateProxyNodeHealth(ctx, nodeID, lat, alive)
|
|
h.cache.SetProxyLatency(node.RuntimeName, lat)
|
|
return lat, err
|
|
}
|
|
|
|
func (h *Checker) TestAll(ctx context.Context, _ ReloadFunc) error {
|
|
if !h.testing.CompareAndSwap(false, true) {
|
|
return nil
|
|
}
|
|
go func() {
|
|
defer h.testing.Store(false)
|
|
nodes, err := h.store.ListProxyNodes(context.Background(), "", nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, n := range nodes {
|
|
if !n.Enabled {
|
|
continue
|
|
}
|
|
_, _ = h.TestNode(context.Background(), n.ID, nil)
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (h *Checker) ProxyLoaded(name string) bool {
|
|
data, err := h.getJSON("/proxies")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
proxies, ok := data["proxies"].(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, ok = proxies[name]
|
|
return ok
|
|
}
|
|
|
|
func (h *Checker) testRuntimeName(name string) (int, error) {
|
|
testURL := fmt.Sprintf("%s/proxies/%s/delay?timeout=5000&url=%s", h.apiBase, url.PathEscape(name), url.QueryEscape("http://www.gstatic.com/generate_204"))
|
|
req, err := http.NewRequest(http.MethodGet, testURL, nil)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
if h.secret != "" {
|
|
req.Header.Set("Authorization", "Bearer "+h.secret)
|
|
}
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 400 {
|
|
return -1, fmt.Errorf("delay test failed: %s", string(body))
|
|
}
|
|
var result struct {
|
|
Delay int `json:"delay"`
|
|
}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return -1, err
|
|
}
|
|
return result.Delay, nil
|
|
}
|
|
|
|
func (h *Checker) getJSON(path string) (map[string]any, error) {
|
|
req, err := http.NewRequest(http.MethodGet, h.apiBase+path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if h.secret != "" {
|
|
req.Header.Set("Authorization", "Bearer "+h.secret)
|
|
}
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func isNotFound(err error) bool {
|
|
return err != nil && strings.Contains(err.Error(), "Resource not found")
|
|
}
|
|
|
|
func (h *Checker) UpdateAPIBase(apiBase, secret string) {
|
|
h.apiBase = stringsTrimRight(apiBase, "/")
|
|
h.secret = secret
|
|
}
|