package panel import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "regexp" "strings" "sync" "time" ) var tokenRe = regexp.MustCompile(`^awp_[A-Za-z0-9_-]+$`) type Client struct { HTTPBudget time.Duration httpClient *http.Client cacheMu sync.Mutex serverCache map[string]serverCacheEntry } type serverCacheEntry struct { servers []Server until time.Time } type Server struct { ID int `json:"id"` Name string `json:"name"` Host string `json:"host"` } type Response struct { Code int Body []byte Err error } type AddResult struct { ClientID string RawJSON string } func New(budget time.Duration) *Client { return &Client{ HTTPBudget: budget, httpClient: &http.Client{Timeout: 120 * time.Second}, serverCache: map[string]serverCacheEntry{}, } } func NormalizeToken(raw string) string { t := strings.TrimSpace(raw) t = strings.TrimPrefix(t, "\ufeff") t = strings.Map(func(r rune) rune { switch r { case '\u200b', '\u200c', '\u200d', '\ufeff', '\u2060', '\u202a', '\u202b', '\u202c', '\u202d', '\u202e': return -1 default: return r } }, t) t = strings.TrimSpace(t) if strings.HasPrefix(strings.ToLower(t), "bearer ") { t = strings.TrimSpace(t[7:]) } return strings.Trim(t, " \t\n\r\"'") } func TokenOK(t string) bool { return len(t) >= 24 && tokenRe.MatchString(t) } func APIProtocol(protocol string) string { if protocol == "vless" { return "xray" } return protocol } func CreateTimeout(protocol string, budget time.Duration) time.Duration { var d time.Duration switch protocol { case "vless", "xray": d = 100 * time.Second default: d = 75 * time.Second } if budget > 0 && d > budget { d = budget } if d < 5*time.Second { d = 5 * time.Second } return d } func (c *Client) Request(ctx context.Context, method, baseURL, path, token string, jsonBody []byte, timeout time.Duration) Response { if timeout <= 0 { timeout = 60 * time.Second } if c.HTTPBudget > 0 && timeout > c.HTTPBudget { timeout = c.HTTPBudget } url := strings.TrimRight(baseURL, "/") + path reqCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() var body io.Reader if jsonBody != nil { body = bytes.NewReader(jsonBody) } req, err := http.NewRequestWithContext(reqCtx, method, url, body) if err != nil { return Response{Err: err} } req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "AmneziaSiteBridge/1.0 (+Go; Dokploy)") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } if jsonBody != nil { req.Header.Set("Content-Type", "application/json; charset=utf-8") } resp, err := c.httpClient.Do(req) if err != nil { return Response{Err: err} } defer resp.Body.Close() b, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) return Response{Code: resp.StatusCode, Body: b} } func (c *Client) AddConnection(ctx context.Context, baseURL, token string, serverID int, protocol, name string) (AddResult, error) { payload, _ := json.Marshal(map[string]string{ "protocol": APIProtocol(protocol), "name": name, }) timeout := CreateTimeout(protocol, c.HTTPBudget) resp := c.Request(ctx, http.MethodPost, baseURL, fmt.Sprintf("/api/servers/%d/connections/add", serverID), token, payload, timeout) if resp.Err != nil { if strings.Contains(strings.ToLower(resp.Err.Error()), "timeout") || strings.Contains(strings.ToLower(resp.Err.Error()), "deadline") { return AddResult{}, fmt.Errorf("Сервер создаёт ключ слишком долго. Подождите минуту и попробуйте снова.") } return AddResult{}, fmt.Errorf("Не удалось создать подключение.") } if resp.Code < 200 || resp.Code >= 300 { return AddResult{}, fmt.Errorf("Не удалось создать подключение. Выберите другой пункт или тип VPN.") } var dec map[string]any _ = json.Unmarshal(resp.Body, &dec) clientID := "" if v, ok := dec["client_id"].(string); ok { clientID = strings.TrimSpace(v) } if clientID == "" { if v, ok := dec["clientId"].(string); ok { clientID = strings.TrimSpace(v) } } if clientID == "" { return AddResult{}, fmt.Errorf("Сервис не выдал ключ подключения.") } return AddResult{ClientID: clientID, RawJSON: string(resp.Body)}, nil } func (c *Client) RemoveConnection(ctx context.Context, baseURL, token string, serverID int, protocol, clientID string, treat404 bool) error { clientID = strings.TrimSpace(clientID) protocol = strings.TrimSpace(protocol) if clientID == "" || protocol == "" { return fmt.Errorf("неполные параметры удаления") } payload, _ := json.Marshal(map[string]string{ "protocol": APIProtocol(protocol), "client_id": clientID, }) resp := c.Request(ctx, http.MethodPost, baseURL, fmt.Sprintf("/api/servers/%d/connections/remove", serverID), token, payload, 45*time.Second) if resp.Err != nil { return resp.Err } if resp.Code >= 200 && resp.Code < 300 { return nil } if resp.Code == 404 && treat404 { return nil } return fmt.Errorf("HTTP %d", resp.Code) } func (c *Client) RemoveClientSmart(ctx context.Context, baseURL, token string, serverID int, protocol, clientID string) error { protos := uniqueStrings([]string{APIProtocol(protocol), protocol, "vless", "xray", "awg2", "wireguard", "awg", "awg_legacy"}) var last error for _, p := range protos { if err := c.RemoveConnection(ctx, baseURL, token, serverID, p, clientID, true); err == nil { return nil } else { last = err } } if last == nil { last = fmt.Errorf("не удалось удалить") } return last } func (c *Client) ListServers(ctx context.Context, baseURL, token string) ([]Server, error) { key := strings.TrimRight(baseURL, "/") c.cacheMu.Lock() if e, ok := c.serverCache[key]; ok && time.Now().Before(e.until) { out := append([]Server(nil), e.servers...) c.cacheMu.Unlock() return out, nil } c.cacheMu.Unlock() paths := []string{"/api/servers", "/api/servers/", "/api/v1/servers"} var servers []Server for _, p := range paths { resp := c.Request(ctx, http.MethodGet, baseURL, p, token, nil, 20*time.Second) if resp.Err != nil || resp.Code < 200 || resp.Code >= 300 { continue } servers = parseServers(resp.Body) if len(servers) > 0 { break } } c.cacheMu.Lock() c.serverCache[key] = serverCacheEntry{servers: servers, until: time.Now().Add(2 * time.Minute)} c.cacheMu.Unlock() return servers, nil } func (c *Client) ClearCache() { c.cacheMu.Lock() c.serverCache = map[string]serverCacheEntry{} c.cacheMu.Unlock() } func (c *Client) Ping(ctx context.Context, baseURL, token string, serverID int) (alive bool, ms int, errMsg string) { resp := c.Request(ctx, http.MethodGet, baseURL, fmt.Sprintf("/api/servers/%d/ping", serverID), token, nil, 15*time.Second) if resp.Err != nil { return false, 0, resp.Err.Error() } var dec map[string]any _ = json.Unmarshal(resp.Body, &dec) if v, ok := dec["alive"].(bool); ok { alive = v } switch v := dec["ms"].(type) { case float64: ms = int(v) case int: ms = v } if e, ok := dec["error"].(string); ok { errMsg = e } return } func parseServers(body []byte) []Server { var root any if err := json.Unmarshal(body, &root); err != nil { return nil } var list []any switch t := root.(type) { case []any: list = t case map[string]any: for _, k := range []string{"servers", "items", "results", "list"} { if arr, ok := t[k].([]any); ok { list = arr break } } if list == nil { if data, ok := t["data"].(map[string]any); ok { if arr, ok := data["servers"].([]any); ok { list = arr } } } } out := make([]Server, 0, len(list)) for _, item := range list { m, ok := item.(map[string]any) if !ok { continue } id := anyInt(m["id"]) name, _ := m["name"].(string) host, _ := m["host"].(string) if host == "" { host, _ = m["ip"].(string) } out = append(out, Server{ID: id, Name: name, Host: host}) } return out } func anyInt(v any) int { switch t := v.(type) { case float64: return int(t) case int: return t case json.Number: n, _ := t.Int64() return int(n) case string: var n int fmt.Sscanf(t, "%d", &n) return n default: return 0 } } func uniqueStrings(in []string) []string { seen := map[string]struct{}{} var out []string for _, s := range in { s = strings.TrimSpace(s) if s == "" { continue } if _, ok := seen[s]; ok { continue } seen[s] = struct{}{} out = append(out, s) } return out }