package mihomoapi import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" ) type Client struct { base string secret string http *http.Client mu sync.Mutex proxyCache map[string]ProxyState proxyCacheAt time.Time } type ProxyState struct { Type string `json:"type"` Now string `json:"now"` All []string `json:"all"` } func New(base, secret string) *Client { return &Client{ base: stringsTrimRight(base, "/"), secret: secret, http: &http.Client{ Timeout: 10 * time.Second, }, } } func (c *Client) ListProxies() (map[string]ProxyState, error) { return c.ListProxiesCached(0) } func (c *Client) ListProxiesCached(ttl time.Duration) (map[string]ProxyState, error) { c.mu.Lock() if ttl > 0 && c.proxyCache != nil && time.Since(c.proxyCacheAt) < ttl { out := copyProxyStates(c.proxyCache) c.mu.Unlock() return out, nil } c.mu.Unlock() var out struct { Proxies map[string]ProxyState `json:"proxies"` } if err := c.getJSON("/proxies", &out); err != nil { c.mu.Lock() defer c.mu.Unlock() if c.proxyCache != nil { return copyProxyStates(c.proxyCache), nil } return nil, err } if out.Proxies == nil { out.Proxies = map[string]ProxyState{} } c.mu.Lock() c.proxyCache = out.Proxies c.proxyCacheAt = time.Now() c.mu.Unlock() return copyProxyStates(out.Proxies), nil } func (c *Client) Reachable() bool { req, err := http.NewRequest(http.MethodGet, c.base+"/version", nil) if err != nil { return false } if c.secret != "" { req.Header.Set("Authorization", "Bearer "+c.secret) } client := &http.Client{Timeout: 2 * time.Second} resp, err := client.Do(req) if err != nil { return false } defer resp.Body.Close() return resp.StatusCode < 400 } func copyProxyStates(in map[string]ProxyState) map[string]ProxyState { out := make(map[string]ProxyState, len(in)) for k, v := range in { out[k] = v } return out } func (c *Client) SelectProxy(groupName, proxyName string) error { body, _ := json.Marshal(map[string]string{"name": proxyName}) return c.putJSON("/proxies/"+url.PathEscape(groupName), body) } func (c *Client) getJSON(path string, dest any) error { req, err := http.NewRequest(http.MethodGet, c.base+path, nil) if err != nil { return err } if c.secret != "" { req.Header.Set("Authorization", "Bearer "+c.secret) } resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode >= 400 { return fmt.Errorf("mihomo api %d: %s", resp.StatusCode, string(data)) } return json.Unmarshal(data, dest) } func (c *Client) putJSON(path string, body []byte) error { req, err := http.NewRequest(http.MethodPut, c.base+path, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if c.secret != "" { req.Header.Set("Authorization", "Bearer "+c.secret) } resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return fmt.Errorf("mihomo api %d: %s", resp.StatusCode, string(data)) } return nil } func stringsTrimRight(s, cut string) string { for len(s) > 0 && strings.HasSuffix(s, cut) { s = s[:len(s)-len(cut)] } return s } func IsPolicyGroupType(t string) bool { switch t { case "Selector", "URLTest", "Fallback", "LoadBalance", "Relay": return true default: return false } }