package main import ( "context" "crypto/ecdh" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "os/exec" "strconv" "strings" "sync" "time" statsc "sdm-panel/stats" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) const ( defaultConfigPath = "/etc/xray/config.json" panelPath = "/etc/xray/panel.json" service = "xray" ) type PanelConfig struct { Bind string `json:"bind"` Login string `json:"login"` Password string `json:"password"` Config string `json:"config"` ServerName string `json:"serverName"` Fingerprint string `json:"fingerprint"` } var panelCfg PanelConfig func loadPanel() PanelConfig { p := PanelConfig{Bind: "127.0.0.1:8080", Config: defaultConfigPath} if b, err := os.ReadFile(panelPath); err == nil { _ = json.Unmarshal(b, &p) } if p.Bind == "" { p.Bind = "127.0.0.1:8080" } if p.Config == "" { p.Config = defaultConfigPath } return p } func savePanel(p PanelConfig) error { b, _ := json.MarshalIndent(p, "", " ") tmp := panelPath + ".tmp" if err := os.WriteFile(tmp, b, 0o600); err != nil { return err } if err := os.Rename(tmp, panelPath); err != nil { return err } _ = os.Chmod(panelPath, 0o600) return nil } // --- sessions --- var ( sessMu sync.Mutex sess = map[string]time.Time{} ) func issueSession() string { b := make([]byte, 16) _, _ = rand.Read(b) tok := hex.EncodeToString(b) sessMu.Lock() sess[tok] = time.Now().Add(24 * time.Hour) sessMu.Unlock() return tok } func validSession(r *http.Request) bool { tok := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if tok == "" { tok = r.URL.Query().Get("token") } if tok == "" { return false } sessMu.Lock() defer sessMu.Unlock() exp, ok := sess[tok] if !ok { return false } if time.Now().After(exp) { delete(sess, tok) return false } return true } func requireSession(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !validSession(r) { writeJSON(w, 401, map[string]any{"error": "unauthorized"}) return } next(w, r) } } func checkPassword(login, pw string) bool { if panelCfg.Login != "" && login != panelCfg.Login { return false } return subtle.ConstantTimeCompare([]byte(pw), []byte(panelCfg.Password)) == 1 } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } func errStr(err error) string { if err == nil { return "" } return err.Error() } func sh(cmd string, args ...string) (string, error) { out, err := exec.Command(cmd, args...).CombinedOutput() return strings.TrimSpace(string(out)), err } func restartXray() (string, error) { return sh("systemctl", "restart", service) } // --- config helpers --- func readConfig() (map[string]any, error) { b, err := os.ReadFile(panelCfg.Config) if err != nil { return nil, err } var c map[string]any if err := json.Unmarshal(b, &c); err != nil { return nil, err } return c, nil } func writeConfig(c map[string]any) error { b, err := json.MarshalIndent(c, "", " ") if err != nil { return err } tmp := panelCfg.Config + ".tmp" if err := os.WriteFile(tmp, b, 0o600); err != nil { return err } return os.Rename(tmp, panelCfg.Config) } func stealthInbounds(c map[string]any) []map[string]any { var out []map[string]any ibs, _ := c["inbounds"].([]any) for _, ib := range ibs { m, ok := ib.(map[string]any) if !ok { continue } ss, _ := m["streamSettings"].(map[string]any) if net, _ := ss["network"].(string); net == "stealth" { out = append(out, m) } } return out } func getStealthSettings(c map[string]any) (map[string]any, bool) { ibs := stealthInbounds(c) if len(ibs) == 0 { return nil, false } ss, _ := ibs[0]["streamSettings"].(map[string]any) st, _ := ss["stealthSettings"].(map[string]any) return st, st != nil } func uuid() string { var b [16]byte _, _ = rand.Read(b[:]) b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } func hexKey(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) } func setClients(ibs []map[string]any, clients []any) { for _, ib := range ibs { s, _ := ib["settings"].(map[string]any) s["clients"] = clients } } // --- handlers --- func hLogin(w http.ResponseWriter, r *http.Request) { var req struct { Login string `json:"login"` Password string `json:"password"` } _ = json.NewDecoder(r.Body).Decode(&req) if !checkPassword(req.Login, req.Password) { writeJSON(w, 401, map[string]any{"error": "bad credentials"}) return } writeJSON(w, 200, map[string]any{"token": issueSession()}) } func hStatus(w http.ResponseWriter, r *http.Request) { active, _ := sh("systemctl", "is-active", service) ts, _ := sh("systemctl", "show", service, "--property=ActiveEnterTimestamp", "--value") uptime, _ := sh("cat", "/proc/uptime") c, err := readConfig() info := map[string]any{ "service": active, "activeAt": strings.TrimSpace(ts), "uptime": strings.TrimSpace(uptime), "configPath": panelCfg.Config, "bind": panelCfg.Bind, } if err == nil { var ports []any for _, ib := range c["inbounds"].([]any) { if m, ok := ib.(map[string]any); ok { ports = append(ports, m["port"]) } } info["ports"] = ports if st, ok := getStealthSettings(c); ok { info["paths"] = st["paths"] if t, ok := st["target"].(string); ok { info["target"] = t } } if ibs := stealthInbounds(c); len(ibs) > 0 { cl := ibs[0]["settings"].(map[string]any)["clients"].([]any) info["clientCount"] = len(cl) } } writeJSON(w, 200, info) } func hClients(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } s := ibs[0]["settings"].(map[string]any) clients, _ := s["clients"].([]any) var list []map[string]any for _, cl := range clients { m, _ := cl.(map[string]any) list = append(list, map[string]any{ "id": m["id"], "name": m["email"], }) } writeJSON(w, 200, map[string]any{"clients": list}) } func hAddClient(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` } _ = json.NewDecoder(r.Body).Decode(&req) c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } id := uuid() email := req.Name if email == "" { email = id } clients := ibs[0]["settings"].(map[string]any)["clients"].([]any) clients = append(clients, map[string]any{"id": id, "flow": "", "email": email, "level": 0}) setClients(ibs, clients) if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{"id": id}) } func hDelClient(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } s := ibs[0]["settings"].(map[string]any) clients, _ := s["clients"].([]any) var nc []any for _, cl := range clients { m, _ := cl.(map[string]any) if m["id"] != id { nc = append(nc, m) } } setClients(ibs, nc) if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{"ok": true}) } func hRotateClient(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } s := ibs[0]["settings"].(map[string]any) clients, _ := s["clients"].([]any) newID := uuid() done := false for i, cl := range clients { m, _ := cl.(map[string]any) if m["id"] == id { m["id"] = newID clients[i] = m done = true } } if !done { writeJSON(w, 404, map[string]any{"error": "client not found"}) return } setClients(ibs, clients) if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{"id": newID}) } func hClientConfig(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } // verify client exists found := false for _, cl := range ibs[0]["settings"].(map[string]any)["clients"].([]any) { if m, ok := cl.(map[string]any); ok && m["id"] == id { found = true break } } if !found { writeJSON(w, 404, map[string]any{"error": "client not found"}) return } st, _ := getStealthSettings(c) if st == nil { st = map[string]any{} } serverName := panelCfg.ServerName if serverName == "" { serverName = "YOUR-DOMAIN" } fp := panelCfg.Fingerprint if fp == "" { fp = "chrome" } pin := "" if ibs := stealthInbounds(c); len(ibs) > 0 { if ss, ok := ibs[0]["streamSettings"].(map[string]any); ok { if sec, _ := ss["security"].(string); sec == "" || sec == "tls" { if ts, ok := ss["tlsSettings"].(map[string]any); ok { pin = selfSignedPin(tlsCertFile(ts)) } } } } cfg := map[string]any{ "log": map[string]any{"loglevel": "warning"}, "inbounds": []any{ map[string]any{"listen": "127.0.0.1", "port": 10808, "protocol": "socks"}, map[string]any{"listen": "127.0.0.1", "port": 10809, "protocol": "http"}, }, "outbounds": []any{ map[string]any{ "protocol": "vless", "settings": map[string]any{ "vnext": []any{map[string]any{ "address": serverName, "port": 443, "users": []any{map[string]any{"id": id, "encryption": "none"}}, }}, }, "streamSettings": map[string]any{ "network": "stealth", "stealthSettings": st, "security": "tls", "tlsSettings": map[string]any{"serverName": serverName, "fingerprint": fp, "pinnedPeerCertSha256": pin}, }, }, map[string]any{"protocol": "freedom", "tag": "direct"}, }, } b, err := json.MarshalIndent(cfg, "", " ") if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") _, _ = w.Write(b) } func hClientLink(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } ib := ibs[0] // find client name := "" found := false for _, cl := range ib["settings"].(map[string]any)["clients"].([]any) { if m, ok := cl.(map[string]any); ok && m["id"] == id { found = true name, _ = m["email"].(string) break } } if !found { writeJSON(w, 404, map[string]any{"error": "client not found"}) return } st, _ := getStealthSettings(c) if st == nil { st = map[string]any{} } serverName := panelCfg.ServerName if serverName == "" { serverName = "YOUR-DOMAIN" } ss := ib["streamSettings"].(map[string]any) security, _ := ss["security"].(string) if security == "" { security = "tls" } q := url.Values{} q.Set("encryption", "none") q.Set("security", security) q.Set("type", "stealth") q.Set("sni", serverName) // fingerprint fp := panelCfg.Fingerprint if fp == "" { fp = "chrome" } if security == "reality" { if rs, ok := ss["realitySettings"].(map[string]any); ok { if v, ok := rs["serverName"].(string); ok && v != "" { q.Set("sni", v) } if v, ok := rs["fingerprint"].(string); ok && v != "" { fp = v } if v, ok := rs["publicKey"].(string); ok { q.Set("pbk", v) } if v, ok := rs["shortId"].(string); ok { q.Set("sid", v) } } } else if ts, ok := ss["tlsSettings"].(map[string]any); ok { if v, ok := ts["fingerprint"].(string); ok && v != "" { fp = v } } q.Set("fp", fp) if security == "tls" || security == "" { if ts, ok := ss["tlsSettings"].(map[string]any); ok { if v := selfSignedPin(tlsCertFile(ts)); v != "" { q.Set("pin", v) } } } // stealth settings if v, ok := st["key"].(string); ok { q.Set("key", v) } if ps, ok := st["paths"].([]any); ok { var arr []string for _, x := range ps { if str, ok := x.(string); ok { arr = append(arr, str) } } if len(arr) > 0 { q.Set("paths", strings.Join(arr, ",")) } } port := "443" if pr, ok := st["ports"].([]any); ok && len(pr) > 0 { if n, ok := pr[0].(float64); ok && n > 0 { port = strconv.Itoa(int(n)) } } link := "vless://" + id + "@" + serverName + ":" + port + "?" + q.Encode() + "#" + url.QueryEscape(name) writeJSON(w, 200, map[string]any{"link": link}) } func findXrayBinary() string { for _, p := range []string{"/usr/local/xray/xray", "/usr/bin/xray"} { if _, err := os.Stat(p); err == nil { return p } } return "" } func hVersion(w http.ResponseWriter, r *http.Request) { ver := "xray not found" if xr := findXrayBinary(); xr != "" { out, _ := sh(xr, "version") for _, line := range strings.Split(out, "\n") { if strings.Contains(line, "Xray") { ver = strings.TrimSpace(line) break } } } certFile := "" if c, err := readConfig(); err == nil { if ibs := stealthInbounds(c); len(ibs) > 0 { ss := ibs[0]["streamSettings"].(map[string]any) if ts, ok := ss["tlsSettings"].(map[string]any); ok { if certs, ok := ts["certificates"].([]any); ok && len(certs) > 0 { if cm, ok := certs[0].(map[string]any); ok { certFile, _ = cm["certificateFile"].(string) } } } } } certExpiry := "" if certFile != "" { out, _ := sh("openssl", "x509", "-enddate", "-noout", "-in", certFile) if i := strings.Index(out, "notAfter="); i >= 0 { certExpiry = strings.TrimSpace(out[i+len("notAfter="):]) } } writeJSON(w, 200, map[string]any{"xray": ver, "certFile": certFile, "certExpiry": certExpiry}) } func hGetLogLevel(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } lv := "warning" if l, ok := c["log"].(map[string]any); ok { if v, ok := l["loglevel"].(string); ok && v != "" { lv = v } } writeJSON(w, 200, map[string]any{"loglevel": lv}) } func hSetLogLevel(w http.ResponseWriter, r *http.Request) { var req struct { Loglevel string `json:"loglevel"` } _ = json.NewDecoder(r.Body).Decode(&req) if req.Loglevel != "debug" && req.Loglevel != "info" && req.Loglevel != "warning" && req.Loglevel != "error" { writeJSON(w, 400, map[string]any{"error": "bad loglevel"}) return } c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } l, _ := c["log"].(map[string]any) if l == nil { l = map[string]any{} c["log"] = l } l["loglevel"] = req.Loglevel if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } out, rerr := restartXray() writeJSON(w, 200, map[string]any{"saved": true, "restart": strings.TrimSpace(out), "restartErr": errStr(rerr)}) } func genX25519() (string, string) { priv, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { return "", "" } pub := priv.PublicKey().Bytes() return base64.RawURLEncoding.EncodeToString(priv.Bytes()), base64.RawURLEncoding.EncodeToString(pub) } func hGetSecurity(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } ibs := stealthInbounds(c) if len(ibs) == 0 { writeJSON(w, 500, map[string]any{"error": "no stealth inbound"}) return } ss := ibs[0]["streamSettings"].(map[string]any) sec, _ := ss["security"].(string) if sec == "" { sec = "tls" } res := map[string]any{"security": sec} if sec == "reality" { if rs, ok := ss["realitySettings"].(map[string]any); ok { res["dest"] = rs["dest"] res["publicKey"] = rs["publicKey"] if sids, ok := rs["shortIds"].([]any); ok && len(sids) > 0 { res["shortId"] = sids[0] } } } writeJSON(w, 200, res) } func hSetSecurity(w http.ResponseWriter, r *http.Request) { var req struct { Security string `json:"security"` Dest string `json:"dest"` ShortId string `json:"shortId"` } _ = json.NewDecoder(r.Body).Decode(&req) if req.Security != "tls" && req.Security != "reality" { writeJSON(w, 400, map[string]any{"error": "bad security"}) return } c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } for _, ib := range stealthInbounds(c) { ss := ib["streamSettings"].(map[string]any) ss["security"] = req.Security if req.Security == "reality" { if req.Dest == "" { req.Dest = "www.apple.com" } if req.ShortId == "" { req.ShortId = hexKey(4) } priv, pub := genX25519() ss["realitySettings"] = map[string]any{ "show": false, "dest": req.Dest + ":443", "xver": 0, "serverNames": []string{req.Dest}, "privateKey": priv, "shortIds": []string{req.ShortId}, "publicKey": pub, } } } secPub := "" for _, ib := range stealthInbounds(c) { ss := ib["streamSettings"].(map[string]any) if req.Security == "reality" { if rs, ok := ss["realitySettings"].(map[string]any); ok { secPub, _ = rs["publicKey"].(string) } } } if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } out, rerr := restartXray() writeJSON(w, 200, map[string]any{"saved": true, "publicKey": secPub, "restart": strings.TrimSpace(out), "restartErr": errStr(rerr)}) } func hSecurityKey(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } priv, pub := genX25519() for _, ib := range stealthInbounds(c) { ss := ib["streamSettings"].(map[string]any) if rs, ok := ss["realitySettings"].(map[string]any); ok { rs["privateKey"] = priv rs["publicKey"] = pub } } if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } out, rerr := restartXray() writeJSON(w, 200, map[string]any{"privateKey": priv, "publicKey": pub, "restart": strings.TrimSpace(out), "restartErr": errStr(rerr)}) } func hRenameClient(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var req struct { Name string `json:"name"` } _ = json.NewDecoder(r.Body).Decode(&req) c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } found := false for _, ib := range stealthInbounds(c) { clients, _ := ib["settings"].(map[string]any)["clients"].([]any) for i, cl := range clients { if m, ok := cl.(map[string]any); ok && m["id"] == id { email := req.Name if email == "" { email = id } m["email"] = email clients[i] = m found = true } } } if !found { writeJSON(w, 404, map[string]any{"error": "client not found"}) return } if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{"saved": true}) } func hBackup(w http.ResponseWriter, r *http.Request) { b, err := os.ReadFile(panelCfg.Config) if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="config.json"`) _, _ = w.Write(b) } func hRestore(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(io.LimitReader(r.Body, 2<<20)) if err != nil { writeJSON(w, 400, map[string]any{"error": err.Error()}) return } var c map[string]any if err := json.Unmarshal(b, &c); err != nil { writeJSON(w, 400, map[string]any{"error": "invalid json"}) return } if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } out, rerr := restartXray() writeJSON(w, 200, map[string]any{"saved": true, "restart": strings.TrimSpace(out), "restartErr": errStr(rerr)}) } func statsInboundPort(c map[string]any) int { api, ok := c["api"].(map[string]any) if !ok { return 0 } ls, _ := api["listen"].(string) if ls == "" { return 0 } if i := strings.LastIndex(ls, ":"); i >= 0 { if n, err := strconv.Atoi(ls[i+1:]); err == nil { return n } } return 0 } func ensureStats(c map[string]any) bool { changed := false if api, ok := c["api"].(map[string]any); ok { if _, ok := api["listen"]; !ok { api["listen"] = "127.0.0.1:10085" changed = true } } else { c["api"] = map[string]any{ "tag": "api", "listen": "127.0.0.1:10085", "services": []string{"HandlerService", "LoggerService", "StatsService"}, } changed = true } if _, ok := c["stats"]; !ok { c["stats"] = map[string]any{} changed = true } pol, _ := c["policy"].(map[string]any) if pol == nil { pol = map[string]any{} c["policy"] = pol } levels, _ := pol["levels"].(map[string]any) if levels == nil { levels = map[string]any{} pol["levels"] = levels } if _, ok := levels["0"]; !ok { levels["0"] = map[string]any{"statsUserUplink": true, "statsUserDownlink": true, "statsUserOnline": true} changed = true } return changed } func hStats(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } if ensureStats(c) { if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } _, _ = restartXray() time.Sleep(2 * time.Second) } port := statsInboundPort(c) if port == 0 { writeJSON(w, 500, map[string]any{"error": "stats api not configured"}) return } conn, err := grpc.NewClient("127.0.0.1:"+strconv.Itoa(port), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } defer conn.Close() ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() cl := statsc.NewStatsServiceClient(conn) traffic := map[string]map[string]int64{} tresp, err := cl.QueryStats(ctx, &statsc.QueryStatsRequest{Pattern: "user>>>"}) if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } for _, st := range tresp.Stat { name := st.Name if !strings.HasPrefix(name, "user>>>") { continue } rest := strings.TrimPrefix(name, "user>>>") idx := strings.Index(rest, ">>>traffic>>>") if idx < 0 { continue } email := rest[:idx] dir := rest[idx+len(">>>traffic>>>"):] if traffic[email] == nil { traffic[email] = map[string]int64{} } traffic[email][dir] = st.Value } online := map[string]bool{} oresp, err := cl.GetAllOnlineUsers(ctx, &statsc.GetAllOnlineUsersRequest{}) if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } for _, u := range oresp.Users { email := u email = strings.TrimPrefix(email, "user>>>") email = strings.TrimSuffix(email, ">>>online") online[email] = true } ipsOf := map[string][]string{} for email := range online { ipr, ierr := cl.GetStatsOnlineIpList(ctx, &statsc.GetStatsRequest{Name: "user>>>" + email + ">>>online"}) if ierr != nil { continue } for ip := range ipr.Ips { ipsOf[email] = append(ipsOf[email], ip) } } emails := map[string]bool{} for e := range traffic { emails[e] = true } for e := range online { emails[e] = true } out := []map[string]any{} for e := range emails { t := traffic[e] ul, dl := int64(0), int64(0) if t != nil { ul = t["uplink"] dl = t["downlink"] } out = append(out, map[string]any{ "email": e, "ips": ipsOf[e], "uplink": ul, "downlink": dl, }) } writeJSON(w, 200, map[string]any{"users": out}) } func tlsCertFile(ts map[string]any) string { if certs, ok := ts["certificates"].([]any); ok && len(certs) > 0 { if cm, ok := certs[0].(map[string]any); ok { f, _ := cm["certificateFile"].(string) return f } } return "" } // selfSignedPin возвращает base64(SHA256(der)) для self-signed сертификата (иначе ""). func selfSignedPin(certFile string) string { if certFile == "" { return "" } out, err := sh("openssl", "x509", "-noout", "-issuer", "-subject", "-in", certFile) if err != nil { return "" } issuer, subject := "", "" for _, line := range strings.Split(out, "\n") { if strings.HasPrefix(line, "issuer=") { issuer = strings.TrimSpace(strings.TrimPrefix(line, "issuer=")) } if strings.HasPrefix(line, "subject=") { subject = strings.TrimSpace(strings.TrimPrefix(line, "subject=")) } } if issuer == "" || issuer != subject { return "" } der, err := sh("openssl", "x509", "-in", certFile, "-outform", "der") if err != nil { return "" } sum := sha256.Sum256([]byte(der)) return hex.EncodeToString(sum[:]) } func hGetStealth(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } st, ok := getStealthSettings(c) if !ok { writeJSON(w, 500, map[string]any{"error": "no stealth settings"}) return } writeJSON(w, 200, st) } func hSetStealth(w http.ResponseWriter, r *http.Request) { var patch map[string]any if err := json.NewDecoder(r.Body).Decode(&patch); err != nil { writeJSON(w, 400, map[string]any{"error": err.Error()}) return } c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } for _, ib := range stealthInbounds(c) { ss := ib["streamSettings"].(map[string]any) st, _ := ss["stealthSettings"].(map[string]any) if st == nil { st = map[string]any{} } for k, v := range patch { st[k] = v } ss["stealthSettings"] = st } if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } out, rerr := restartXray() writeJSON(w, 200, map[string]any{"saved": true, "restart": strings.TrimSpace(out), "restartErr": errStr(rerr)}) } func hRotateStealthKey(w http.ResponseWriter, r *http.Request) { c, err := readConfig() if err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } key := hexKey(8) for _, ib := range stealthInbounds(c) { ss := ib["streamSettings"].(map[string]any) st, _ := ss["stealthSettings"].(map[string]any) st["key"] = key } if err := writeConfig(c); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } out, rerr := restartXray() writeJSON(w, 200, map[string]any{"key": key, "restart": strings.TrimSpace(out), "restartErr": errStr(rerr)}) } func hGetPanel(w http.ResponseWriter, r *http.Request) { fp := panelCfg.Fingerprint if fp == "" { fp = "chrome" } writeJSON(w, 200, map[string]any{"bind": panelCfg.Bind, "login": panelCfg.Login, "config": panelCfg.Config, "serverName": panelCfg.ServerName, "fingerprint": fp, "passwordSet": panelCfg.Password != ""}) } func hSetPanel(w http.ResponseWriter, r *http.Request) { var req struct { Bind string `json:"bind"` Config string `json:"config"` Login string `json:"login"` Password string `json:"password"` ServerName string `json:"serverName"` Fingerprint string `json:"fingerprint"` } _ = json.NewDecoder(r.Body).Decode(&req) if req.Bind != "" { panelCfg.Bind = req.Bind } if req.Config != "" { panelCfg.Config = req.Config } if req.Login != "" { panelCfg.Login = req.Login } if req.Password != "" { panelCfg.Password = req.Password } panelCfg.ServerName = req.ServerName panelCfg.Fingerprint = req.Fingerprint if err := savePanel(panelCfg); err != nil { writeJSON(w, 500, map[string]any{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{"saved": true}) } func hLogs(w http.ResponseWriter, r *http.Request) { n := r.URL.Query().Get("n") if n == "" { n = "150" } if _, err := strconv.Atoi(n); err != nil || n == "" { n = "150" } out, _ := sh("journalctl", "-u", service, "-n", n, "--no-pager") writeJSON(w, 200, map[string]any{"logs": out}) } func hRestart(w http.ResponseWriter, r *http.Request) { out, err := restartXray() writeJSON(w, 200, map[string]any{"restart": strings.TrimSpace(out), "error": errStr(err)}) } func main() { panelCfg = loadPanel() if panelCfg.Login == "" { panelCfg.Login = "admin" } if panelCfg.Password == "" { panelCfg.Password = hexKey(8) _ = savePanel(panelCfg) fmt.Printf("panel login: %s\npanel password: %s\n", panelCfg.Login, panelCfg.Password) } mux := http.NewServeMux() mux.HandleFunc("/api/login", hLogin) mux.HandleFunc("/api/status", requireSession(hStatus)) mux.HandleFunc("/api/clients", requireSession(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: hClients(w, r) case http.MethodPost: hAddClient(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } })) mux.HandleFunc("/api/clients/{id}", requireSession(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodDelete: hDelClient(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } })) mux.HandleFunc("/api/clients/{id}/rotate", requireSession(hRotateClient)) mux.HandleFunc("/api/clients/{id}/config", requireSession(hClientConfig)) mux.HandleFunc("/api/clients/{id}/link", requireSession(hClientLink)) mux.HandleFunc("/api/clients/{id}/rename", requireSession(hRenameClient)) mux.HandleFunc("/api/version", requireSession(hVersion)) mux.HandleFunc("/api/loglevel", requireSession(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: hGetLogLevel(w, r) case http.MethodPost: hSetLogLevel(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } })) mux.HandleFunc("/api/security", requireSession(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: hGetSecurity(w, r) case http.MethodPost: hSetSecurity(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } })) mux.HandleFunc("/api/security/key", requireSession(hSecurityKey)) mux.HandleFunc("/api/backup", requireSession(hBackup)) mux.HandleFunc("/api/stats", requireSession(hStats)) mux.HandleFunc("/api/restore", requireSession(hRestore)) mux.HandleFunc("/api/stealth", requireSession(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: hGetStealth(w, r) case http.MethodPost: hSetStealth(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } })) mux.HandleFunc("/api/stealth/key", requireSession(hRotateStealthKey)) mux.HandleFunc("/api/panel", requireSession(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: hGetPanel(w, r) case http.MethodPost: hSetPanel(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } })) mux.HandleFunc("/api/logs", requireSession(hLogs)) mux.HandleFunc("/api/restart", requireSession(hRestart)) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = io.WriteString(w, ui) return } http.NotFound(w, r) }) fmt.Printf("sdm-panel listening on %s\n", panelCfg.Bind) if err := http.ListenAndServe(panelCfg.Bind, mux); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }