Add Go rewrite with Postgres 17 and Dokploy Docker Compose

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-27 08:21:21 +03:00
co-authored by Cursor
parent bac78dd3fc
commit 99a27f00be
67 changed files with 17864 additions and 0 deletions
+341
View File
@@ -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
}