Files

1262 lines
32 KiB
Go

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)
}
}