diff --git a/install_bridge.sh b/install_bridge.sh index d62d90e..28983ff 100644 --- a/install_bridge.sh +++ b/install_bridge.sh @@ -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" < 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 {