diff --git a/README.md b/README.md index 3de94723..7f47b8e6 100644 --- a/README.md +++ b/README.md @@ -1,194 +1,211 @@ # SecuBox - Security Suite for OpenWrt -**Version:** 0.17.0 πŸŽ‰ **First Public Release** -**Last Updated:** 2026-01-31 -**Status:** Production Ready -**Modules:** 38 LuCI Applications +**Version:** 0.18.0 +**Last Updated:** 2026-03-04 +**Status:** Production Ready +**Modules:** 86 LuCI Applications [![Build OpenWrt Packages](https://github.com/CyberMind-FR/secubox-openwrt/actions/workflows/build-openwrt-packages.yml/badge.svg)](https://github.com/CyberMind-FR/secubox-openwrt/actions/workflows/build-openwrt-packages.yml) [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/CyberMind-FR/secubox-openwrt?include_prereleases&label=release)](https://github.com/CyberMind-FR/secubox-openwrt/releases) -## πŸŽ‰ First Public Release - -SecuBox v0.17.0 marks the **First Public Release** of the project. All core features are now stable and ready for production deployment. - -### What's Ready - -- βœ… **38 LuCI modules** β€” Complete security and network management suite -- βœ… **Three-Loop Security Architecture** β€” Operational, Tactical, and Strategic defense layers -- βœ… **CrowdSec Integration** β€” Real-time threat intelligence and automated blocking -- βœ… **Deep Packet Inspection** β€” netifyd/nDPId protocol analysis -- βœ… **WireGuard VPN** β€” Encrypted mesh connectivity -- βœ… **Multi-architecture support** β€” ARM64, ARM32, MIPS, x86 - -### Coming Next (v0.18+) - -- πŸ”΅ **P2P Hub** β€” Decentralized threat intelligence sharing -- πŸ”΅ **did:plc Identity** β€” Self-sovereign node identity for trust networks - --- ## Overview -SecuBox is a comprehensive security and network management suite for OpenWrt, providing a unified ecosystem of 38 specialized dashboards and tools. All modules are compiled automatically for multiple OpenWrt architectures via GitHub Actions. +SecuBox is a comprehensive security and network management suite for OpenWrt, providing a unified ecosystem of 86 specialized dashboards and tools. The platform implements a **Four-Layer Architecture** for defense in depth, featuring AI-powered threat analysis, P2P mesh networking, and multi-channel service exposure. -**Website:** [secubox.maegia.tv](https://secubox.maegia.tv) +**Website:** [secubox.maegia.tv](https://secubox.maegia.tv) **Publisher:** [CyberMind.fr](https://cybermind.fr) --- -## Three-Loop Security Architecture - -SecuBox implements a **Three-Loop Security Model** for defense in depth: +## Four-Layer Architecture ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ LOOP 3: STRATEGIC β”‚ -β”‚ (Hours β†’ Days) β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ LOOP 2: TACTICAL β”‚ β”‚ -β”‚ β”‚ (Minutes β†’ Hours) β”‚ β”‚ -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ -β”‚ β”‚ β”‚ LOOP 1: OPERATIONAL β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ (Milliseconds β†’ Seconds) β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ DETECT β†’ DECIDE β†’ BLOCK β”‚ β”‚ β”‚ -β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ -β”‚ β”‚ CORRELATE β†’ ANALYZE β†’ ADAPT β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ AGGREGATE β†’ ANTICIPATE β†’ EVOLVE β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ++============================================================+ +| LAYER 4: MESH NETWORKING | +| MirrorNet / P2P Hub / Services Mirrors | +| +--------------------------------------------------------+ | +| | LAYER 3: AI GATEWAY | | +| | MCP Server / Threat Analyst / DNS Guard | | +| | +----------------------------------------------------+ | | +| | | LAYER 2: TACTICAL | | | +| | | CrowdSec / WAF / Scenarios | | | +| | | +------------------------------------------------+ | | | +| | | | LAYER 1: OPERATIONAL | | | | +| | | | fw4 / DPI / Bouncer / HAProxy | | | | +| | | +------------------------------------------------+ | | | +| | +----------------------------------------------------+ | | +| +--------------------------------------------------------+ | ++============================================================+ ``` -| Loop | Function | SecuBox Modules | -|------|----------|-----------------| -| **Loop 1** | Real-time blocking | nftables/fw4, netifyd DPI, CrowdSec Bouncer | -| **Loop 2** | Pattern correlation | CrowdSec Agent/LAPI, Scenarios, Netdata | -| **Loop 3** | Threat intelligence | CrowdSec CAPI, Blocklists, P2P Hub (v0.18+) | - -See [DOCS/THREE-LOOP-ARCHITECTURE.md](DOCS/THREE-LOOP-ARCHITECTURE.md) for detailed analysis. +| Layer | Function | Time Scale | SecuBox Components | +|-------|----------|------------|-------------------| +| **Layer 1** | Real-time blocking | ms β†’ seconds | nftables/fw4, netifyd DPI, CrowdSec Bouncer | +| **Layer 2** | Pattern correlation | minutes β†’ hours | CrowdSec Agent/LAPI, mitmproxy WAF, Scenarios | +| **Layer 3** | AI analysis | minutes β†’ hours | MCP Server, Threat Analyst, DNS Guard | +| **Layer 4** | Mesh networking | continuous | P2P Hub, MirrorBox, Services Registry | --- -## SecuBox Modules +## Key Features -### SecuBox Core (5 modules) +### Security -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-secubox** | 0.7.1 | Central dashboard/Hub for all SecuBox modules | -| **luci-app-secubox-portal** | 0.7.0 | Unified entry point with tabbed navigation | -| **luci-app-secubox-admin** | 1.0.0 | Admin control center with appstore and monitoring | -| **luci-app-secubox-bonus** | 0.2.0 | Documentation, local repo, and app store | -| **luci-app-system-hub** | 0.5.1 | Central system control with logs and backup | +- **CrowdSec Integration** β€” Real-time threat intelligence, CAPI enrollment, auto-banning +- **mitmproxy WAF** β€” HTTPS inspection with CVE detection, sensitivity-based auto-ban +- **Deep Packet Inspection** β€” netifyd/nDPId protocol analysis +- **MAC Guardian** β€” WiFi MAC spoofing detection with CrowdSec integration +- **DNS Guard** β€” AI-powered DGA, tunneling, and anomaly detection -### Security & Threat Management (9 modules) +### AI Gateway -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-crowdsec-dashboard** | 0.7.0 | Real-time CrowdSec security monitoring | -| **luci-app-secubox-security-threats** | 1.0.0 | Unified netifyd DPI + CrowdSec intelligence | -| **luci-app-client-guardian** | 0.4.0 | Network access, captive portal, parental controls | -| **luci-app-auth-guardian** | 0.4.0 | OAuth2/OIDC authentication, voucher system | -| **luci-app-exposure** | 1.0.0 | Service exposure manager | -| **luci-app-tor-shield** | 1.0.0 | Tor anonymization dashboard | -| **luci-app-mitmproxy** | 0.4.0 | HTTPS traffic inspection | -| **luci-app-cyberfeed** | 0.1.1 | Cyberpunk RSS feed aggregator | -| **luci-app-ksm-manager** | 0.4.0 | Cryptographic key/HSM management | +- **MCP Server** β€” Model Context Protocol for Claude Desktop integration +- **Threat Analyst** β€” Autonomous AI agent for threat analysis and rule generation +- **LocalAI** β€” Self-hosted LLM with model management -### Deep Packet Inspection (2 modules) +### Mesh Networking -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-ndpid** | 1.1.2 | nDPId deep packet inspection dashboard | -| **luci-app-secubox-netifyd** | 1.2.1 | netifyd DPI with real-time flow monitoring | +- **P2P Hub** β€” Decentralized peer discovery with globe visualization +- **MirrorBox** β€” Distributed service catalog with auto-sync +- **App Store** β€” P2P package distribution across mesh peers +- **Master Link** β€” Secure mesh onboarding with dynamic IPK generation -### Network & Connectivity (8 modules) +### Service Exposure -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-vhost-manager** | 0.5.0 | Nginx reverse proxy with Let's Encrypt SSL | -| **luci-app-haproxy** | 1.0.0 | Load balancer with vhosts and SSL | -| **luci-app-wireguard-dashboard** | 0.7.0 | WireGuard VPN monitoring | -| **luci-app-network-modes** | 0.5.0 | Sniffer, AP, Relay, Router modes | -| **luci-app-network-tweaks** | 1.0.0 | Auto Proxy DNS & Hosts from vhosts | -| **luci-app-mqtt-bridge** | 0.4.0 | USB-to-MQTT IoT hub | -| **luci-app-cdn-cache** | 0.5.0 | Content delivery optimization | -| **luci-app-media-flow** | 0.6.4 | Streaming detection (Netflix, YouTube, Spotify) | +- **Punk Exposure** β€” Multi-channel service emancipation (Tor + DNS/SSL + Mesh) +- **HAProxy** β€” Load balancer with webroot ACME, auto-SSL +- **Tor Shield** β€” .onion hidden services with split-routing -### Bandwidth & Traffic Management (2 modules) +### Media & Content -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-bandwidth-manager** | 0.5.0 | QoS rules, client quotas, SQM integration | -| **luci-app-traffic-shaper** | 0.4.0 | TC/CAKE traffic shaping | +- **Jellyfin** β€” LXC media server with setup wizard +- **Lyrion** β€” Music server with CIFS integration +- **Zigbee2MQTT** β€” LXC Alpine container for IoT +- **Domoticz** β€” Home automation with MQTT bridge -### Content & Web Platforms (5 modules) +--- -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-gitea** | 1.0.0 | Gitea Platform management | -| **luci-app-hexojs** | 1.0.0 | Hexo static site generator | -| **luci-app-metabolizer** | 1.0.0 | Metabolizer CMS support | -| **luci-app-magicmirror2** | 0.4.0 | MagicMirror2 smart display | -| **luci-app-mmpm** | 0.2.0 | MagicMirror Package Manager | +## SecuBox Modules (86 Total) -### AI/LLM & Analytics (4 modules) +### Core (6 modules) -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-localai** | 0.1.0 | LocalAI LLM management | -| **luci-app-ollama** | 0.1.0 | Ollama LLM management | -| **luci-app-glances** | 1.0.0 | Glances system monitoring | -| **luci-app-netdata-dashboard** | 0.5.0 | Real-time Netdata monitoring | +| Module | Description | +|--------|-------------| +| luci-app-secubox | Central dashboard/Hub | +| luci-app-secubox-portal | Unified entry point with tabs | +| luci-app-secubox-admin | Admin control center | +| secubox-app-bonus | App store and documentation | +| luci-app-system-hub | System control with backup | +| luci-theme-secubox | KISS UI theme | -### Streaming & Data Processing (2 modules) +### Security (15 modules) -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-streamlit** | 1.0.0 | Streamlit Platform management | -| **luci-app-picobrew** | 1.0.0 | PicoBrew Server management | +| Module | Description | +|--------|-------------| +| luci-app-crowdsec-dashboard | CrowdSec monitoring | +| luci-app-security-threats | Unified netifyd + CrowdSec | +| luci-app-client-guardian | Captive portal, parental controls | +| luci-app-auth-guardian | OAuth2/OIDC, vouchers | +| luci-app-exposure | Service exposure manager | +| luci-app-tor-shield | Tor anonymization | +| luci-app-mitmproxy | HTTPS inspection WAF | +| luci-app-mac-guardian | WiFi MAC security | +| luci-app-dns-guard | AI-powered DNS anomaly | +| luci-app-waf | Web Application Firewall | +| luci-app-threat-analyst | AI threat analysis | +| luci-app-ksm-manager | Key/HSM management | +| luci-app-master-link | Mesh onboarding | +| luci-app-routes-status | VHosts route checker | +| secubox-mcp-server | MCP protocol server | -### IoT & Smart Devices (1 module) +### Network (12 modules) -| Module | Version | Description | -|--------|---------|-------------| -| **luci-app-zigbee2mqtt** | 1.0.0 | Zigbee2MQTT docker management | +| Module | Description | +|--------|-------------| +| luci-app-haproxy | Load balancer with SSL | +| luci-app-wireguard-dashboard | WireGuard VPN | +| luci-app-vhost-manager | Nginx reverse proxy | +| luci-app-network-modes | Sniffer/AP/Relay/Router | +| luci-app-network-tweaks | DNS & proxy controls | +| luci-app-dns-provider | DNS provider API | +| luci-app-cdn-cache | CDN optimization | +| luci-app-bandwidth-manager | QoS and quotas | +| luci-app-traffic-shaper | TC/CAKE shaping | +| luci-app-mqtt-bridge | USB-to-MQTT IoT | +| luci-app-media-flow | Streaming detection | +| luci-app-netdiag | Network diagnostics | + +### DPI (2 modules) + +| Module | Description | +|--------|-------------| +| luci-app-ndpid | nDPId deep packet inspection | +| luci-app-netifyd | netifyd flow monitoring | + +### P2P Mesh (4 modules) + +| Module | Description | +|--------|-------------| +| luci-app-p2p | P2P Hub with MirrorBox | +| luci-app-service-registry | Service catalog | +| luci-app-device-intel | Device intelligence | +| secubox-content-pkg | Content distribution | + +### AI/LLM (4 modules) + +| Module | Description | +|--------|-------------| +| luci-app-localai | LocalAI v3.9.0 | +| luci-app-ollama | Ollama LLM | +| luci-app-glances | System monitoring | +| luci-app-netdata-dashboard | Netdata real-time | + +### Media (7 modules) + +| Module | Description | +|--------|-------------| +| luci-app-jellyfin | Media server (LXC) | +| luci-app-lyrion | Music server | +| luci-app-zigbee2mqtt | Zigbee gateway (LXC) | +| luci-app-domoticz | Home automation (LXC) | +| luci-app-ksmbd | SMB/CIFS shares | +| luci-app-smbfs | Remote mount manager | +| luci-app-magicmirror2 | Smart display | + +### Content Platforms (6 modules) + +| Module | Description | +|--------|-------------| +| luci-app-gitea | Git platform | +| luci-app-hexojs | Static site generator | +| luci-app-metablogizer | Metabolizer CMS | +| luci-app-streamlit | Streamlit apps | +| luci-app-picobrew | PicoBrew server | +| luci-app-jitsi | Video conferencing | + +### Remote Access (3 modules) + +| Module | Description | +|--------|-------------| +| luci-app-rustdesk | RustDesk relay | +| luci-app-guacamole | Clientless desktop | +| luci-app-simplex | SimpleX Chat | + +### *Plus 27 additional supporting packages...* --- ## Supported Architectures -### ARM 64-bit (AArch64) - -| Target | Devices | -|--------|---------| -| `aarch64-cortex-a53` | ESPRESSObin, BananaPi R64 | -| `aarch64-cortex-a72` | MOCHAbin, Raspberry Pi 4, NanoPi R4S | -| `mediatek-filogic` | GL.iNet MT3000, BananaPi R3 | -| `rockchip-armv8` | NanoPi R4S/R5S, FriendlyARM | -| `bcm27xx-bcm2711` | Raspberry Pi 4, Compute Module 4 | - -### ARM 32-bit - -| Target | Devices | -|--------|---------| -| `arm-cortex-a7-neon` | Orange Pi, BananaPi, Allwinner | -| `arm-cortex-a9-neon` | Linksys WRT, Turris Omnia | -| `qualcomm-ipq40xx` | Google WiFi, Zyxel NBG6617 | - -### MIPS - -| Target | Devices | -|--------|---------| -| `mips-24kc` | TP-Link Archer, Ubiquiti | -| `mipsel-24kc` | Xiaomi, GL.iNet, Netgear | - -### x86 - -| Target | Devices | -|--------|---------| -| `x86-64` | PC, VMs, Docker, Proxmox | +| Architecture | Targets | Example Devices | +|--------------|---------|-----------------| +| **ARM64** | aarch64-cortex-a53/a72, mediatek-filogic, rockchip-armv8 | MOCHAbin, NanoPi R4S/R5S, GL.iNet MT3000, Raspberry Pi 4 | +| **ARM32** | arm-cortex-a7/a9-neon, qualcomm-ipq40xx | Turris Omnia, Google WiFi | +| **MIPS** | mips-24kc, mipsel-24kc | TP-Link Archer, Xiaomi | +| **x86** | x86-64 | PC, VMs, Docker, Proxmox | --- @@ -196,12 +213,9 @@ See [DOCS/THREE-LOOP-ARCHITECTURE.md](DOCS/THREE-LOOP-ARCHITECTURE.md) for detai ### From Pre-built Packages -Download from [GitHub Releases](https://github.com/CyberMind-FR/secubox-openwrt/releases): - ```bash opkg update opkg install luci-app-secubox-portal_*.ipk -opkg install luci-app-system-hub_*.ipk opkg install luci-app-crowdsec-dashboard_*.ipk ``` @@ -213,73 +227,53 @@ cd ~/openwrt-sdk/package/ git clone https://github.com/CyberMind-FR/secubox-openwrt.git secubox # Build -cd ~/openwrt-sdk/ make package/secubox/luci-app-secubox-portal/compile V=s ``` -### Add as OpenWrt Feed - -Add to `feeds.conf.default`: +### Add as Feed ``` src-git secubox https://github.com/CyberMind-FR/secubox-openwrt.git ``` -Then: - -```bash -./scripts/feeds update secubox -./scripts/feeds install -a -p secubox -make menuconfig # Select modules under LuCI > Applications -make V=s -``` - --- -## Repository Structure +## MCP Integration (Claude Desktop) -``` -secubox-openwrt/ -β”œβ”€β”€ package/secubox/ # All 38 SecuBox LuCI packages -β”œβ”€β”€ secubox-tools/ # Build tools and local SDK -β”œβ”€β”€ DOCS/ # Documentation -β”‚ β”œβ”€β”€ THREE-LOOP-ARCHITECTURE.md # Security model analysis -β”‚ β”œβ”€β”€ DEVELOPMENT-GUIDELINES.md -β”‚ β”œβ”€β”€ QUICK-START.md -β”‚ └── VALIDATION-GUIDE.md -└── .github/workflows/ # CI/CD +SecuBox includes an MCP server for AI integration: + +```json +{ + "mcpServers": { + "secubox": { + "command": "ssh", + "args": ["root@192.168.255.1", "/usr/bin/secubox-mcp"] + } + } +} ``` ---- - -## OpenWrt Compatibility - -| Version | Status | Package Format | -|---------|--------|----------------| -| 25.x | Testing | `.apk` | -| 24.10.x | **Recommended** | `.ipk` | -| 23.05.x | Supported | `.ipk` | +**Available tools:** `crowdsec.alerts`, `crowdsec.decisions`, `waf.logs`, `dns.queries`, `network.flows`, `system.metrics`, `wireguard.status`, `ai.analyze_threats`, `ai.cve_lookup`, `ai.suggest_waf_rules` --- ## Roadmap -| Phase | Version | Status | Focus | -|-------|---------|--------|-------| -| **Core Mesh** | v0.17 | βœ… Released | Loops 1+2 complete | -| **Service Mesh** | v0.18 | πŸ”΅ In Progress | P2P Hub foundation | -| **Intelligence Mesh** | v0.19 | βšͺ Planned | Full P2P intelligence | -| **AI Mesh** | v0.20 | βšͺ Planned | ML in Loop 2 | -| **Certification** | v1.0 | βšͺ Planned | ANSSI certification | +| Version | Status | Focus | +|---------|--------|-------| +| **v0.17** | Released | Core Mesh, 38 modules | +| **v0.18** | Current | P2P Hub, AI Gateway, 86 modules | +| **v0.19** | Planned | Full P2P intelligence | +| **v1.0** | Planned | ANSSI certification | --- ## Links -* **Website**: [secubox.maegia.tv](https://secubox.maegia.tv) -* **GitHub**: [github.com/CyberMind-FR/secubox-openwrt](https://github.com/CyberMind-FR/secubox-openwrt) -* **Publisher**: [CyberMind.fr](https://cybermind.fr) -* **Issues**: [GitHub Issues](https://github.com/CyberMind-FR/secubox-openwrt/issues) +- **Website**: [secubox.maegia.tv](https://secubox.maegia.tv) +- **GitHub**: [github.com/CyberMind-FR/secubox-openwrt](https://github.com/CyberMind-FR/secubox-openwrt) +- **Publisher**: [CyberMind.fr](https://cybermind.fr) +- **Issues**: [GitHub Issues](https://github.com/CyberMind-FR/secubox-openwrt/issues) --- @@ -289,20 +283,10 @@ Apache-2.0 Β© 2024-2026 CyberMind.fr --- -## Contributing - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - ---- - ## Author **Gandalf** - [CyberMind.fr](https://cybermind.fr) **Ex Tenebris, Lux Securitas** -πŸ‡«πŸ‡· Made with love in France +Made in France diff --git a/package/secubox/luci-app-routes-status/htdocs/luci-static/resources/view/routes-status/overview.js b/package/secubox/luci-app-routes-status/htdocs/luci-static/resources/view/routes-status/overview.js index 96577925..02c6bfee 100644 --- a/package/secubox/luci-app-routes-status/htdocs/luci-static/resources/view/routes-status/overview.js +++ b/package/secubox/luci-app-routes-status/htdocs/luci-static/resources/view/routes-status/overview.js @@ -8,6 +8,7 @@ var callStatus = rpc.declare({ object: 'luci.routes-status', method: 'status', + params: ['offset', 'limit'], expect: { } }); @@ -25,46 +26,36 @@ var callAddRoute = rpc.declare({ }); return view.extend({ + allVhosts: [], + currentOffset: 0, + pageSize: 50, + totalVhosts: 0, + statusData: null, + load: function() { - return callStatus(); + return callStatus(0, 50); }, - renderHeaderChip: function(icon, label, value, tone) { - var display = (value == null ? 'β€”' : value).toString(); - return E('div', { 'class': 'sh-header-chip' + (tone ? ' ' + tone : '') }, [ - E('span', { 'class': 'sh-chip-icon' }, icon), - E('div', { 'class': 'sh-chip-text' }, [ - E('span', { 'class': 'sh-chip-label' }, label), - E('strong', {}, display) - ]) - ]); - }, - - renderPill: function(text, type) { - var colors = { - success: '#4CAF50', - warning: '#ff9800', - danger: '#f44336', - info: '#2196F3', - muted: '#9e9e9e' - }; + // Emoji-based status pill + pill: function(emoji, label, ok) { return E('span', { - 'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:4px;color:#fff;background:' + (colors[type] || colors.muted) + ';font-size:0.8em;font-weight:500;' - }, text); + 'class': 'vhost-pill' + (ok ? ' ok' : ' warn'), + 'title': label + }, emoji); }, handleSync: function() { - ui.showModal(_('Syncing Routes...'), [ - E('p', { 'class': 'spinning' }, _('Please wait...')) + ui.showModal(_('Syncing...'), [ + E('p', { 'class': 'spinning' }, _('Syncing routes from HAProxy...')) ]); callSyncRoutes().then(function(res) { ui.hideModal(); if (res && res.success) { - ui.addNotification(null, E('p', {}, _('Routes synchronized successfully')), 'success'); + ui.addNotification(null, E('p', {}, 'βœ… ' + _('Routes synchronized')), 'success'); location.reload(); } else { - ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error'); + ui.addNotification(null, E('p', {}, '❌ ' + (res.error || 'Sync failed')), 'error'); } }); }, @@ -74,11 +65,11 @@ return view.extend({ ui.showModal(_('Add Route'), [ E('div', { 'class': 'cbi-section' }, [ - E('p', {}, _('Add mitmproxy route for: %s').format(domain)), + E('p', {}, _('Add mitmproxy route for: ') + domain), E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Backend Port')), + E('label', { 'class': 'cbi-value-title' }, _('Port')), E('div', { 'class': 'cbi-value-field' }, [ - E('input', { 'type': 'number', 'id': 'route-port', 'value': port || '443', 'style': 'width:100px;' }) + E('input', { 'type': 'number', 'id': 'route-port', 'value': port || '443', 'style': 'width:80px;' }) ]) ]) ]), @@ -93,61 +84,102 @@ return view.extend({ self.doAddRoute(domain, p); } }, - 'style': 'margin-left:10px;' - }, _('Add Route')) + 'style': 'margin-left:8px;' + }, _('Add')) ]) ]); }, doAddRoute: function(domain, port) { - ui.showModal(_('Adding Route...'), [ - E('p', { 'class': 'spinning' }, _('Please wait...')) + ui.showModal(_('Adding...'), [ + E('p', { 'class': 'spinning' }, _('Adding route...')) ]); callAddRoute(domain, port).then(function(res) { ui.hideModal(); if (res && res.success) { - ui.addNotification(null, E('p', {}, _('Route added successfully')), 'success'); + ui.addNotification(null, E('p', {}, 'βœ… ' + _('Route added')), 'success'); location.reload(); } else { - ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error'); + ui.addNotification(null, E('p', {}, '❌ ' + (res.error || 'Failed')), 'error'); } }); }, - renderVhostRow: function(vhost) { + handleLoadMore: function() { var self = this; - var missingRoutes = !vhost.has_route_out || !vhost.has_route_in; + var btn = document.getElementById('load-more-btn'); + var spinner = document.getElementById('load-spinner'); - return E('tr', {}, [ + if (btn) btn.style.display = 'none'; + if (spinner) spinner.style.display = 'block'; + + this.currentOffset += this.pageSize; + + callStatus(this.currentOffset, this.pageSize).then(function(data) { + if (spinner) spinner.style.display = 'none'; + + var newVhosts = data.vhosts || []; + if (newVhosts.length > 0) { + self.allVhosts = self.allVhosts.concat(newVhosts); + + var tbody = document.getElementById('vhosts-tbody'); + if (tbody) { + newVhosts.forEach(function(v) { + tbody.appendChild(self.renderRow(v)); + }); + } + + var counter = document.getElementById('vhosts-count'); + if (counter) { + counter.textContent = self.allVhosts.length + '/' + self.totalVhosts; + } + } + + if (self.allVhosts.length < self.totalVhosts && btn) { + btn.style.display = 'inline-block'; + } + }); + }, + + renderRow: function(v) { + var self = this; + var needsRoute = !v.has_route_out || !v.has_route_in; + + return E('tr', { 'class': 'vhost-row' }, [ + // Domain E('td', {}, [ E('a', { - 'href': 'https://' + vhost.domain, + 'href': 'https://' + v.domain, 'target': '_blank', - 'style': 'color:#1976D2;text-decoration:none;font-weight:500;' - }, vhost.domain) + 'class': 'vhost-domain' + }, v.domain) ]), - E('td', {}, vhost.backend || '-'), - E('td', {}, [ - vhost.has_route_out ? this.renderPill('OUT', 'success') : this.renderPill('OUT', 'warning'), - vhost.has_route_in ? this.renderPill('IN', 'success') : this.renderPill('IN', 'warning') + // Status indicators (emoji-based) + E('td', { 'class': 'vhost-status' }, [ + // Routes + this.pill(v.has_route_out && v.has_route_in ? 'πŸ”—' : '⚠️', + 'Routes: ' + (v.has_route_out ? 'OUTβœ“' : 'OUTβœ—') + ' ' + (v.has_route_in ? 'INβœ“' : 'INβœ—'), + v.has_route_out && v.has_route_in), + // SSL + this.pill(v.ssl_status === 'valid' ? 'πŸ”’' : v.ssl_status === 'expiring' ? '⏰' : 'πŸ”“', + 'SSL: ' + v.ssl_status, + v.ssl_status === 'valid'), + // WAF + this.pill(v.waf_bypass ? '🚫' : 'πŸ›‘οΈ', + v.waf_bypass ? 'WAF Bypass' : 'WAF Active', + !v.waf_bypass), + // Active + this.pill(v.active ? 'βœ…' : '⏸️', + v.active ? 'Active' : 'Inactive', + v.active) ]), + // Action E('td', {}, [ - vhost.ssl_status === 'valid' ? this.renderPill('SSL', 'success') : - vhost.ssl_status === 'expiring' ? this.renderPill('Expiring', 'warning') : - vhost.ssl_status === 'expired' ? this.renderPill('Expired', 'danger') : - this.renderPill('No SSL', 'muted') - ]), - E('td', {}, [ - vhost.waf_bypass ? this.renderPill('Bypass', 'danger') : this.renderPill('WAF', 'info') - ]), - E('td', {}, [ - vhost.active ? this.renderPill('Active', 'success') : this.renderPill('Inactive', 'muted'), - missingRoutes ? E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { self.handleAddRoute(vhost.domain, vhost.backend_port); }, - 'style': 'margin-left:8px;padding:2px 8px;font-size:0.8em;' - }, _('+ Route')) : null + needsRoute ? E('button', { + 'class': 'btn btn-sm', + 'click': function() { self.handleAddRoute(v.domain, v.backend_port || 443); } + }, 'βž•') : null ]) ]); }, @@ -155,98 +187,100 @@ return view.extend({ render: function(data) { var self = this; var vhosts = data.vhosts || []; + this.allVhosts = vhosts; + this.totalVhosts = data.total || vhosts.length; + this.currentOffset = data.offset || 0; + this.statusData = data; - // Sort by domain - vhosts.sort(function(a, b) { - return a.domain.localeCompare(b.domain); - }); + // Quick stats from first page + var stats = { + active: vhosts.filter(function(v) { return v.active; }).length, + missing: vhosts.filter(function(v) { return !v.has_route_out || !v.has_route_in; }).length, + bypass: vhosts.filter(function(v) { return v.waf_bypass; }).length, + ssl: vhosts.filter(function(v) { return v.ssl_status === 'valid'; }).length + }; - // Stats - var totalVhosts = vhosts.length; - var activeVhosts = vhosts.filter(function(v) { return v.active; }).length; - var missingRoutes = vhosts.filter(function(v) { return !v.has_route_out || !v.has_route_in; }).length; - var wafBypassed = vhosts.filter(function(v) { return v.waf_bypass; }).length; - var sslValid = vhosts.filter(function(v) { return v.ssl_status === 'valid'; }).length; + var content = E('div', { 'class': 'vhosts-checker' }, [ + // Inline styles for dark theme compatibility + E('style', {}, [ + '.vhosts-checker { font-family: system-ui, sans-serif; }', + '.vhosts-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; }', + '.vhosts-title { font-size: 1.4em; font-weight: 600; display: flex; align-items: center; gap: 8px; }', + '.vhosts-stats { display: flex; gap: 16px; font-size: 0.9em; opacity: 0.8; }', + '.vhosts-stat { display: flex; align-items: center; gap: 4px; }', + '.services { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }', + '.service-badge { padding: 6px 12px; border-radius: 6px; display: flex; align-items: center; gap: 6px; font-size: 0.85em; background: rgba(255,255,255,0.1); }', + '.service-badge.ok { background: rgba(76, 175, 80, 0.2); }', + '.service-badge.err { background: rgba(244, 67, 54, 0.2); }', + '.vhosts-table { width: 100%; border-collapse: collapse; }', + '.vhosts-table th { text-align: left; padding: 8px; opacity: 0.7; font-weight: 500; border-bottom: 1px solid rgba(255,255,255,0.1); }', + '.vhost-row td { padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); }', + '.vhost-domain { color: #64b5f6; text-decoration: none; }', + '.vhost-domain:hover { text-decoration: underline; }', + '.vhost-status { display: flex; gap: 6px; }', + '.vhost-pill { font-size: 1.1em; cursor: help; opacity: 0.9; }', + '.vhost-pill.warn { opacity: 0.6; }', + '.load-more { text-align: center; padding: 16px; }', + '.btn-sm { padding: 4px 8px; font-size: 0.9em; }' + ].join('\n')), - var vhostRows = vhosts.map(function(v) { return self.renderVhostRow(v); }); - - var content = E('div', { 'class': 'routes-status-page' }, [ - // KISS Header - E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [ - E('div', {}, [ - E('h2', { 'class': 'sh-page-title' }, [ - E('span', { 'class': 'sh-page-title-icon' }, 'πŸ”€'), - _('Routes Status') - ]), - E('p', { 'class': 'sh-page-subtitle' }, - _('HAProxy vhosts and mitmproxy route configuration overview.')) + // Header + E('div', { 'class': 'vhosts-header' }, [ + E('div', { 'class': 'vhosts-title' }, [ + 'πŸ”€ ', _('VHosts Checker') ]), - E('div', { 'class': 'sh-header-meta' }, [ - this.renderHeaderChip('🌐', _('Vhosts'), totalVhosts), - this.renderHeaderChip('βœ…', _('Active'), activeVhosts), - this.renderHeaderChip('⚠️', _('Missing Routes'), missingRoutes, missingRoutes > 0 ? 'warn' : ''), - this.renderHeaderChip('πŸ›‘οΈ', _('WAF Bypass'), wafBypassed, wafBypassed > 0 ? 'warn' : ''), - this.renderHeaderChip('πŸ”’', _('SSL Valid'), sslValid) + E('div', { 'class': 'vhosts-stats' }, [ + E('span', { 'class': 'vhosts-stat' }, ['πŸ“Š ', this.totalVhosts, ' ', _('total')]), + E('span', { 'class': 'vhosts-stat' }, ['βœ… ', stats.active, '+', ' ', _('active')]), + stats.missing > 0 ? E('span', { 'class': 'vhosts-stat' }, ['⚠️ ', stats.missing, ' ', _('missing routes')]) : null, + stats.bypass > 0 ? E('span', { 'class': 'vhosts-stat' }, ['🚫 ', stats.bypass, ' ', _('WAF bypass')]) : null ]) ]), - // Service Status Cards - E('div', { 'class': 'sh-card-grid', 'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin:20px 0;' }, [ - E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);' }, [ - E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:8px;' }, [ - E('span', { 'style': 'font-size:1.5em;' }, 'βš–οΈ'), - E('strong', {}, 'HAProxy') - ]), - data.haproxy_running ? - this.renderPill('Running', 'success') : - this.renderPill('Stopped', 'danger') + // Service status badges + E('div', { 'class': 'services' }, [ + E('span', { 'class': 'service-badge' + (data.haproxy_running ? ' ok' : ' err') }, [ + data.haproxy_running ? 'βœ…' : '❌', ' HAProxy' ]), - E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);' }, [ - E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:8px;' }, [ - E('span', { 'style': 'font-size:1.5em;' }, 'πŸ”'), - E('strong', {}, 'mitmproxy') - ]), - data.mitmproxy_running ? - this.renderPill('Running', 'success') : - this.renderPill('Stopped', 'danger') + E('span', { 'class': 'service-badge' + (data.mitmproxy_running ? ' ok' : ' err') }, [ + data.mitmproxy_running ? 'βœ…' : '❌', ' mitmproxy' ]), - E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);' }, [ - E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:8px;' }, [ - E('span', { 'style': 'font-size:1.5em;' }, 'πŸ–₯️'), - E('strong', {}, _('Host IP')) - ]), - E('code', { 'style': 'background:#f5f5f5;padding:4px 8px;border-radius:4px;' }, data.host_ip || '192.168.255.1') - ]) - ]), - - // Actions - E('div', { 'style': 'margin:20px 0;' }, [ + E('span', { 'class': 'service-badge' }, ['πŸ–₯️ ', data.host_ip || '192.168.255.1']), E('button', { - 'class': 'cbi-button cbi-button-action', + 'class': 'btn cbi-button-action', 'click': function() { self.handleSync(); }, - 'style': 'margin-right:10px;' - }, 'πŸ”„ ' + _('Sync Routes from HAProxy')) + 'style': 'margin-left: auto;' + }, 'πŸ”„ ' + _('Sync')) ]), - // Vhosts Table - E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);overflow-x:auto;' }, [ - E('h3', { 'style': 'margin:0 0 16px 0;' }, '🌐 ' + _('Virtual Hosts (%d)').format(totalVhosts)), - vhosts.length > 0 ? - E('table', { 'class': 'table', 'style': 'width:100%;border-collapse:collapse;' }, [ - E('thead', {}, [ - E('tr', { 'style': 'background:#f5f5f5;' }, [ - E('th', { 'style': 'padding:10px;text-align:left;' }, _('Domain')), - E('th', { 'style': 'padding:10px;text-align:left;' }, _('Backend')), - E('th', { 'style': 'padding:10px;text-align:left;' }, _('Routes')), - E('th', { 'style': 'padding:10px;text-align:left;' }, _('SSL')), - E('th', { 'style': 'padding:10px;text-align:left;' }, _('WAF')), - E('th', { 'style': 'padding:10px;text-align:left;' }, _('Status')) - ]) - ]), - E('tbody', {}, vhostRows) - ]) : - E('p', { 'style': 'color:#666;text-align:center;padding:20px;' }, _('No virtual hosts configured.')) - ]) + // Table + vhosts.length > 0 ? + E('table', { 'class': 'vhosts-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Domain')), + E('th', {}, _('Status')), + E('th', { 'style': 'width: 50px;' }, '') + ]) + ]), + E('tbody', { 'id': 'vhosts-tbody' }, vhosts.map(function(v) { return self.renderRow(v); })) + ]) : + E('p', { 'style': 'text-align: center; opacity: 0.6; padding: 20px;' }, _('No vhosts found.')), + + // Load more + this.totalVhosts > vhosts.length ? E('div', { 'class': 'load-more' }, [ + E('button', { + 'id': 'load-more-btn', + 'class': 'btn cbi-button-neutral', + 'click': function() { self.handleLoadMore(); } + }, [ + 'πŸ“₯ ', _('Load More'), + ' (', + E('span', { 'id': 'vhosts-count' }, vhosts.length + '/' + this.totalVhosts), + ')' + ]), + E('p', { 'id': 'load-spinner', 'class': 'spinning', 'style': 'display: none;' }, _('Loading...')) + ]) : null ]); return KissTheme.wrap([content], 'admin/status/vhosts-checker'); diff --git a/package/secubox/luci-app-routes-status/root/usr/libexec/rpcd/luci.routes-status b/package/secubox/luci-app-routes-status/root/usr/libexec/rpcd/luci.routes-status index 45b25d43..f4cee045 100755 --- a/package/secubox/luci-app-routes-status/root/usr/libexec/rpcd/luci.routes-status +++ b/package/secubox/luci-app-routes-status/root/usr/libexec/rpcd/luci.routes-status @@ -1,6 +1,7 @@ #!/bin/sh # RPCD handler for Routes Status dashboard # Shows HAProxy vhosts and mitmproxy route configuration status +# Optimized with pagination . /usr/share/libubox/jshn.sh @@ -13,11 +14,24 @@ get_host_ip() { uci -q get network.lan.ipaddr || echo "192.168.255.1" } -# Main status method - optimized to fetch data once +# Main status method - optimized with pagination method_status() { + local offset=0 + local limit=50 + + # Read JSON input for pagination params + read -r input 2>/dev/null + if [ -n "$input" ]; then + json_load "$input" 2>/dev/null + json_get_var offset offset 2>/dev/null + json_get_var limit limit 2>/dev/null + fi + [ -z "$offset" ] && offset=0 + [ -z "$limit" ] && limit=50 + local host_ip=$(get_host_ip) - local haproxy_running=$(pgrep haproxy >/dev/null 2>&1 && echo "true" || echo "false") - local mitmproxy_running=$(pgrep -f mitmproxy >/dev/null 2>&1 && echo "true" || echo "false") + local haproxy_running=$(pgrep haproxy >/dev/null 2>&1 && echo "1" || echo "0") + local mitmproxy_running=$(pgrep -f mitmproxy >/dev/null 2>&1 && echo "1" || echo "0") # Fetch vhost list once to temp file local vhost_tmp="/tmp/vhosts_$$" @@ -27,41 +41,60 @@ method_status() { touch "$vhost_tmp" fi + # Count total vhosts + local total=$(wc -l < "$vhost_tmp" | tr -d ' ') + json_init json_add_boolean haproxy_running "$haproxy_running" json_add_boolean mitmproxy_running "$mitmproxy_running" json_add_string host_ip "$host_ip" + json_add_int total "$total" + json_add_int offset "$offset" + json_add_int limit "$limit" json_add_array vhosts - # Process vhost data line by line (using file redirection to avoid subshell) + # Process vhost data with pagination + local count=0 + local processed=0 while IFS= read -r line; do [ -z "$line" ] && continue + # Skip until offset + if [ "$count" -lt "$offset" ]; then + count=$((count + 1)) + continue + fi + + # Stop after limit + if [ "$processed" -ge "$limit" ]; then + break + fi + # Parse line: " domain.com -> backend_name [enabled] SSL ..." local domain=$(echo "$line" | awk '{print $1}') local backend=$(echo "$line" | awk '{print $3}') - local enabled=$(echo "$line" | grep -qF '[enabled]' && echo "true" || echo "false") + local enabled=$(echo "$line" | grep -qF '[enabled]' && echo "1" || echo "0") [ -z "$domain" ] && continue - # Check mitmproxy routes (grep for domain in JSON) - local has_route_out="false" - local has_route_in="false" - if [ -f "$MITMPROXY_ROUTES" ] && grep -qF "\"$domain\"" "$MITMPROXY_ROUTES" 2>/dev/null; then - has_route_out="true" + # Check mitmproxy routes (use 1/0 for jshn booleans) + local has_route_out="0" + local has_route_in="0" + if [ -f "$MITMPROXY_ROUTES" ] && grep "$domain" "$MITMPROXY_ROUTES" >/dev/null 2>&1; then + has_route_out="1" fi - if [ -f "$MITMPROXY_IN_ROUTES" ] && grep -qF "\"$domain\"" "$MITMPROXY_IN_ROUTES" 2>/dev/null; then - has_route_in="true" + if [ -f "$MITMPROXY_IN_ROUTES" ] && grep "$domain" "$MITMPROXY_IN_ROUTES" >/dev/null 2>&1; then + has_route_in="1" fi # Check SSL cert local ssl_status="missing" [ -f "$HAPROXY_CERTS/${domain}.pem" ] && ssl_status="valid" - # WAF bypass check - local waf_bypass="false" - [ "$backend" != "mitmproxy_inspector" ] && waf_bypass="true" + # WAF bypass check (1=bypassed, 0=waf active) + local waf_bypass="0" + [ "$backend" != "mitmproxy_inspector" ] && waf_bypass="1" json_add_object "" json_add_string domain "$domain" @@ -75,6 +108,9 @@ method_status() { json_add_string route_target_in "" json_add_boolean waf_bypass "$waf_bypass" json_close_object + + count=$((count + 1)) + processed=$((processed + 1)) done < "$vhost_tmp" json_close_array @@ -90,12 +126,12 @@ method_sync_routes() { if [ -x /usr/sbin/mitmproxyctl ]; then result=$(/usr/sbin/mitmproxyctl sync-routes 2>&1) json_init - json_add_boolean success true + json_add_boolean success 1 json_add_string output "$result" json_dump else json_init - json_add_boolean success false + json_add_boolean success 0 json_add_string error "mitmproxyctl not found" json_dump fi @@ -113,7 +149,7 @@ method_add_route() { if [ -z "$domain" ] || [ -z "$port" ]; then json_init - json_add_boolean success false + json_add_boolean success 0 json_add_string error "Missing domain or port parameter" json_dump return @@ -122,7 +158,7 @@ method_add_route() { local host_ip=$(get_host_ip) # Add route to both mitmproxy route files - local success="true" + local success="1" local errors="" for routes_file in "$MITMPROXY_ROUTES" "$MITMPROXY_IN_ROUTES"; do @@ -134,9 +170,7 @@ method_add_route() { '. + {($d): [$h, $p]}' "$routes_file" > "$tmpfile" 2>/dev/null else # Fallback: manual JSON manipulation - # Remove trailing } and add new entry sed 's/}$//' "$routes_file" > "$tmpfile" - # Check if file has content (not empty object) if grep -q '": \[' "$routes_file"; then printf ',\n "%s": ["%s", %s]\n}\n' "$domain" "$host_ip" "$port" >> "$tmpfile" else @@ -148,14 +182,14 @@ method_add_route() { mv "$tmpfile" "$routes_file" else rm -f "$tmpfile" - success="false" + success="0" errors="$errors Failed to update $routes_file." fi fi done # Restart mitmproxy to apply changes - if [ "$success" = "true" ]; then + if [ "$success" = "1" ]; then /etc/init.d/mitmproxy restart >/dev/null 2>&1 fi @@ -169,6 +203,8 @@ method_add_route() { list_methods() { json_init json_add_object status + json_add_int offset 0 + json_add_int limit 50 json_close_object json_add_object sync_routes json_close_object