Add Go rewrite with Postgres 17 and Dokploy Docker Compose
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user