Launch
This commit is contained in:
44
README.md
Normal file
44
README.md
Normal file
@@ -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
|
||||
```
|
||||
135
install_bridge.sh
Normal file
135
install_bridge.sh
Normal file
@@ -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" <<EOF
|
||||
{
|
||||
"listen": ":$PORT",
|
||||
"username": "$USER_NAME",
|
||||
"password": "$PASSWORD",
|
||||
"token": "$TOKEN",
|
||||
"data_dir": "$CONFIG_DIR"
|
||||
}
|
||||
EOF
|
||||
chmod 600 "$CONFIG_DIR/config.json"
|
||||
|
||||
cat > "$SERVICE" <<EOF
|
||||
[Unit]
|
||||
Description=DragonCore Generic Bridge API
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$BIN -config $CONFIG_DIR/config.json
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
User=root
|
||||
LimitNOFILE=1048576
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now dragoncore-bridge
|
||||
|
||||
if [ "$OPEN_FIREWALL" = "yes" ] || [ "$OPEN_FIREWALL" = "true" ] || [ "$OPEN_FIREWALL" = "1" ]; then
|
||||
if need_cmd ufw && ufw status 2>/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' '============================================================'
|
||||
960
main.go
Normal file
960
main.go
Normal file
@@ -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 == "<nil>" || 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() }
|
||||
Reference in New Issue
Block a user