Fix Onlines
This commit is contained in:
@@ -167,12 +167,20 @@ chmod 755 "$BIN"
|
||||
USER_NAME="admin"
|
||||
PASSWORD="$(rand_hex 10)"
|
||||
TOKEN="$(rand_hex 24)"
|
||||
PANEL_URL=""
|
||||
PANEL_SERVER_ID="0"
|
||||
PANEL_SERVER_IP=""
|
||||
PANEL_PUSH_INTERVAL="60"
|
||||
|
||||
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)"
|
||||
PANEL_URL="$(grep -o '"panel_url"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/config.json" | head -1 | cut -d '"' -f4 || echo '')"
|
||||
PANEL_SERVER_ID="$(grep -o '"panel_server_id"[[:space:]]*:[[:space:]]*[0-9]*' "$CONFIG_DIR/config.json" | head -1 | grep -o '[0-9]*$' || echo 0)"
|
||||
PANEL_SERVER_IP="$(grep -o '"panel_server_ip"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/config.json" | head -1 | cut -d '"' -f4 || echo '')"
|
||||
PANEL_PUSH_INTERVAL="$(grep -o '"panel_push_interval_seconds"[[:space:]]*:[[:space:]]*[0-9]*' "$CONFIG_DIR/config.json" | head -1 | grep -o '[0-9]*$' || echo 60)"
|
||||
EXISTING_PORT="$(extract_listen_port "$CONFIG_DIR/config.json" || true)"
|
||||
if [ "$PORT_SET" = "0" ] && [ -n "$EXISTING_PORT" ]; then
|
||||
PORT="$EXISTING_PORT"
|
||||
@@ -191,7 +199,11 @@ cat > "$CONFIG_DIR/config.json" <<EOF
|
||||
"username": "$USER_NAME",
|
||||
"password": "$PASSWORD",
|
||||
"token": "$TOKEN",
|
||||
"data_dir": "$CONFIG_DIR"
|
||||
"data_dir": "$CONFIG_DIR",
|
||||
"panel_url": "$PANEL_URL",
|
||||
"panel_server_id": $PANEL_SERVER_ID,
|
||||
"panel_server_ip": "$PANEL_SERVER_IP",
|
||||
"panel_push_interval_seconds": $PANEL_PUSH_INTERVAL
|
||||
}
|
||||
EOF
|
||||
chmod 600 "$CONFIG_DIR/config.json"
|
||||
|
||||
273
main.go
273
main.go
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -24,11 +26,15 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Listen string `json:"listen"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
DataDir string `json:"data_dir"`
|
||||
Listen string `json:"listen"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
DataDir string `json:"data_dir"`
|
||||
PanelURL string `json:"panel_url"`
|
||||
PanelServerID int `json:"panel_server_id"`
|
||||
PanelServerIP string `json:"panel_server_ip"`
|
||||
PanelPushIntervalSeconds int `json:"panel_push_interval_seconds"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
@@ -48,11 +54,14 @@ type Store struct {
|
||||
}
|
||||
|
||||
type App struct {
|
||||
cfg Config
|
||||
sessions map[string]time.Time
|
||||
sessMu sync.Mutex
|
||||
store *Store
|
||||
startedAt time.Time
|
||||
cfg Config
|
||||
configPath string
|
||||
sessions map[string]time.Time
|
||||
sessMu sync.Mutex
|
||||
store *Store
|
||||
startedAt time.Time
|
||||
panelMu sync.Mutex
|
||||
lastPush time.Time
|
||||
}
|
||||
|
||||
var usernameRE = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,31}$`)
|
||||
@@ -77,6 +86,9 @@ func main() {
|
||||
if cfg.Password == "" || cfg.Token == "" {
|
||||
log.Fatalf("username/password/token must be configured in %s", *cfgPath)
|
||||
}
|
||||
if cfg.PanelPushIntervalSeconds <= 0 {
|
||||
cfg.PanelPushIntervalSeconds = 60
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cfg.DataDir, 0700); err != nil {
|
||||
log.Fatalf("data dir: %v", err)
|
||||
@@ -86,14 +98,16 @@ func main() {
|
||||
log.Fatalf("store: %v", err)
|
||||
}
|
||||
|
||||
app := &App{cfg: cfg, sessions: map[string]time.Time{}, store: st, startedAt: time.Now()}
|
||||
app := &App{cfg: cfg, configPath: *cfgPath, sessions: map[string]time.Time{}, store: st, startedAt: time.Now()}
|
||||
go app.expiryLoop()
|
||||
go app.onlinePushLoop()
|
||||
|
||||
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/onlines", app.auth(http.HandlerFunc(app.handleOnlines)))
|
||||
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)))
|
||||
@@ -392,6 +406,7 @@ func (a *App) auth(next http.Handler) http.Handler {
|
||||
}
|
||||
for _, tok := range staticTokens {
|
||||
if strings.TrimSpace(tok) != "" && strings.TrimSpace(tok) == a.cfg.Token {
|
||||
a.learnPanelFromRequest(r)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -414,6 +429,7 @@ func (a *App) auth(next http.Handler) http.Handler {
|
||||
errText(w, 401, "unauthorized")
|
||||
return
|
||||
}
|
||||
a.learnPanelFromRequest(r)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -431,6 +447,234 @@ func (a *App) handleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, users)
|
||||
}
|
||||
|
||||
func (a *App) handleOnlines(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
counts := collectOnlineCounts()
|
||||
users := make([]map[string]interface{}, 0, len(counts))
|
||||
total := 0
|
||||
for username, count := range counts {
|
||||
if count <= 0 {
|
||||
continue
|
||||
}
|
||||
users = append(users, map[string]interface{}{"username": username, "usuario": username, "quantidade": count, "active_conns": count})
|
||||
total += count
|
||||
}
|
||||
sort.Slice(users, func(i, j int) bool { return fmt.Sprint(users[i]["username"]) < fmt.Sprint(users[j]["username"]) })
|
||||
writeJSON(w, map[string]interface{}{"ok": true, "total": total, "users": users})
|
||||
}
|
||||
|
||||
func normalizePanelURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(raw), "http://") && !strings.HasPrefix(strings.ToLower(raw), "https://") {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimRight(raw, "/")
|
||||
}
|
||||
|
||||
func (a *App) learnPanelFromRequest(r *http.Request) {
|
||||
panelURL := normalizePanelURL(r.Header.Get("X-Dragon-Panel-Url"))
|
||||
serverIP := strings.TrimSpace(r.Header.Get("X-Dragon-Panel-Server-Ip"))
|
||||
serverID, _ := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-Dragon-Panel-Server-Id")))
|
||||
if panelURL == "" && serverID <= 0 && serverIP == "" {
|
||||
return
|
||||
}
|
||||
a.panelMu.Lock()
|
||||
changed := false
|
||||
if panelURL != "" && panelURL != a.cfg.PanelURL {
|
||||
a.cfg.PanelURL = panelURL
|
||||
changed = true
|
||||
}
|
||||
if serverID > 0 && serverID != a.cfg.PanelServerID {
|
||||
a.cfg.PanelServerID = serverID
|
||||
changed = true
|
||||
}
|
||||
if serverIP != "" && serverIP != a.cfg.PanelServerIP {
|
||||
a.cfg.PanelServerIP = serverIP
|
||||
changed = true
|
||||
}
|
||||
cfgCopy := a.cfg
|
||||
a.panelMu.Unlock()
|
||||
if changed {
|
||||
log.Printf("panel online push target set to %s server_id=%d server_ip=%s", cfgCopy.PanelURL, cfgCopy.PanelServerID, cfgCopy.PanelServerIP)
|
||||
a.saveConfig(cfgCopy)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) saveConfig(cfg Config) {
|
||||
if strings.TrimSpace(a.configPath) == "" {
|
||||
return
|
||||
}
|
||||
b, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := ioutil.WriteFile(a.configPath, append(b, '\n'), 0600); err != nil {
|
||||
log.Printf("saving bridge config failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) onlinePushLoop() {
|
||||
initialDelay := 10 * time.Second
|
||||
time.Sleep(initialDelay)
|
||||
for {
|
||||
interval := a.cfg.PanelPushIntervalSeconds
|
||||
if interval <= 0 {
|
||||
interval = 60
|
||||
}
|
||||
a.pushOnlineSnapshot()
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) pushOnlineSnapshot() {
|
||||
a.panelMu.Lock()
|
||||
panelURL := normalizePanelURL(a.cfg.PanelURL)
|
||||
serverID := a.cfg.PanelServerID
|
||||
serverIP := a.cfg.PanelServerIP
|
||||
a.panelMu.Unlock()
|
||||
if panelURL == "" {
|
||||
return
|
||||
}
|
||||
counts := collectOnlineCounts()
|
||||
users := make([]map[string]interface{}, 0, len(counts))
|
||||
total := 0
|
||||
for username, count := range counts {
|
||||
if count <= 0 {
|
||||
continue
|
||||
}
|
||||
users = append(users, map[string]interface{}{"usuario": username, "username": username, "quantidade": count})
|
||||
total += count
|
||||
}
|
||||
sort.Slice(users, func(i, j int) bool { return fmt.Sprint(users[i]["usuario"]) < fmt.Sprint(users[j]["usuario"]) })
|
||||
body := map[string]interface{}{
|
||||
"server_id": serverID,
|
||||
"server_ip": serverIP,
|
||||
"hostname": hostname(),
|
||||
"total": total,
|
||||
"onlines": users,
|
||||
"sent_at": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
endpoint := panelURL + "/api/bridge_onlines.php"
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
// The panel validates this against the same bridge/module password/token that
|
||||
// was registered in the server form.
|
||||
req.Header.Set("Senha", a.cfg.Password)
|
||||
req.Header.Set("X-API-Token", a.cfg.Token)
|
||||
req.Header.Set("X-Bridge-Password", a.cfg.Password)
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("online push failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
raw, _ := ioutil.ReadAll(resp.Body)
|
||||
log.Printf("online push rejected: http=%d body=%s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
return
|
||||
}
|
||||
a.panelMu.Lock()
|
||||
a.lastPush = time.Now()
|
||||
a.panelMu.Unlock()
|
||||
}
|
||||
|
||||
func hostname() string {
|
||||
h, err := os.Hostname()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func collectOnlineCounts() map[string]int {
|
||||
counts := map[string]int{}
|
||||
commands := []string{
|
||||
`ps -ef | grep -oP "sshd: \K\w+(?= \[priv\])" || true`,
|
||||
`sed '/^10.8.0./d' /etc/openvpn/openvpn-status.log 2>/dev/null | grep 127.0.0.1 | awk -F',' '{print $1}' || true`,
|
||||
`printf 'status\n' | nc -q0 127.0.0.1 7505 2>/dev/null | grep -oP '.*?,\K.*?(?=,)' | sort | uniq | grep -v ':' || true`,
|
||||
`awk -v date="$(date -d '60 seconds ago' +'%Y/%m/%d %H:%M:%S')" '$0 > date && /email:/ { sub(/.*email: /, "", $0); sub(/@gmail\.com$/, "", $0); if (!seen[$0]++) print }' /var/log/v2ray/access.log 2>/dev/null || true`,
|
||||
}
|
||||
for _, cmd := range commands {
|
||||
for _, line := range runShellLines(cmd, 8*time.Second) {
|
||||
user := sanitizeOnlineUser(line)
|
||||
if user == "" {
|
||||
continue
|
||||
}
|
||||
counts[user]++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func runShellLines(command string, timeout time.Duration) []string {
|
||||
var cmd *exec.Cmd
|
||||
if timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, "bash", "-lc", command)
|
||||
} else {
|
||||
cmd = exec.Command("bash", "-lc", command)
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil && len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return splitLines(string(out))
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
parts := strings.Split(s, "\n")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sanitizeOnlineUser(line string) string {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return ""
|
||||
}
|
||||
badFragments := []string{
|
||||
"No such file or directory",
|
||||
"nc: port number invalid",
|
||||
"UNDEF",
|
||||
"unknown",
|
||||
}
|
||||
lower := strings.ToLower(line)
|
||||
if lower == "root" {
|
||||
return ""
|
||||
}
|
||||
for _, bad := range badFragments {
|
||||
if strings.Contains(line, bad) || strings.Contains(lower, strings.ToLower(bad)) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if strings.ContainsAny(line, " \t,;|&<>$`\\\"'") {
|
||||
return ""
|
||||
}
|
||||
if len(line) > 64 {
|
||||
return ""
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
@@ -852,11 +1096,12 @@ func (a *App) expiryLoop() {
|
||||
}
|
||||
|
||||
func listSystemUsers(st *Store) []map[string]interface{} {
|
||||
counts := collectOnlineCounts()
|
||||
st.mu.Lock()
|
||||
defer st.mu.Unlock()
|
||||
out := make([]map[string]interface{}, 0, len(st.Accounts))
|
||||
for _, ac := range st.Accounts {
|
||||
active := activeProcCount(ac.Username)
|
||||
active := counts[ac.Username]
|
||||
out = append(out, map[string]interface{}{
|
||||
"username": ac.Username,
|
||||
"active_conns": active,
|
||||
@@ -864,14 +1109,14 @@ func listSystemUsers(st *Store) []map[string]interface{} {
|
||||
"max_connections": ac.MaxConnections,
|
||||
"expires_at": ac.ExpiresAt,
|
||||
"uuid": ac.UUID,
|
||||
"online_source": "openssh",
|
||||
"online_source": "bridge-local",
|
||||
})
|
||||
}
|
||||
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 {
|
||||
return activeSSHConnectionCount(username)
|
||||
return collectOnlineCounts()[username]
|
||||
}
|
||||
|
||||
func dragonCoreInstalled() bool {
|
||||
|
||||
Reference in New Issue
Block a user