From 9ed7c52fcdb3d8e81af16e9a18db4f8852e7db5a Mon Sep 17 00:00:00 2001 From: penguinehis Date: Mon, 25 May 2026 13:48:51 -0300 Subject: [PATCH] Launch --- README.md | 44 +++ go.mod | 3 + install_bridge.sh | 135 +++++++ main.go | 960 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1142 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 install_bridge.sh create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3d0bd5 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# DragonCore Generic Bridge API + +This bridge is for Linux SSH/V2Ray/Xray servers that do **not** run DragonCoreSSH-NewWEB. It exposes a small HTTP API compatible with the DragonCore Panel server connector and keeps itself running through systemd. + +## Install from GitHub + +Replace the repository URL with your own hosted bridge repository: + +```bash +curl -fsSL https://raw.githubusercontent.com/YOUR_USER/dragoncore-bridge/main/install_bridge.sh | bash -s -- \ + --repo https://github.com/YOUR_USER/dragoncore-bridge.git \ + --branch main \ + --port 6969 \ + --open-firewall yes +``` + +The installer prints the panel login data at the end: + +```text +API Type : Bridge Generic +IP : SERVER_IP +API Port : 6969 +User : admin +Password : GENERATED_PASSWORD +Legacy token/Senha header: GENERATED_TOKEN +``` + +## What it does + +- Compiles the Go API. +- Creates `/etc/dragoncore-bridge/config.json`. +- Creates a `dragoncore-bridge` systemd service. +- Opens the selected TCP port in UFW/firewalld/iptables when `--open-firewall yes` is used. +- Supports SSH user create/delete/expiry. +- Supports VLESS client add/remove in `/usr/local/etc/xray/config.json`, `/etc/xray/config.json`, and `/etc/v2ray/config.json`. +- Supports both SSHPlus-style `/root/usuarios.db` + `/etc/SSHPlus/senha` and DragonCore-style `/opt/DragonCore/menu.php` when present. + +## Service commands + +```bash +systemctl status dragoncore-bridge +journalctl -u dragoncore-bridge -f +systemctl restart dragoncore-bridge +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d6518f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module dragoncore-bridge + +go 1.21 diff --git a/install_bridge.sh b/install_bridge.sh new file mode 100644 index 0000000..095b835 --- /dev/null +++ b/install_bridge.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_URL="${REPO_URL:-}" +BRANCH="${BRANCH:-main}" +PORT="${PORT:-6969}" +OPEN_FIREWALL="${OPEN_FIREWALL:-yes}" +INSTALL_DIR="/opt/dragoncore-bridge" +CONFIG_DIR="/etc/dragoncore-bridge" +BIN="/usr/local/bin/dragoncore-bridge" +SERVICE="/etc/systemd/system/dragoncore-bridge.service" + +while [ $# -gt 0 ]; do + case "$1" in + --repo) REPO_URL="$2"; shift 2 ;; + --branch) BRANCH="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --open-firewall) OPEN_FIREWALL="$2"; shift 2 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +if [ "$(id -u)" != "0" ]; then + echo "Run as root." + exit 1 +fi + +need_cmd() { command -v "$1" >/dev/null 2>&1; } +rand_hex() { openssl rand -hex "$1"; } + +export DEBIAN_FRONTEND=noninteractive +if need_cmd apt-get; then + apt-get update -y + apt-get install -y ca-certificates curl wget git openssl build-essential + if ! need_cmd go; then + apt-get install -y golang-go + fi +elif need_cmd yum; then + yum install -y ca-certificates curl wget git openssl gcc make golang +elif need_cmd dnf; then + dnf install -y ca-certificates curl wget git openssl gcc make golang +fi + +mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" +chmod 700 "$CONFIG_DIR" + +if [ -n "$REPO_URL" ]; then + rm -rf "$INSTALL_DIR/src" + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR/src" +else + # Local install: useful if this script is executed from inside the repository. + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + rm -rf "$INSTALL_DIR/src" + mkdir -p "$INSTALL_DIR/src" + cp -a "$SCRIPT_DIR"/. "$INSTALL_DIR/src/" +fi + +cd "$INSTALL_DIR/src" +go build -trimpath -ldflags "-s -w" -o "$BIN" . +chmod 755 "$BIN" + +USER_NAME="admin" +PASSWORD="$(rand_hex 10)" +TOKEN="$(rand_hex 24)" + +if [ -f "$CONFIG_DIR/config.json" ]; then + echo "Existing config found: $CONFIG_DIR/config.json" + USER_NAME="$(grep -o '"username"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/config.json" | head -1 | cut -d '"' -f4 || echo admin)" + PASSWORD="$(grep -o '"password"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/config.json" | head -1 | cut -d '"' -f4 || rand_hex 10)" + TOKEN="$(grep -o '"token"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/config.json" | head -1 | cut -d '"' -f4 || rand_hex 24)" +fi + +cat > "$CONFIG_DIR/config.json" < "$SERVICE" </dev/null | grep -qi "Status: active"; then + ufw allow "$PORT"/tcp || true + fi + if need_cmd firewall-cmd && firewall-cmd --state >/dev/null 2>&1; then + firewall-cmd --permanent --add-port="$PORT/tcp" || true + firewall-cmd --reload || true + fi + if need_cmd iptables; then + iptables -C INPUT -p tcp --dport "$PORT" -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport "$PORT" -j ACCEPT || true + if need_cmd netfilter-persistent; then netfilter-persistent save || true; fi + if need_cmd iptables-save && [ -d /etc/iptables ]; then iptables-save > /etc/iptables/rules.v4 || true; fi + fi +fi + +PUBLIC_IP="$(curl -fsS --max-time 4 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}')" + +echo +printf '%s\n' '============================================================' +printf '%s\n' 'DragonCore Bridge installed.' +printf '%s\n' 'Use these values in the DragonCore Panel:' +printf 'API Type : %s\n' 'Bridge Generic' +printf 'IP : %s\n' "$PUBLIC_IP" +printf 'API Port : %s\n' "$PORT" +printf 'User : %s\n' "$USER_NAME" +printf 'Password : %s\n' "$PASSWORD" +printf 'Legacy token/Senha header: %s\n' "$TOKEN" +printf '%s\n' 'Service commands:' +printf '%s\n' ' systemctl status dragoncore-bridge' +printf '%s\n' ' journalctl -u dragoncore-bridge -f' +printf '%s\n' '============================================================' diff --git a/main.go b/main.go new file mode 100644 index 0000000..4a17f3e --- /dev/null +++ b/main.go @@ -0,0 +1,960 @@ +package main + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + "os/exec" + "os/user" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" +) + +type Config struct { + Listen string `json:"listen"` + Username string `json:"username"` + Password string `json:"password"` + Token string `json:"token"` + DataDir string `json:"data_dir"` +} + +type Account struct { + Username string `json:"username"` + Password string `json:"password,omitempty"` + MaxConnections int `json:"max_connections"` + UUID string `json:"uuid,omitempty"` + WithXray bool `json:"with_xray"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type Store struct { + mu sync.Mutex + path string + Accounts map[string]Account `json:"accounts"` +} + +type App struct { + cfg Config + sessions map[string]time.Time + sessMu sync.Mutex + store *Store +} + +var usernameRE = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,31}$`) + +func main() { + cfgPath := flag.String("config", getenv("DRAGON_BRIDGE_CONFIG", "/etc/dragoncore-bridge/config.json"), "config file") + flag.Parse() + + cfg, err := loadConfig(*cfgPath) + if err != nil { + log.Fatalf("config: %v", err) + } + if cfg.Listen == "" { + cfg.Listen = ":6969" + } + if cfg.DataDir == "" { + cfg.DataDir = "/etc/dragoncore-bridge" + } + if cfg.Username == "" { + cfg.Username = "admin" + } + if cfg.Password == "" || cfg.Token == "" { + log.Fatalf("username/password/token must be configured in %s", *cfgPath) + } + + if err := os.MkdirAll(cfg.DataDir, 0700); err != nil { + log.Fatalf("data dir: %v", err) + } + st, err := loadStore(filepath.Join(cfg.DataDir, "accounts.json")) + if err != nil { + log.Fatalf("store: %v", err) + } + + app := &App{cfg: cfg, sessions: map[string]time.Time{}, store: st} + go app.expiryLoop() + + mux := http.NewServeMux() + mux.HandleFunc("/", app.handleLegacyCommand) + mux.HandleFunc("/api/auth/login", app.handleLogin) + mux.Handle("/api/auth/me", app.auth(http.HandlerFunc(app.handleMe))) + mux.Handle("/api/users", app.auth(http.HandlerFunc(app.handleUsers))) + mux.Handle("/api/users/create", app.auth(http.HandlerFunc(app.handleCreateUser))) + mux.Handle("/api/users/delete", app.auth(http.HandlerFunc(app.handleDeleteUser))) + mux.Handle("/api/dragonpanel/create", app.auth(http.HandlerFunc(app.handleDragonCreate))) + mux.Handle("/api/dragonpanel/delete", app.auth(http.HandlerFunc(app.handleDragonDelete))) + mux.Handle("/api/dragonpanel/sync", app.auth(http.HandlerFunc(app.handleDragonSync))) + mux.Handle("/api/stats", app.auth(http.HandlerFunc(app.handleStats))) + mux.Handle("/api/system/reboot", app.auth(http.HandlerFunc(app.handleReboot))) + mux.Handle("/api/system/restart-ssh", app.auth(http.HandlerFunc(app.handleRestartSSH))) + mux.Handle("/api/system/cleanup", app.auth(http.HandlerFunc(app.handleCleanup))) + mux.Handle("/api/xray/fix", app.auth(http.HandlerFunc(app.handleXrayFix))) + mux.Handle("/api/xray/inbounds", app.auth(http.HandlerFunc(app.handleXrayInbounds))) + mux.Handle("/api/xray/clients/add", app.auth(http.HandlerFunc(app.handleXrayClientAdd))) + mux.Handle("/api/xray/clients/remove", app.auth(http.HandlerFunc(app.handleXrayClientRemove))) + + log.Printf("DragonCore bridge listening on %s", cfg.Listen) + srv := &http.Server{Addr: cfg.Listen, Handler: mux, ReadHeaderTimeout: 10 * time.Second} + log.Fatal(srv.ListenAndServe()) +} + +func getenv(k, def string) string { + if v := os.Getenv(k); v != "" { + return v + } + return def +} + +func loadConfig(path string) (Config, error) { + b, err := os.ReadFile(path) + if err != nil { + return Config{}, err + } + var c Config + if err := json.Unmarshal(b, &c); err != nil { + return Config{}, err + } + return c, nil +} + +func loadStore(path string) (*Store, error) { + st := &Store{path: path, Accounts: map[string]Account{}} + b, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return st, nil + } + if err != nil { + return nil, err + } + if len(bytes.TrimSpace(b)) == 0 { + return st, nil + } + if err := json.Unmarshal(b, st); err != nil { + return nil, err + } + if st.Accounts == nil { + st.Accounts = map[string]Account{} + } + st.path = path + return st, nil +} + +func (s *Store) saveLocked() error { + tmp := s.path + ".tmp" + b, _ := json.MarshalIndent(s, "", " ") + if err := os.WriteFile(tmp, b, 0600); err != nil { + return err + } + return os.Rename(tmp, s.path) +} + +func randToken(n int) string { b := make([]byte, n); _, _ = rand.Read(b); return hex.EncodeToString(b) } + +func writeJSON(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(v) +} +func errText(w http.ResponseWriter, code int, msg string) { http.Error(w, msg, code) } + +func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Username, Password string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + errText(w, 400, "invalid json") + return + } + if req.Username != a.cfg.Username || req.Password != a.cfg.Password { + errText(w, 401, "invalid credentials") + return + } + tok := randToken(24) + a.sessMu.Lock() + a.sessions[tok] = time.Now().Add(12 * time.Hour) + a.sessMu.Unlock() + writeJSON(w, map[string]any{"token": tok, "username": a.cfg.Username, "role": "admin"}) +} + +func (a *App) auth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Senha") == a.cfg.Token || strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") == a.cfg.Token { + next.ServeHTTP(w, r) + return + } + tok := r.Header.Get("X-Session-Token") + a.sessMu.Lock() + exp, ok := a.sessions[tok] + if ok && time.Now().After(exp) { + delete(a.sessions, tok) + ok = false + } + a.sessMu.Unlock() + if !ok { + errText(w, 401, "unauthorized") + return + } + next.ServeHTTP(w, r) + }) +} + +func (a *App) handleMe(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]any{"username": a.cfg.Username, "role": "admin"}) +} + +func (a *App) handleUsers(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + users := listSystemUsers(a.store) + writeJSON(w, users) +} + +func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var p struct { + Username string `json:"username"` + Password *string `json:"password"` + MaxConnections int `json:"max_connections"` + ExpiresAt string `json:"expires_at"` + LimitUpMbps int `json:"limit_mbps_up"` + LimitDownMbps int `json:"limit_mbps_down"` + } + if err := json.NewDecoder(r.Body).Decode(&p); err != nil { + errText(w, 400, "invalid json") + return + } + pass := "" + if p.Password != nil { + pass = *p.Password + } + exp, _ := parseTimeMaybe(p.ExpiresAt) + if err := a.createSSH(p.Username, pass, p.MaxConnections, exp, "", false); err != nil { + errText(w, 400, err.Error()) + return + } + w.WriteHeader(http.StatusCreated) +} + +func (a *App) handleDeleteUser(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + u := r.URL.Query().Get("username") + if u == "" { + errText(w, 400, "username required") + return + } + if err := a.deleteSSH(u, ""); err != nil { + errText(w, 400, err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (a *App) handleDragonCreate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var p struct { + Username string `json:"username"` + Password string `json:"password"` + UUID string `json:"uuid"` + Days int `json:"days"` + Minutes int `json:"minutes"` + MaxConnections int `json:"max_connections"` + WithXray bool `json:"with_xray"` + } + if err := json.NewDecoder(r.Body).Decode(&p); err != nil { + errText(w, 400, "invalid json") + return + } + var exp *time.Time + if p.Minutes > 0 { + t := time.Now().Add(time.Duration(p.Minutes) * time.Minute) + exp = &t + } else if p.Days > 0 { + t := time.Now().AddDate(0, 0, p.Days) + exp = &t + } + if err := a.createSSH(p.Username, p.Password, p.MaxConnections, exp, p.UUID, p.WithXray); err != nil { + errText(w, 400, err.Error()) + return + } + writeJSON(w, map[string]any{"ok": true}) +} + +func (a *App) handleDragonDelete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost && r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var p struct{ Username, UUID string } + if r.Method == http.MethodDelete { + p.Username = r.URL.Query().Get("username") + p.UUID = r.URL.Query().Get("uuid") + } else { + _ = json.NewDecoder(r.Body).Decode(&p) + } + if err := a.deleteSSH(p.Username, p.UUID); err != nil { + errText(w, 400, err.Error()) + return + } + writeJSON(w, map[string]any{"ok": true}) +} + +func (a *App) handleDragonSync(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct { + Accounts []struct { + Username string `json:"username"` + Password string `json:"password"` + ExpiresAt string `json:"expires_at"` + UUID string `json:"uuid"` + MaxConnections int `json:"max_connections"` + } `json:"accounts"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + errText(w, 400, "invalid json") + return + } + ok, fail := 0, []string{} + for _, ac := range req.Accounts { + exp, _ := parseTimeMaybe(ac.ExpiresAt) + if err := a.createSSH(ac.Username, ac.Password, ac.MaxConnections, exp, ac.UUID, ac.UUID != ""); err != nil { + fail = append(fail, ac.Username+":"+err.Error()) + } else { + ok++ + } + } + writeJSON(w, map[string]any{"ok": ok, "fail": fail}) +} + +func (a *App) createSSH(username, password string, limit int, expiresAt *time.Time, uuid string, withXray bool) error { + if !usernameRE.MatchString(username) { + return fmt.Errorf("invalid username") + } + if password == "" { + return fmt.Errorf("password required") + } + _ = a.deleteSSH(username, "") + hash, err := passwordHash(password) + if err != nil { + return err + } + args := []string{"-M", "-s", "/bin/false", "-p", hash} + if expiresAt != nil { + linuxExpiry := *expiresAt + if linuxExpiry.Before(time.Now().Add(24 * time.Hour)) { + linuxExpiry = time.Now().AddDate(0, 0, 2) + } + args = append(args, "-e", linuxExpiry.Format("2006-01-02")) + } + args = append(args, username) + if out, err := exec.Command("useradd", args...).CombinedOutput(); err != nil { + return fmt.Errorf("useradd: %v: %s", err, strings.TrimSpace(string(out))) + } + if err := writeCompatUserFiles(username, password, limit); err != nil { + log.Printf("compat files: %v", err) + } + if withXray && uuid != "" { + if err := addXrayClientAll(uuid, username); err != nil { + return err + } + } + a.store.mu.Lock() + defer a.store.mu.Unlock() + a.store.Accounts[username] = Account{Username: username, Password: password, MaxConnections: limit, UUID: uuid, WithXray: withXray, ExpiresAt: expiresAt, CreatedAt: time.Now()} + return a.store.saveLocked() +} + +func (a *App) deleteSSH(username, uuid string) error { + if username == "" { + return fmt.Errorf("username required") + } + if username == "root" { + return fmt.Errorf("refusing to delete root") + } + if !usernameRE.MatchString(username) { + return fmt.Errorf("invalid username") + } + if uuid == "" { + a.store.mu.Lock() + if ac, ok := a.store.Accounts[username]; ok { + uuid = ac.UUID + } + a.store.mu.Unlock() + } + if uuid != "" { + _ = removeXrayClientAll(uuid) + } + if _, err := user.Lookup(username); err == nil { + dummy, _ := passwordHash("disabled-dragoncore") + _ = exec.Command("usermod", "-p", dummy, username).Run() + _ = exec.Command("pkill", "-u", username).Run() + _ = exec.Command("userdel", "--force", username).Run() + } + _ = removeCompatUserFiles(username) + a.store.mu.Lock() + delete(a.store.Accounts, username) + err := a.store.saveLocked() + a.store.mu.Unlock() + return err +} + +func passwordHash(password string) (string, error) { + out, err := exec.Command("openssl", "passwd", "-1", password).Output() + if err != nil { + return "", fmt.Errorf("openssl passwd failed: %w", err) + } + return strings.TrimSpace(string(out)), nil +} + +func writeCompatUserFiles(username, password string, limit int) error { + _ = os.MkdirAll("/etc/SSHPlus/senha", 0755) + _ = os.WriteFile("/etc/SSHPlus/senha/"+username, []byte(password+"\n"), 0600) + upsertLine("/root/usuarios.db", username, fmt.Sprintf("%s %d", username, limit)) + if _, err := os.Stat("/opt/DragonCore/menu.php"); err == nil { + _ = exec.Command("php", "/opt/DragonCore/menu.php", "deleteData", username).Run() + _ = exec.Command("php", "/opt/DragonCore/menu.php", "insertData", username, password, strconv.Itoa(limit)).Run() + } + return nil +} + +func removeCompatUserFiles(username string) error { + _ = os.Remove("/etc/SSHPlus/senha/" + username) + removeLinePrefix("/root/usuarios.db", username) + if _, err := os.Stat("/opt/DragonCore/menu.php"); err == nil { + _ = exec.Command("php", "/opt/DragonCore/menu.php", "deleteData", username).Run() + } + return nil +} + +func upsertLine(path, prefix, line string) { + removeLinePrefix(path, prefix) + f, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if f != nil { + defer f.Close() + _, _ = f.WriteString(line + "\n") + } +} +func removeLinePrefix(path, prefix string) { + b, err := os.ReadFile(path) + if err != nil { + return + } + var out []string + for _, l := range strings.Split(string(b), "\n") { + if l == "" { + continue + } + if strings.HasPrefix(l, prefix+" ") || l == prefix { + continue + } + out = append(out, l) + } + _ = os.WriteFile(path, []byte(strings.Join(out, "\n")+"\n"), 0644) +} + +func (a *App) expiryLoop() { + for { + time.Sleep(60 * time.Second) + now := time.Now() + var expired []Account + a.store.mu.Lock() + for _, ac := range a.store.Accounts { + if ac.ExpiresAt != nil && now.After(*ac.ExpiresAt) { + expired = append(expired, ac) + } + } + a.store.mu.Unlock() + for _, ac := range expired { + log.Printf("expiring %s", ac.Username) + _ = a.deleteSSH(ac.Username, ac.UUID) + } + } +} + +func listSystemUsers(st *Store) []map[string]any { + st.mu.Lock() + defer st.mu.Unlock() + out := make([]map[string]any, 0, len(st.Accounts)) + for _, ac := range st.Accounts { + out = append(out, map[string]any{"username": ac.Username, "active_conns": activeProcCount(ac.Username), "max_connections": ac.MaxConnections, "expires_at": ac.ExpiresAt, "uuid": ac.UUID}) + } + sort.Slice(out, func(i, j int) bool { return fmt.Sprint(out[i]["username"]) < fmt.Sprint(out[j]["username"]) }) + return out +} +func activeProcCount(username string) int { + out, err := exec.Command("pgrep", "-u", username).Output() + if err != nil { + return 0 + } + n := 0 + for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if l != "" { + n++ + } + } + return n +} + +func parseTimeMaybe(s string) (*time.Time, error) { + s = strings.TrimSpace(s) + if s == "" { + return nil, nil + } + layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"} + for _, l := range layouts { + if t, err := time.Parse(l, s); err == nil { + return &t, nil + } + } + return nil, fmt.Errorf("invalid time") +} + +func xrayConfigPaths() []string { + return []string{"/usr/local/etc/xray/config.json", "/etc/xray/config.json", "/etc/v2ray/config.json"} +} +func readJSONFile(path string) (map[string]any, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var m map[string]any + err = json.Unmarshal(b, &m) + return m, err +} +func writeJSONFile(path string, m map[string]any) error { + b, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err + } + tmp := path + ".tmp" + if err := os.WriteFile(tmp, b, 0644); err != nil { + return err + } + return os.Rename(tmp, path) +} + +func addXrayClientAll(uuid, email string) error { + changed := false + for _, path := range xrayConfigPaths() { + if _, err := os.Stat(path); err != nil { + continue + } + cfg, err := readJSONFile(path) + if err != nil { + return err + } + inb, _ := cfg["inbounds"].([]any) + fileChanged := false + for _, it := range inb { + im, ok := it.(map[string]any) + if !ok || fmt.Sprint(im["protocol"]) != "vless" { + continue + } + settings, _ := im["settings"].(map[string]any) + if settings == nil { + settings = map[string]any{} + im["settings"] = settings + } + clients, _ := settings["clients"].([]any) + exists := false + for _, c := range clients { + cm, _ := c.(map[string]any) + if fmt.Sprint(cm["id"]) == uuid || fmt.Sprint(cm["email"]) == email { + exists = true + } + } + if !exists { + settings["clients"] = append(clients, map[string]any{"id": uuid, "alterId": 0, "email": email}) + fileChanged = true + } + } + if fileChanged { + if err := writeJSONFile(path, cfg); err != nil { + return err + } + changed = true + } + } + if changed { + restartXray() + } + return nil +} + +func removeXrayClientAll(uuid string) error { + changed := false + for _, path := range xrayConfigPaths() { + if _, err := os.Stat(path); err != nil { + continue + } + cfg, err := readJSONFile(path) + if err != nil { + return err + } + inb, _ := cfg["inbounds"].([]any) + fileChanged := false + for _, it := range inb { + im, ok := it.(map[string]any) + if !ok || fmt.Sprint(im["protocol"]) != "vless" { + continue + } + settings, _ := im["settings"].(map[string]any) + if settings == nil { + continue + } + clients, _ := settings["clients"].([]any) + out := make([]any, 0, len(clients)) + for _, c := range clients { + cm, _ := c.(map[string]any) + if fmt.Sprint(cm["id"]) == uuid { + fileChanged = true + continue + } + out = append(out, c) + } + settings["clients"] = out + } + if fileChanged { + if err := writeJSONFile(path, cfg); err != nil { + return err + } + changed = true + } + } + if changed { + restartXray() + } + return nil +} + +func restartXray() { + _ = exec.Command("systemctl", "restart", "xray").Run() + _ = exec.Command("systemctl", "restart", "v2ray").Run() +} + +func (a *App) handleXrayInbounds(w http.ResponseWriter, r *http.Request) { + type Inb struct { + Tag string `json:"tag"` + Protocol string `json:"protocol"` + Clients []map[string]any `json:"clients"` + } + var out []Inb + for _, path := range xrayConfigPaths() { + cfg, err := readJSONFile(path) + if err != nil { + continue + } + inb, _ := cfg["inbounds"].([]any) + for i, it := range inb { + im, _ := it.(map[string]any) + proto := fmt.Sprint(im["protocol"]) + tag := fmt.Sprint(im["tag"]) + if tag == "" || tag == "" { + tag = fmt.Sprintf("%s-%d", proto, i) + } + settings, _ := im["settings"].(map[string]any) + clientsAny, _ := settings["clients"].([]any) + clients := []map[string]any{} + for _, c := range clientsAny { + if cm, ok := c.(map[string]any); ok { + clients = append(clients, cm) + } + } + out = append(out, Inb{Tag: tag, Protocol: proto, Clients: clients}) + } + } + writeJSON(w, out) +} +func (a *App) handleXrayClientAdd(w http.ResponseWriter, r *http.Request) { + var p struct { + UUID string `json:"uuid"` + Email string `json:"email"` + Name string `json:"name"` + } + _ = json.NewDecoder(r.Body).Decode(&p) + if p.UUID == "" { + errText(w, 400, "uuid required") + return + } + email := p.Email + if email == "" { + email = p.Name + } + if email == "" { + email = p.UUID + } + if err := addXrayClientAll(p.UUID, email); err != nil { + errText(w, 400, err.Error()) + return + } + w.WriteHeader(200) +} +func (a *App) handleXrayClientRemove(w http.ResponseWriter, r *http.Request) { + uuid := r.URL.Query().Get("uuid") + if uuid == "" { + errText(w, 400, "uuid required") + return + } + if err := removeXrayClientAll(uuid); err != nil { + errText(w, 400, err.Error()) + return + } + w.WriteHeader(204) +} + +func (a *App) handleStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, collectStats()) } + +type cpuSample struct{ idle, total uint64 } + +func readCPU() cpuSample { + b, _ := os.ReadFile("/proc/stat") + fields := strings.Fields(strings.SplitN(string(b), "\n", 2)[0]) + var vals []uint64 + for _, f := range fields[1:] { + v, _ := strconv.ParseUint(f, 10, 64) + vals = append(vals, v) + } + var total uint64 + for _, v := range vals { + total += v + } + idle := uint64(0) + if len(vals) > 3 { + idle = vals[3] + } + return cpuSample{idle, total} +} +func collectStats() map[string]any { + c1 := readCPU() + time.Sleep(150 * time.Millisecond) + c2 := readCPU() + cpu := 0.0 + if c2.total > c1.total { + cpu = 100 * (1 - float64(c2.idle-c1.idle)/float64(c2.total-c1.total)) + } + mem := readMem() + return map[string]any{"cpu_percent": cpu, "mem_total_bytes": mem["total"], "mem_used_bytes": mem["used"], "mem_avail_bytes": mem["avail"], "mem_percent": mem["percent"], "interfaces": readNet()} +} +func readMem() map[string]any { + b, _ := os.ReadFile("/proc/meminfo") + vals := map[string]uint64{} + for _, l := range strings.Split(string(b), "\n") { + f := strings.Fields(l) + if len(f) >= 2 { + v, _ := strconv.ParseUint(f[1], 10, 64) + vals[strings.TrimSuffix(f[0], ":")] = v * 1024 + } + } + total := vals["MemTotal"] + avail := vals["MemAvailable"] + used := uint64(0) + pct := 0.0 + if total > 0 { + used = total - avail + pct = 100 * float64(used) / float64(total) + } + return map[string]any{"total": total, "used": used, "avail": avail, "percent": pct} +} +func readNet() []map[string]any { + b, _ := os.ReadFile("/proc/net/dev") + var out []map[string]any + for _, l := range strings.Split(string(b), "\n") { + if !strings.Contains(l, ":") { + continue + } + parts := strings.SplitN(l, ":", 2) + name := strings.TrimSpace(parts[0]) + if name == "lo" || name == "" { + continue + } + f := strings.Fields(parts[1]) + if len(f) >= 16 { + rx, _ := strconv.ParseUint(f[0], 10, 64) + tx, _ := strconv.ParseUint(f[8], 10, 64) + out = append(out, map[string]any{"name": name, "rx_bytes": rx, "tx_bytes": tx}) + } + } + return out +} + +func (a *App) handleReboot(w http.ResponseWriter, r *http.Request) { + go exec.Command("shutdown", "-r", "+1", "DragonCore bridge reboot requested").Run() + writeJSON(w, map[string]any{"ok": true}) +} +func (a *App) handleRestartSSH(w http.ResponseWriter, r *http.Request) { + _ = exec.Command("systemctl", "restart", "ssh").Run() + _ = exec.Command("systemctl", "restart", "sshd").Run() + writeJSON(w, map[string]any{"ok": true}) +} +func (a *App) handleCleanup(w http.ResponseWriter, r *http.Request) { + _ = exec.Command("sh", "-c", "apt-get clean 2>/dev/null; journalctl --vacuum-time=3d 2>/dev/null; rm -rf /tmp/* 2>/dev/null").Run() + writeJSON(w, map[string]any{"ok": true}) +} +func (a *App) handleXrayFix(w http.ResponseWriter, r *http.Request) { + restartXray() + writeJSON(w, map[string]any{"ok": true}) +} + +func (a *App) handleLegacyCommand(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + if r.Method != http.MethodPost { + _, _ = w.Write([]byte("DragonCore Bridge API")) + return + } + if r.Header.Get("Senha") != a.cfg.Token { + errText(w, 401, "Nao autorizado") + return + } + _ = r.ParseForm() + cmd := r.FormValue("comando") + msg, err := a.runLegacyDragonCommand(cmd) + if err != nil { + _, _ = w.Write([]byte(err.Error())) + return + } + _, _ = w.Write([]byte(msg)) +} + +func shellFields(s string) []string { + var out []string + var cur strings.Builder + quote := rune(0) + esc := false + for _, r := range s { + if esc { + cur.WriteRune(r) + esc = false + continue + } + if r == '\\' { + esc = true + continue + } + if quote != 0 { + if r == quote { + quote = 0 + } else { + cur.WriteRune(r) + } + continue + } + if r == '\'' || r == '"' { + quote = r + continue + } + if r == ' ' || r == '\t' || r == '\n' { + if cur.Len() > 0 { + out = append(out, cur.String()) + cur.Reset() + } + continue + } + cur.WriteRune(r) + } + if cur.Len() > 0 { + out = append(out, cur.String()) + } + return out +} +func (a *App) runLegacyDragonCommand(cmd string) (string, error) { + f := shellFields(cmd) + if len(f) > 0 && strings.Contains(f[0], "dragonmodule") { + f = f[1:] + } + if len(f) == 0 { + return "", fmt.Errorf("empty command") + } + switch f[0] { + case "createssh": + if len(f) < 5 { + return "", fmt.Errorf("bad createssh") + } + days, _ := strconv.Atoi(f[3]) + lim, _ := strconv.Atoi(f[4]) + t := time.Now().AddDate(0, 0, days) + return "CRIADOCOMSUCESSO", a.createSSH(f[1], f[2], lim, &t, "", false) + case "createsshteste": + if len(f) < 5 { + return "", fmt.Errorf("bad createsshteste") + } + mins, _ := strconv.Atoi(f[3]) + lim, _ := strconv.Atoi(f[4]) + t := time.Now().Add(time.Duration(mins) * time.Minute) + return "CRIADOCOMSUCESSO", a.createSSH(f[1], f[2], lim, &t, "", false) + case "v2rayadd": + if len(f) < 6 { + return "", fmt.Errorf("bad v2rayadd") + } + days, _ := strconv.Atoi(f[4]) + lim, _ := strconv.Atoi(f[5]) + t := time.Now().AddDate(0, 0, days) + return "1\nCRIADOCOMSUCESSO", a.createSSH(f[2], f[3], lim, &t, f[1], true) + case "v2rayaddteste": + if len(f) < 6 { + return "", fmt.Errorf("bad v2rayaddteste") + } + mins, _ := strconv.Atoi(f[4]) + lim, _ := strconv.Atoi(f[5]) + t := time.Now().Add(time.Duration(mins) * time.Minute) + return "1\nCRIADOCOMSUCESSO", a.createSSH(f[2], f[3], lim, &t, f[1], true) + case "removessh": + if len(f) < 2 { + return "", fmt.Errorf("bad removessh") + } + return "90Cbp1PK1ExPingu", a.deleteSSH(f[1], "") + case "v2raydel": + if len(f) < 3 { + return "", fmt.Errorf("bad v2raydel") + } + return "90Cbp1PK1ExPingu", a.deleteSSH(f[2], f[1]) + default: + return "", fmt.Errorf("unsupported command") + } +} + +func localIP() string { + ifaces, _ := net.Interfaces() + for _, i := range ifaces { + addrs, _ := i.Addrs() + for _, a := range addrs { + ipnet, ok := a.(*net.IPNet) + if ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + return "127.0.0.1" +} +func dropPrivilegesNotUsed() { _ = syscall.Getuid() }